blob: c1846e3cd5fd75d96088c28e982578dc87f7011c [file] [log] [blame]
Luis Hector Chaveza1518052018-06-14 08:19:34 -07001#!/usr/bin/env python2
2# -*- coding: utf-8 -*-
Chris Sosa7cd23202013-10-15 17:22:57 -07003# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Integration tests for the devserver.
8
9This module is responsible for testing the actual devserver APIs and should be
10run whenever changes are made to the devserver.
11
xixuan52c2fba2016-05-20 17:02:48 -070012To run the integration test for devserver:
13 python ./devserver_integration_test.py
Chris Sosa7cd23202013-10-15 17:22:57 -070014"""
15
Gabe Black3b567202015-09-23 14:07:59 -070016from __future__ import print_function
17
Chris Sosa7cd23202013-10-15 17:22:57 -070018import json
Chris Sosa7cd23202013-10-15 17:22:57 -070019import os
Chris Sosa7cd23202013-10-15 17:22:57 -070020import shutil
Gilad Arnold7de05f72014-02-14 13:14:20 -080021import socket
Chris Sosa7cd23202013-10-15 17:22:57 -070022import subprocess
Amin Hassaniba847c82019-12-06 16:30:56 -080023import sys
Chris Sosa7cd23202013-10-15 17:22:57 -070024import tempfile
25import time
26import unittest
Chris Sosa7cd23202013-10-15 17:22:57 -070027
Amin Hassani0b4843b2019-09-25 16:38:56 -070028from string import Template
29
30from xml.dom import minidom
31
Amin Hassani6eec8792020-01-09 14:06:48 -080032import requests
33
Amin Hassani469f5702019-10-21 15:35:06 -070034from six.moves import urllib
35
36import psutil # pylint: disable=import-error
37
Amin Hassani469f5702019-10-21 15:35:06 -070038import setup_chromite # pylint: disable=unused-import
Luis Hector Chaveza1518052018-06-14 08:19:34 -070039from chromite.lib import cros_logging as logging
Amin Hassanie427e212019-10-28 11:04:27 -070040from chromite.lib import cros_update_progress
Achuith Bhandarkar662fb722019-10-31 16:12:49 -070041from chromite.lib.xbuddy import devserver_constants
Luis Hector Chaveza1518052018-06-14 08:19:34 -070042
Chris Sosa7cd23202013-10-15 17:22:57 -070043
44# Paths are relative to this script's base directory.
45LABEL = 'devserver'
46TEST_IMAGE_PATH = 'testdata/devserver'
Amin Hassani0b4843b2019-09-25 16:38:56 -070047TEST_UPDATE_PAYLOAD_NAME = 'update.gz'
48TEST_UPDATE_PAYLOAD_METADATA_NAME = 'update.gz.json'
Chris Sosa7cd23202013-10-15 17:22:57 -070049
50# Update request based on Omaha v3 protocol format.
Amin Hassani0b4843b2019-09-25 16:38:56 -070051UPDATE_REQUEST = Template("""<?xml version="1.0" encoding="UTF-8"?>
Amin Hassani495f1de2019-02-26 11:13:39 -080052<request protocol="3.0" updater="ChromeOSUpdateEngine" updaterversion="0.1.0.0" ismachine="1">
Chris Sosa7cd23202013-10-15 17:22:57 -070053 <os version="Indy" platform="Chrome OS" sp="0.11.254.2011_03_09_1814_i686"></os>
Amin Hassani0b4843b2019-09-25 16:38:56 -070054 <app appid="$appid" version="11.254.2011_03_09_1814" lang="en-US" track="developer-build" board="x86-generic" hardware_class="BETA DVT" delta_okay="true">
Chris Sosa7cd23202013-10-15 17:22:57 -070055 <updatecheck></updatecheck>
56 </app>
57</request>
Amin Hassani0b4843b2019-09-25 16:38:56 -070058""")
Amin Hassaniaef2b292020-01-10 10:44:16 -080059DOWNLOAD_STARTED_REQUEST = Template("""<?xml version="1.0" encoding="UTF-8"?>
60<request protocol="3.0" updater="ChromeOSUpdateEngine" updaterversion="0.1.0.0" ismachine="1">
61 <os version="Indy" platform="Chrome OS" sp="0.11.254.2011_03_09_1814_i686"></os>
62 <app appid="$appid" version="11.254.2011_03_09_1814" lang="en-US" track="developer-build" board="x86-generic" hardware_class="BETA DVT" delta_okay="true">
63 <event eventtype="13" eventresult="1"></event>
64 </app>
65</request>
66""")
Chris Sosa7cd23202013-10-15 17:22:57 -070067
68# RPC constants.
69STAGE = 'stage'
70IS_STAGED = 'is_staged'
71STATIC = 'static'
72UPDATE = 'update'
73CHECK_HEALTH = 'check_health'
74CONTROL_FILES = 'controlfiles'
75XBUDDY = 'xbuddy'
Prashanth Ba06d2d22014-03-07 15:35:19 -080076LIST_IMAGE_DIR = 'list_image_dir'
Chris Sosa7cd23202013-10-15 17:22:57 -070077
78# API rpcs and constants.
Chris Sosa7cd23202013-10-15 17:22:57 -070079API_SET_UPDATE_REQUEST = 'new_update-test/the-new-update'
80API_TEST_IP_ADDR = '127.0.0.1'
81
82DEVSERVER_START_TIMEOUT = 15
Gilad Arnold08516112014-02-14 13:14:03 -080083DEVSERVER_START_SLEEP = 1
Gilad Arnold7de05f72014-02-14 13:14:20 -080084MAX_START_ATTEMPTS = 5
Chris Sosa7cd23202013-10-15 17:22:57 -070085
86
87class DevserverFailedToStart(Exception):
88 """Raised if we could not start the devserver."""
89
90
Gilad Arnold08516112014-02-14 13:14:03 -080091class DevserverTestBase(unittest.TestCase):
Chris Sosa7cd23202013-10-15 17:22:57 -070092 """Class containing common logic between devserver test classes."""
93
94 def setUp(self):
Gilad Arnold08516112014-02-14 13:14:03 -080095 """Creates and populates a test directory, temporary files."""
Chris Sosa7cd23202013-10-15 17:22:57 -070096 self.test_data_path = tempfile.mkdtemp()
97 self.src_dir = os.path.dirname(__file__)
98
Gilad Arnold08516112014-02-14 13:14:03 -080099 # Copy the payload to the location of the update label.
Amin Hassani0b4843b2019-09-25 16:38:56 -0700100 self._CreateLabelAndCopyUpdatePayloadFiles(LABEL)
Chris Sosa7cd23202013-10-15 17:22:57 -0700101
102 # Copy the payload to the location of forced label.
Amin Hassani0b4843b2019-09-25 16:38:56 -0700103 self._CreateLabelAndCopyUpdatePayloadFiles(API_SET_UPDATE_REQUEST)
Chris Sosa7cd23202013-10-15 17:22:57 -0700104
Gilad Arnold08516112014-02-14 13:14:03 -0800105 # Allocate temporary files for various devserver outputs.
106 self.pidfile = self._MakeTempFile('pid')
107 self.portfile = self._MakeTempFile('port')
108 self.logfile = self._MakeTempFile('log')
Chris Sosa7cd23202013-10-15 17:22:57 -0700109
Gilad Arnold08516112014-02-14 13:14:03 -0800110 # Initialize various runtime values.
111 self.devserver_url = self.port = self.pid = None
Amin Hassaniba847c82019-12-06 16:30:56 -0800112 self.devserver = None
Chris Sosa7cd23202013-10-15 17:22:57 -0700113
114 def tearDown(self):
Gilad Arnold08516112014-02-14 13:14:03 -0800115 """Kill the server, remove the test directory and temporary files."""
Amin Hassaniba847c82019-12-06 16:30:56 -0800116
117 self._StopServer()
Chris Sosa7cd23202013-10-15 17:22:57 -0700118
Gilad Arnold08516112014-02-14 13:14:03 -0800119 self._RemoveFile(self.pidfile)
120 self._RemoveFile(self.portfile)
Amin Hassaniba847c82019-12-06 16:30:56 -0800121 # If the unittest did not succeed, print out the devserver log.
122 if sys.exc_info() != (None, None, None):
123 with open(self.logfile, 'r') as f:
124 logging.info('--- BEGINNING OF DEVSERVER LOG ---')
125 logging.info(f.read())
126 logging.info('--- ENDING OF DEVSERVER LOG ---')
Gilad Arnold08516112014-02-14 13:14:03 -0800127 self._RemoveFile(self.logfile)
128 shutil.rmtree(self.test_data_path)
Chris Sosa7cd23202013-10-15 17:22:57 -0700129
130 # Helper methods begin here.
131
Amin Hassani0b4843b2019-09-25 16:38:56 -0700132 def _CreateLabelAndCopyUpdatePayloadFiles(self, label):
Gilad Arnold08516112014-02-14 13:14:03 -0800133 """Creates a label location and copies an image to it."""
Amin Hassani0b4843b2019-09-25 16:38:56 -0700134 update_dir = os.path.join(self.src_dir, TEST_IMAGE_PATH)
Gilad Arnold08516112014-02-14 13:14:03 -0800135 label_dir = os.path.join(self.test_data_path, label)
136 os.makedirs(label_dir)
Amin Hassani0b4843b2019-09-25 16:38:56 -0700137 for name in (TEST_UPDATE_PAYLOAD_NAME, TEST_UPDATE_PAYLOAD_METADATA_NAME):
138 shutil.copy(os.path.join(update_dir, name), label_dir)
Gilad Arnold08516112014-02-14 13:14:03 -0800139
140 def _MakeTempFile(self, suffix):
141 """Return path of a newly created temporary file."""
142 with tempfile.NamedTemporaryFile(suffix='-devserver-%s' % suffix) as f:
143 name = f.name
144 f.close()
145
146 return name
147
148 def _RemoveFile(self, filename):
149 """Removes a file if it is present."""
150 if os.path.isfile(filename):
151 os.remove(filename)
152
153 def _ReadIntValueFromFile(self, path, desc):
154 """Reads a string from file and returns its conversion into an integer."""
155 if not os.path.isfile(path):
156 raise DevserverFailedToStart('Devserver did not drop %s (%r).' %
157 (desc, path))
158
159 with open(path) as f:
160 value_str = f.read()
161
162 try:
163 return int(value_str)
164 except ValueError:
165 raise DevserverFailedToStart('Devserver did not drop a valid value '
166 'in %s (%r).' % (desc, value_str))
167
168 def _StartServer(self, port=0):
Chris Sosa7cd23202013-10-15 17:22:57 -0700169 """Attempts to start devserver on |port|.
170
Gilad Arnold08516112014-02-14 13:14:03 -0800171 In the default case where port == 0, the server will bind to an arbitrary
Dan Shi2f136862016-02-11 15:38:38 -0800172 available port. If successful, this method will set the devserver's pid
Gilad Arnold08516112014-02-14 13:14:03 -0800173 (self.pid), actual listening port (self.port) and URL (self.devserver_url).
Chris Sosa7cd23202013-10-15 17:22:57 -0700174
175 Raises:
176 DevserverFailedToStart: If the devserver could not be started.
177 """
178 cmd = [
Chris Sosa7cd23202013-10-15 17:22:57 -0700179 os.path.join(self.src_dir, 'devserver.py'),
Chris Sosa7cd23202013-10-15 17:22:57 -0700180 '--static_dir', self.test_data_path,
181 '--pidfile', self.pidfile,
Gilad Arnold08516112014-02-14 13:14:03 -0800182 '--portfile', self.portfile,
Chris Sosa7cd23202013-10-15 17:22:57 -0700183 '--port', str(port),
Amin Hassaniba847c82019-12-06 16:30:56 -0800184 '--logfile', self.logfile,
185 ]
Chris Sosa7cd23202013-10-15 17:22:57 -0700186
187 # Pipe all output. Use logfile to get devserver log.
Amin Hassaniba847c82019-12-06 16:30:56 -0800188 self.devserver = subprocess.Popen(cmd, stderr=subprocess.PIPE,
189 stdout=subprocess.PIPE)
Chris Sosa7cd23202013-10-15 17:22:57 -0700190
Gilad Arnold08516112014-02-14 13:14:03 -0800191 # Wait for devserver to start, determining its actual serving port and URL.
Chris Sosa7cd23202013-10-15 17:22:57 -0700192 current_time = time.time()
193 deadline = current_time + DEVSERVER_START_TIMEOUT
Amin Hassaniba847c82019-12-06 16:30:56 -0800194 error = None
Chris Sosa7cd23202013-10-15 17:22:57 -0700195 while current_time < deadline:
Chris Sosa7cd23202013-10-15 17:22:57 -0700196 try:
Gilad Arnold08516112014-02-14 13:14:03 -0800197 self.port = self._ReadIntValueFromFile(self.portfile, 'portfile')
198 self.devserver_url = 'http://127.0.0.1:%d' % self.port
Gabe Black3b567202015-09-23 14:07:59 -0700199 self._MakeRPC(CHECK_HEALTH, timeout=1)
Chris Sosa7cd23202013-10-15 17:22:57 -0700200 break
Amin Hassaniba847c82019-12-06 16:30:56 -0800201 except Exception as e:
202 error = e
Gilad Arnold08516112014-02-14 13:14:03 -0800203 time.sleep(DEVSERVER_START_SLEEP)
204 current_time = time.time()
Chris Sosa7cd23202013-10-15 17:22:57 -0700205 else:
Amin Hassaniba847c82019-12-06 16:30:56 -0800206 raise DevserverFailedToStart(
207 'Devserver failed to start within timeout with error: %s' % error)
Chris Sosa7cd23202013-10-15 17:22:57 -0700208
Gilad Arnold08516112014-02-14 13:14:03 -0800209 # Retrieve PID.
210 self.pid = self._ReadIntValueFromFile(self.pidfile, 'pidfile')
Chris Sosa7cd23202013-10-15 17:22:57 -0700211
Amin Hassaniba847c82019-12-06 16:30:56 -0800212 def _StopServer(self):
213 """Stops the current running devserver."""
214 if not self.pid:
215 return
216
217 self.devserver.terminate()
218
219 # Just to flush the stdout/stderr so python3 doesn't complain about the
220 # unclosed file.
221 self.devserver.communicate()
222
223 self.devserver.wait()
224
225 self.pid = None
226 self.devserver = None
227
228
Amin Hassani0b4843b2019-09-25 16:38:56 -0700229 def VerifyHandleUpdate(self, label, use_test_payload=True,
230 appid='{DEV-BUILD}'):
Chris Sosa7cd23202013-10-15 17:22:57 -0700231 """Verifies that we can send an update request to the devserver.
232
233 This method verifies (using a fake update_request blob) that the devserver
234 can interpret the payload and give us back the right payload.
235
236 Args:
237 label: Label that update is served from e.g. <board>-release/<version>
238 use_test_payload: If set to true, expects to serve payload under
239 testdata/ and does extra checks i.e. compares hash and content of
240 payload.
Amin Hassaniba847c82019-12-06 16:30:56 -0800241 appid: The APP ID of the board.
Gilad Arnold08516112014-02-14 13:14:03 -0800242
Chris Sosa7cd23202013-10-15 17:22:57 -0700243 Returns:
244 url of the update payload if we verified the update.
245 """
246 update_label = '/'.join([UPDATE, label])
Amin Hassani0b4843b2019-09-25 16:38:56 -0700247 response = self._MakeRPC(
Amin Hassani6eec8792020-01-09 14:06:48 -0800248 update_label, data=UPDATE_REQUEST.substitute({'appid': appid}),
249 critical_update=True)
Chris Sosa7cd23202013-10-15 17:22:57 -0700250 self.assertNotEqual('', response)
Amin Hassani6eec8792020-01-09 14:06:48 -0800251 self.assertIn('deadline="now"', response)
Chris Sosa7cd23202013-10-15 17:22:57 -0700252
253 # Parse the response and check if it contains the right result.
254 dom = minidom.parseString(response)
255 update = dom.getElementsByTagName('updatecheck')[0]
256 expected_static_url = '/'.join([self.devserver_url, STATIC, label])
Amin Hassani0b4843b2019-09-25 16:38:56 -0700257 url = self.VerifyV3Response(update, expected_static_url)
Chris Sosa7cd23202013-10-15 17:22:57 -0700258
259 # Verify the image we download is correct since we already know what it is.
260 if use_test_payload:
Amin Hassani469f5702019-10-21 15:35:06 -0700261 connection = urllib.request.urlopen(url)
Amin Hassaniba847c82019-12-06 16:30:56 -0800262 contents = connection.read().decode('utf-8')
Chris Sosa7cd23202013-10-15 17:22:57 -0700263 connection.close()
264 self.assertEqual('Developers, developers, developers!\n', contents)
265
266 return url
267
Amin Hassani0b4843b2019-09-25 16:38:56 -0700268 def VerifyV3Response(self, update, expected_static_url):
Chris Sosa7cd23202013-10-15 17:22:57 -0700269 """Verifies the update DOM from a v3 response and returns the url."""
270 # Parse the response and check if it contains the right result.
271 urls = update.getElementsByTagName('urls')[0]
272 url = urls.getElementsByTagName('url')[0]
273
274 static_url = url.getAttribute('codebase')
275 # Static url's end in /.
276 self.assertEqual(expected_static_url + '/', static_url)
277
278 manifest = update.getElementsByTagName('manifest')[0]
279 packages = manifest.getElementsByTagName('packages')[0]
280 package = packages.getElementsByTagName('package')[0]
281 filename = package.getAttribute('name')
Amin Hassani0b4843b2019-09-25 16:38:56 -0700282 self.assertEqual(TEST_UPDATE_PAYLOAD_NAME, filename)
Chris Sosa7cd23202013-10-15 17:22:57 -0700283
Amin Hassaniba847c82019-12-06 16:30:56 -0800284 return os.path.join(static_url, filename)
Chris Sosa7cd23202013-10-15 17:22:57 -0700285
286 def _MakeRPC(self, rpc, data=None, timeout=None, **kwargs):
Gilad Arnold08516112014-02-14 13:14:03 -0800287 """Makes an RPC call to the devserver.
Chris Sosa7cd23202013-10-15 17:22:57 -0700288
289 Args:
Gilad Arnold08516112014-02-14 13:14:03 -0800290 rpc: The function to run on the devserver, e.g. 'stage'.
Chris Sosa7cd23202013-10-15 17:22:57 -0700291 data: Optional post data to send.
292 timeout: Optional timeout to pass to urlopen.
Gilad Arnold08516112014-02-14 13:14:03 -0800293 kwargs: Optional arguments to the function, e.g. artifact_url='foo/bar'.
Chris Sosa7cd23202013-10-15 17:22:57 -0700294
Gilad Arnold08516112014-02-14 13:14:03 -0800295 Returns:
296 The function output.
Chris Sosa7cd23202013-10-15 17:22:57 -0700297 """
298 request = '/'.join([self.devserver_url, rpc])
299 if kwargs:
300 # Join the kwargs to the URL.
Amin Hassani6eec8792020-01-09 14:06:48 -0800301 request += '?' + '&'.join('%s=%s' % (k, v) for k, v in kwargs.items())
Chris Sosa7cd23202013-10-15 17:22:57 -0700302
Amin Hassani6eec8792020-01-09 14:06:48 -0800303 response = (requests.post(request, data=data, timeout=timeout) if data
304 else requests.get(request, timeout=timeout))
305 response.raise_for_status()
306 return response.text
Chris Sosa7cd23202013-10-15 17:22:57 -0700307
308
Gilad Arnold08516112014-02-14 13:14:03 -0800309class AutoStartDevserverTestBase(DevserverTestBase):
310 """Test base class that automatically starts the devserver."""
311
312 def setUp(self):
313 """Initialize everything, then start the server."""
314 super(AutoStartDevserverTestBase, self).setUp()
315 self._StartServer()
316
317
Gilad Arnold7de05f72014-02-14 13:14:20 -0800318class DevserverStartTests(DevserverTestBase):
319 """Test that devserver starts up correctly."""
320
321 def testStartAnyPort(self):
322 """Starts the devserver, have it bind to an arbitrary available port."""
323 self._StartServer()
324
325 def testStartSpecificPort(self):
326 """Starts the devserver with a specific port."""
327 for _ in range(MAX_START_ATTEMPTS):
328 # This is a cheap hack to find an arbitrary unused port: we open a socket
329 # and bind it to port zero, then pull out the actual port number and
330 # close the socket. In all likelihood, this will leave us with an
331 # available port number that we can use for starting the devserver.
332 # However, this heuristic is susceptible to race conditions, hence the
333 # retry loop.
334 s = socket.socket()
335 s.bind(('', 0))
Gilad Arnold7de05f72014-02-14 13:14:20 -0800336 _, port = s.getsockname()
337 s.close()
338
339 self._StartServer(port=port)
Amin Hassaniba847c82019-12-06 16:30:56 -0800340 self._StopServer()
Gilad Arnold7de05f72014-02-14 13:14:20 -0800341
342
Gilad Arnold08516112014-02-14 13:14:03 -0800343class DevserverBasicTests(AutoStartDevserverTestBase):
344 """Short running tests for the devserver (no remote deps).
Chris Sosa7cd23202013-10-15 17:22:57 -0700345
346 These are technically not unittests because they depend on being able to
347 start a devserver locally which technically requires external resources so
348 they are lumped with the remote tests here.
349 """
350
351 def testHandleUpdateV3(self):
352 self.VerifyHandleUpdate(label=LABEL)
353
Chris Sosa7cd23202013-10-15 17:22:57 -0700354 def testXBuddyLocalAlias(self):
355 """Extensive local image xbuddy unittest.
356
357 This test verifies all the local xbuddy logic by creating a new local folder
358 with the necessary update items and verifies we can use all of them.
359 """
Chris Sosa7cd23202013-10-15 17:22:57 -0700360 build_id = 'x86-generic/R32-9999.0.0-a1'
361 xbuddy_path = 'x86-generic/R32-9999.0.0-a1/test'
362 build_dir = os.path.join(self.test_data_path, build_id)
363 os.makedirs(build_dir)
Amin Hassani0b4843b2019-09-25 16:38:56 -0700364
365 # Writing dummy files.
366 image_data = 'TEST IMAGE'
Chris Sosa7cd23202013-10-15 17:22:57 -0700367 test_image_file = os.path.join(build_dir,
368 devserver_constants.TEST_IMAGE_FILE)
Amin Hassani0b4843b2019-09-25 16:38:56 -0700369 with open(test_image_file, 'w') as f:
370 f.write(image_data)
371
372 stateful_data = 'STATEFUL STUFFS'
Chris Sosa7cd23202013-10-15 17:22:57 -0700373 stateful_file = os.path.join(build_dir, devserver_constants.STATEFUL_FILE)
Amin Hassani0b4843b2019-09-25 16:38:56 -0700374 with open(stateful_file, 'w') as f:
375 f.write(stateful_data)
Chris Sosa7cd23202013-10-15 17:22:57 -0700376
Amin Hassani0b4843b2019-09-25 16:38:56 -0700377 update_dir = os.path.join(self.src_dir, TEST_IMAGE_PATH)
378 for name in (TEST_UPDATE_PAYLOAD_NAME, TEST_UPDATE_PAYLOAD_METADATA_NAME):
379 shutil.copy(os.path.join(update_dir, name), build_dir)
380 with open(os.path.join(build_dir, TEST_UPDATE_PAYLOAD_NAME), 'r') as f:
381 update_data = f.read()
Chris Sosa7cd23202013-10-15 17:22:57 -0700382
Amin Hassani0b4843b2019-09-25 16:38:56 -0700383 for item, data in zip(['full_payload', 'test', 'stateful'],
384 [update_data, image_data, stateful_data]):
Chris Sosa7cd23202013-10-15 17:22:57 -0700385
386 xbuddy_path = '/'.join([build_id, item])
387 logging.info('Testing xbuddy path %s', xbuddy_path)
388 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]))
389 self.assertEqual(response, data)
390
391 expected_dir = '/'.join([self.devserver_url, STATIC, build_id])
392 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]), return_dir=True)
393 self.assertEqual(response, expected_dir)
394
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800395 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]),
396 relative_path=True)
397 self.assertEqual(response, build_id)
398
Chris Sosa7cd23202013-10-15 17:22:57 -0700399 xbuddy_path = '/'.join([build_id, 'test'])
400 logging.info('Testing for_update for %s', xbuddy_path)
401 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]), for_update=True)
402 expected_path = '/'.join([self.devserver_url, UPDATE, build_id])
403 self.assertTrue(response, expected_path)
404
405 logging.info('Verifying the actual payload data')
406 url = self.VerifyHandleUpdate(build_id, use_test_payload=False)
407 logging.info('Verify the actual content of the update payload')
Amin Hassani469f5702019-10-21 15:35:06 -0700408 connection = urllib.request.urlopen(url)
Amin Hassaniba847c82019-12-06 16:30:56 -0800409 contents = connection.read().decode('utf-8')
Chris Sosa7cd23202013-10-15 17:22:57 -0700410 connection.close()
411 self.assertEqual(update_data, contents)
412
Amin Hassaniaef2b292020-01-10 10:44:16 -0800413 def testMaxUpdates(self):
414 """Tests MaxUpdates functionality of autoupdate."""
415 update_label = '/'.join([UPDATE, LABEL])
416 update_request = UPDATE_REQUEST.substitute({'appid': '{DEV-BUILD}'})
417 download_started_request = DOWNLOAD_STARTED_REQUEST.substitute(
418 {'appid': '{DEV-BUILD}'})
419
420 session = 'foo-id'
421 post_data = json.dumps({'max_updates': 2})
422 self._MakeRPC('session/%s' % session, data=post_data)
423
424 # The first request should provide an actual update.
425 response = self._MakeRPC(update_label, data=update_request)
426 self.assertIn('updatecheck status="ok"', response)
427 # Tell devserver we started to download.
428 response = self._MakeRPC(update_label, data=download_started_request,
429 session=session)
430
431 # Another update request also should go fine.
432 response = self._MakeRPC(update_label, data=update_request,
433 session=session)
434 self.assertIn('updatecheck status="ok"', response)
435 # Tell devserver we started to download.
436 response = self._MakeRPC(update_label, data=download_started_request,
437 session=session)
438
439 # But the third update request should not be valid anymore since we hit the
440 # max updates ceiling.
441 response = self._MakeRPC(update_label, data=update_request,
442 session=session)
443 self.assertIn('updatecheck status="noupdate"', response)
444
Chris Sosa7cd23202013-10-15 17:22:57 -0700445 def testPidFile(self):
446 """Test that using a pidfile works correctly."""
447 with open(self.pidfile, 'r') as f:
448 pid = f.read()
Chris Sosa7cd23202013-10-15 17:22:57 -0700449 # Let's assert some process information about the devserver.
Dan Shi2f136862016-02-11 15:38:38 -0800450 self.assertTrue(pid.strip().isdigit())
Chris Sosa7cd23202013-10-15 17:22:57 -0700451 process = psutil.Process(int(pid))
452 self.assertTrue(process.is_running())
Amin Hassaniba847c82019-12-06 16:30:56 -0800453 self.assertIn('./devserver.py', process.cmdline())
Chris Sosa7cd23202013-10-15 17:22:57 -0700454
Amin Hassani28df4212019-10-28 10:16:50 -0700455 def testFileInfo(self):
456 """Verifies the fileinfo API."""
457 response = self._MakeRPC('/'.join(['api', 'fileinfo', LABEL,
458 TEST_UPDATE_PAYLOAD_NAME]))
459 self.assertEqual(
460 response,
461 '{"sha256": "hgfZ19hcsA5OGUN3f4SDtoO0kzT24o+JsGgjNVCVEy0=", '
462 '"size": 36}')
463
Chris Sosa7cd23202013-10-15 17:22:57 -0700464
Gilad Arnold08516112014-02-14 13:14:03 -0800465class DevserverExtendedTests(AutoStartDevserverTestBase):
Chris Sosa7cd23202013-10-15 17:22:57 -0700466 """Longer running integration tests that test interaction with Google Storage.
467
468 Note: due to the interaction with Google Storage, these tests both require
469 1) runner has access to the Google Storage bucket where builders store builds.
470 2) time. These tests actually download the artifacts needed.
471 """
472
xixuan52c2fba2016-05-20 17:02:48 -0700473 def testCrosAU(self):
474 """Tests core autotest workflow where we trigger CrOS auto-update.
475
476 It mainly tests the following API:
477 a. 'get_au_status'
478 b. 'handler_cleanup'
479 c. 'kill_au_proc'
480 """
481 host_name = '100.0.0.0'
xixuan2a0970a2016-08-10 12:12:44 -0700482 p = subprocess.Popen(['sleep 100'], shell=True, preexec_fn=os.setsid)
483 pid = os.getpgid(p.pid)
xixuan52c2fba2016-05-20 17:02:48 -0700484 status = 'updating'
485 progress_tracker = cros_update_progress.AUProgress(host_name, pid)
486 progress_tracker.WriteStatus(status)
487
488 logging.info('Retrieving auto-update status for process %d', pid)
489 response = self._MakeRPC('get_au_status', host_name=host_name, pid=pid)
Luis Hector Chaveza1518052018-06-14 08:19:34 -0700490 self.assertFalse(json.loads(response)['finished'])
491 self.assertEqual(json.loads(response)['status'], status)
xixuan52c2fba2016-05-20 17:02:48 -0700492
493 progress_tracker.WriteStatus(cros_update_progress.FINISHED)
494 logging.info('Mock auto-update process is finished')
495 response = self._MakeRPC('get_au_status', host_name=host_name, pid=pid)
Luis Hector Chaveza1518052018-06-14 08:19:34 -0700496 self.assertTrue(json.loads(response)['finished'])
497 self.assertEqual(json.loads(response)['status'],
498 cros_update_progress.FINISHED)
xixuan52c2fba2016-05-20 17:02:48 -0700499
500 logging.info('Delete auto-update track status file')
501 self.assertTrue(os.path.exists(progress_tracker.track_status_file))
502 self._MakeRPC('handler_cleanup', host_name=host_name, pid=pid)
503 self.assertFalse(os.path.exists(progress_tracker.track_status_file))
504
505 logging.info('Kill the left auto-update processes for host %s', host_name)
506 progress_tracker.WriteStatus(cros_update_progress.FINISHED)
507 response = self._MakeRPC('kill_au_proc', host_name=host_name)
508 self.assertEqual(response, 'True')
509 self.assertFalse(os.path.exists(progress_tracker.track_status_file))
510 self.assertFalse(cros_update_progress.IsProcessAlive(pid))
511
Amin Hassaniba847c82019-12-06 16:30:56 -0800512 p.terminate()
513 p.wait()
xixuan52c2fba2016-05-20 17:02:48 -0700514
Chris Sosa7cd23202013-10-15 17:22:57 -0700515 def testStageAndUpdate(self):
Luis Hector Chaveza1518052018-06-14 08:19:34 -0700516 """Tests core stage/update autotest workflow where with a test payload."""
Amin Hassani0b4843b2019-09-25 16:38:56 -0700517 build_id = 'eve-release/R78-12499.0.0'
Chris Sosa7cd23202013-10-15 17:22:57 -0700518 archive_url = 'gs://chromeos-image-archive/%s' % build_id
519
520 response = self._MakeRPC(IS_STAGED, archive_url=archive_url,
521 artifacts='full_payload,stateful')
522 self.assertEqual(response, 'False')
523
524 logging.info('Staging update artifacts')
525 self._MakeRPC(STAGE, archive_url=archive_url,
526 artifacts='full_payload,stateful')
527 logging.info('Staging complete. '
528 'Verifying files exist and are staged in the staging '
529 'directory.')
530 response = self._MakeRPC(IS_STAGED, archive_url=archive_url,
531 artifacts='full_payload,stateful')
532 self.assertEqual(response, 'True')
533 staged_dir = os.path.join(self.test_data_path, build_id)
534 self.assertTrue(os.path.isdir(staged_dir))
535 self.assertTrue(os.path.exists(
536 os.path.join(staged_dir, devserver_constants.UPDATE_FILE)))
537 self.assertTrue(os.path.exists(
Amin Hassani0b4843b2019-09-25 16:38:56 -0700538 os.path.join(staged_dir, devserver_constants.UPDATE_METADATA_FILE)))
539 self.assertTrue(os.path.exists(
Chris Sosa7cd23202013-10-15 17:22:57 -0700540 os.path.join(staged_dir, devserver_constants.STATEFUL_FILE)))
541
542 logging.info('Verifying we can update using the stage update artifacts.')
Amin Hassani0b4843b2019-09-25 16:38:56 -0700543 self.VerifyHandleUpdate(build_id, use_test_payload=False,
544 appid='{01906EA2-3EB2-41F1-8F62-F0B7120EFD2E}')
Chris Sosa7cd23202013-10-15 17:22:57 -0700545
Luis Hector Chaveza1518052018-06-14 08:19:34 -0700546 @unittest.skip('crbug.com/640063 Broken test.')
Chris Sosa7cd23202013-10-15 17:22:57 -0700547 def testStageAutotestAndGetPackages(self):
Luis Hector Chaveza1518052018-06-14 08:19:34 -0700548 """Another stage/update autotest workflow test with a test payload."""
549 build_id = 'eve-release/R69-10782.0.0'
Chris Sosa7cd23202013-10-15 17:22:57 -0700550 archive_url = 'gs://chromeos-image-archive/%s' % build_id
551 autotest_artifacts = 'autotest,test_suites,au_suite'
552 logging.info('Staging autotest artifacts (may take a while).')
553 self._MakeRPC(STAGE, archive_url=archive_url, artifacts=autotest_artifacts)
554
555 response = self._MakeRPC(IS_STAGED, archive_url=archive_url,
556 artifacts=autotest_artifacts)
557 self.assertEqual(response, 'True')
558
559 # Verify the files exist and are staged in the staging directory.
560 logging.info('Checking directories exist after we staged the files.')
561 staged_dir = os.path.join(self.test_data_path, build_id)
562 autotest_dir = os.path.join(staged_dir, 'autotest')
563 package_dir = os.path.join(autotest_dir, 'packages')
564 self.assertTrue(os.path.isdir(staged_dir))
565 self.assertTrue(os.path.isdir(autotest_dir))
566 self.assertTrue(os.path.isdir(package_dir))
567
568 control_files = self._MakeRPC(CONTROL_FILES, build=build_id,
569 suite_name='bvt')
570 logging.info('Checking for known control file in bvt suite.')
Amin Hassaniba847c82019-12-06 16:30:56 -0800571 self.assertIn('client/site_tests/platform_FilePerms/control', control_files)
Chris Sosa7cd23202013-10-15 17:22:57 -0700572
573 def testRemoteXBuddyAlias(self):
Luis Hector Chaveza1518052018-06-14 08:19:34 -0700574 """Another stage/update autotest workflow test with a test payload."""
575 build_id = 'eve-release/R69-10782.0.0'
576 xbuddy_path = 'remote/eve/R69-10782.0.0/full_payload'
577 xbuddy_bad_path = 'remote/eve/R32-9999.9999.9999'
Chris Sosa7cd23202013-10-15 17:22:57 -0700578 logging.info('Staging artifacts using xbuddy.')
579 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]), return_dir=True)
580
581 logging.info('Verifying static url returned is valid.')
582 expected_static_url = '/'.join([self.devserver_url, STATIC, build_id])
583 self.assertEqual(response, expected_static_url)
584
585 logging.info('Checking for_update returns an update_url for what we just '
586 'staged.')
587 expected_update_url = '/'.join([self.devserver_url, UPDATE, build_id])
588 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]), for_update=True)
589 self.assertEqual(response, expected_update_url)
590
591 logging.info('Now give xbuddy a bad path.')
Amin Hassani6eec8792020-01-09 14:06:48 -0800592 self.assertRaises(requests.exceptions.RequestException,
Chris Sosa7cd23202013-10-15 17:22:57 -0700593 self._MakeRPC,
594 '/'.join([XBUDDY, xbuddy_bad_path]))
595
Prashanth Ba06d2d22014-03-07 15:35:19 -0800596 def testListImageDir(self):
597 """Verifies that we can list the contents of the image directory."""
598 build_id = 'x86-mario-release/R32-4810.0.0'
599 archive_url = 'gs://chromeos-image-archive/%s' % build_id
600 build_dir = os.path.join(self.test_data_path, build_id)
601 shutil.rmtree(build_dir, ignore_errors=True)
602
603 logging.info('checking for %s on an unstaged build.', LIST_IMAGE_DIR)
604 response = self._MakeRPC(LIST_IMAGE_DIR, archive_url=archive_url)
Amin Hassaniba847c82019-12-06 16:30:56 -0800605 self.assertIn(archive_url, response)
606 self.assertIn('not been staged', response)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800607
608 logging.info('Checking for %s on a staged build.', LIST_IMAGE_DIR)
609 fake_file_name = 'fake_file'
610 try:
611 os.makedirs(build_dir)
612 open(os.path.join(build_dir, fake_file_name), 'w').close()
613 except OSError:
614 logging.error('Could not create files to imitate staged content. '
615 'Build dir %s, file %s', build_dir, fake_file_name)
616 raise
617 response = self._MakeRPC(LIST_IMAGE_DIR, archive_url=archive_url)
Amin Hassaniba847c82019-12-06 16:30:56 -0800618 self.assertIn(fake_file_name, response)
Prashanth Ba06d2d22014-03-07 15:35:19 -0800619 shutil.rmtree(build_dir, ignore_errors=True)
Chris Sosa7cd23202013-10-15 17:22:57 -0700620
Amin Hassani28df4212019-10-28 10:16:50 -0700621
Chris Sosa7cd23202013-10-15 17:22:57 -0700622if __name__ == '__main__':
Chris Sosa7cd23202013-10-15 17:22:57 -0700623 unittest.main()