blob: 3dbddb651f250fff3f744e735bba592aff7251ce [file] [log] [blame]
Chris Sosa7cd23202013-10-15 17:22:57 -07001#!/usr/bin/python
2
3# 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
12Note there are two classes of tests here and they can be run separately.
13
14To just run the short-running "unittests" run:
15 ./devserver_integration_tests.py DevserverUnittests
16
17To just run the longer-running tests, run:
18 ./devserver_integration_tests.py DevserverIntegrationTests
19"""
20
21import devserver_constants
22import json
23import logging
24from xml.dom import minidom
25import os
26import psutil
27import shutil
28import signal
29import subprocess
30import tempfile
31import time
32import unittest
33import urllib2
34
35
36# Paths are relative to this script's base directory.
37LABEL = 'devserver'
38TEST_IMAGE_PATH = 'testdata/devserver'
39TEST_IMAGE_NAME = 'update.gz'
40EXPECTED_HASH = 'kGcOinJ0vA8vdYX53FN0F5BdwfY='
41
42# Update request based on Omaha v3 protocol format.
43UPDATE_REQUEST = """<?xml version="1.0" encoding="UTF-8"?>
44<request version="ChromeOSUpdateEngine-0.1.0.0" updaterversion="ChromeOSUpdateEngine-0.1.0.0" protocol="3.0" ismachine="1">
45 <os version="Indy" platform="Chrome OS" sp="0.11.254.2011_03_09_1814_i686"></os>
46 <app appid="{DEV-BUILD}" version="11.254.2011_03_09_1814" lang="en-US" track="developer-build" board="x86-generic" hardware_class="BETA DVT" delta_okay="true">
47 <updatecheck></updatecheck>
48 </app>
49</request>
50"""
51
52# RPC constants.
53STAGE = 'stage'
54IS_STAGED = 'is_staged'
55STATIC = 'static'
56UPDATE = 'update'
57CHECK_HEALTH = 'check_health'
58CONTROL_FILES = 'controlfiles'
59XBUDDY = 'xbuddy'
60
61# API rpcs and constants.
62API_HOST_INFO = 'api/hostinfo'
63API_SET_NEXT_UPDATE = 'api/setnextupdate'
64API_SET_UPDATE_REQUEST = 'new_update-test/the-new-update'
65API_TEST_IP_ADDR = '127.0.0.1'
66
67DEVSERVER_START_TIMEOUT = 15
Gilad Arnold08516112014-02-14 13:14:03 -080068DEVSERVER_START_SLEEP = 1
Chris Sosa7cd23202013-10-15 17:22:57 -070069
70
71class DevserverFailedToStart(Exception):
72 """Raised if we could not start the devserver."""
73
74
Gilad Arnold08516112014-02-14 13:14:03 -080075class DevserverTestBase(unittest.TestCase):
Chris Sosa7cd23202013-10-15 17:22:57 -070076 """Class containing common logic between devserver test classes."""
77
78 def setUp(self):
Gilad Arnold08516112014-02-14 13:14:03 -080079 """Creates and populates a test directory, temporary files."""
Chris Sosa7cd23202013-10-15 17:22:57 -070080 self.test_data_path = tempfile.mkdtemp()
81 self.src_dir = os.path.dirname(__file__)
82
83 # Current location of testdata payload.
84 image_src = os.path.join(self.src_dir, TEST_IMAGE_PATH, TEST_IMAGE_NAME)
85
Gilad Arnold08516112014-02-14 13:14:03 -080086 # Copy the payload to the location of the update label.
87 self._CreateLabelAndCopyImage(LABEL, image_src)
Chris Sosa7cd23202013-10-15 17:22:57 -070088
89 # Copy the payload to the location of forced label.
Gilad Arnold08516112014-02-14 13:14:03 -080090 self._CreateLabelAndCopyImage(API_SET_UPDATE_REQUEST, image_src)
Chris Sosa7cd23202013-10-15 17:22:57 -070091
Gilad Arnold08516112014-02-14 13:14:03 -080092 # Allocate temporary files for various devserver outputs.
93 self.pidfile = self._MakeTempFile('pid')
94 self.portfile = self._MakeTempFile('port')
95 self.logfile = self._MakeTempFile('log')
Chris Sosa7cd23202013-10-15 17:22:57 -070096
Gilad Arnold08516112014-02-14 13:14:03 -080097 # Initialize various runtime values.
98 self.devserver_url = self.port = self.pid = None
Chris Sosa7cd23202013-10-15 17:22:57 -070099
100 def tearDown(self):
Gilad Arnold08516112014-02-14 13:14:03 -0800101 """Kill the server, remove the test directory and temporary files."""
Chris Sosa7cd23202013-10-15 17:22:57 -0700102 if self.pid:
103 os.kill(self.pid, signal.SIGKILL)
104
Gilad Arnold08516112014-02-14 13:14:03 -0800105 self._RemoveFile(self.pidfile)
106 self._RemoveFile(self.portfile)
107 self._RemoveFile(self.logfile)
108 shutil.rmtree(self.test_data_path)
Chris Sosa7cd23202013-10-15 17:22:57 -0700109
110 # Helper methods begin here.
111
Gilad Arnold08516112014-02-14 13:14:03 -0800112 def _CreateLabelAndCopyImage(self, label, image):
113 """Creates a label location and copies an image to it."""
114 label_dir = os.path.join(self.test_data_path, label)
115 os.makedirs(label_dir)
116 shutil.copy(image, os.path.join(label_dir, TEST_IMAGE_NAME))
117
118 def _MakeTempFile(self, suffix):
119 """Return path of a newly created temporary file."""
120 with tempfile.NamedTemporaryFile(suffix='-devserver-%s' % suffix) as f:
121 name = f.name
122 f.close()
123
124 return name
125
126 def _RemoveFile(self, filename):
127 """Removes a file if it is present."""
128 if os.path.isfile(filename):
129 os.remove(filename)
130
131 def _ReadIntValueFromFile(self, path, desc):
132 """Reads a string from file and returns its conversion into an integer."""
133 if not os.path.isfile(path):
134 raise DevserverFailedToStart('Devserver did not drop %s (%r).' %
135 (desc, path))
136
137 with open(path) as f:
138 value_str = f.read()
139
140 try:
141 return int(value_str)
142 except ValueError:
143 raise DevserverFailedToStart('Devserver did not drop a valid value '
144 'in %s (%r).' % (desc, value_str))
145
146 def _StartServer(self, port=0):
Chris Sosa7cd23202013-10-15 17:22:57 -0700147 """Attempts to start devserver on |port|.
148
Gilad Arnold08516112014-02-14 13:14:03 -0800149 In the default case where port == 0, the server will bind to an arbitrary
150 availble port. If successful, this method will set the devserver's pid
151 (self.pid), actual listening port (self.port) and URL (self.devserver_url).
Chris Sosa7cd23202013-10-15 17:22:57 -0700152
153 Raises:
154 DevserverFailedToStart: If the devserver could not be started.
155 """
156 cmd = [
157 'python',
158 os.path.join(self.src_dir, 'devserver.py'),
159 'devserver.py',
160 '--static_dir', self.test_data_path,
161 '--pidfile', self.pidfile,
Gilad Arnold08516112014-02-14 13:14:03 -0800162 '--portfile', self.portfile,
Chris Sosa7cd23202013-10-15 17:22:57 -0700163 '--port', str(port),
164 '--logfile', self.logfile]
165
166 # Pipe all output. Use logfile to get devserver log.
167 subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
168
Gilad Arnold08516112014-02-14 13:14:03 -0800169 # Wait for devserver to start, determining its actual serving port and URL.
Chris Sosa7cd23202013-10-15 17:22:57 -0700170 current_time = time.time()
171 deadline = current_time + DEVSERVER_START_TIMEOUT
172 while current_time < deadline:
Chris Sosa7cd23202013-10-15 17:22:57 -0700173 try:
Gilad Arnold08516112014-02-14 13:14:03 -0800174 self.port = self._ReadIntValueFromFile(self.portfile, 'portfile')
175 self.devserver_url = 'http://127.0.0.1:%d' % self.port
Chris Sosa7cd23202013-10-15 17:22:57 -0700176 self._MakeRPC(CHECK_HEALTH, timeout=0.1)
177 break
178 except Exception:
Gilad Arnold08516112014-02-14 13:14:03 -0800179 time.sleep(DEVSERVER_START_SLEEP)
180 current_time = time.time()
Chris Sosa7cd23202013-10-15 17:22:57 -0700181 else:
182 raise DevserverFailedToStart('Devserver failed to start within timeout.')
183
Gilad Arnold08516112014-02-14 13:14:03 -0800184 # Retrieve PID.
185 self.pid = self._ReadIntValueFromFile(self.pidfile, 'pidfile')
Chris Sosa7cd23202013-10-15 17:22:57 -0700186
187 def VerifyHandleUpdate(self, label, use_test_payload=True):
188 """Verifies that we can send an update request to the devserver.
189
190 This method verifies (using a fake update_request blob) that the devserver
191 can interpret the payload and give us back the right payload.
192
193 Args:
194 label: Label that update is served from e.g. <board>-release/<version>
195 use_test_payload: If set to true, expects to serve payload under
196 testdata/ and does extra checks i.e. compares hash and content of
197 payload.
Gilad Arnold08516112014-02-14 13:14:03 -0800198
Chris Sosa7cd23202013-10-15 17:22:57 -0700199 Returns:
200 url of the update payload if we verified the update.
201 """
202 update_label = '/'.join([UPDATE, label])
203 response = self._MakeRPC(update_label, data=UPDATE_REQUEST)
204 self.assertNotEqual('', response)
205
206 # Parse the response and check if it contains the right result.
207 dom = minidom.parseString(response)
208 update = dom.getElementsByTagName('updatecheck')[0]
209 expected_static_url = '/'.join([self.devserver_url, STATIC, label])
210 expected_hash = EXPECTED_HASH if use_test_payload else None
211 url = self.VerifyV3Response(update, expected_static_url,
212 expected_hash=expected_hash)
213
214 # Verify the image we download is correct since we already know what it is.
215 if use_test_payload:
216 connection = urllib2.urlopen(url)
217 contents = connection.read()
218 connection.close()
219 self.assertEqual('Developers, developers, developers!\n', contents)
220
221 return url
222
223 def VerifyV3Response(self, update, expected_static_url, expected_hash):
224 """Verifies the update DOM from a v3 response and returns the url."""
225 # Parse the response and check if it contains the right result.
226 urls = update.getElementsByTagName('urls')[0]
227 url = urls.getElementsByTagName('url')[0]
228
229 static_url = url.getAttribute('codebase')
230 # Static url's end in /.
231 self.assertEqual(expected_static_url + '/', static_url)
232
233 manifest = update.getElementsByTagName('manifest')[0]
234 packages = manifest.getElementsByTagName('packages')[0]
235 package = packages.getElementsByTagName('package')[0]
236 filename = package.getAttribute('name')
237 self.assertEqual(TEST_IMAGE_NAME, filename)
238
239 if expected_hash:
240 hash_value = package.getAttribute('hash')
241 self.assertEqual(EXPECTED_HASH, hash_value)
242
243 url = os.path.join(static_url, filename)
244 return url
245
246 def _MakeRPC(self, rpc, data=None, timeout=None, **kwargs):
Gilad Arnold08516112014-02-14 13:14:03 -0800247 """Makes an RPC call to the devserver.
Chris Sosa7cd23202013-10-15 17:22:57 -0700248
249 Args:
Gilad Arnold08516112014-02-14 13:14:03 -0800250 rpc: The function to run on the devserver, e.g. 'stage'.
Chris Sosa7cd23202013-10-15 17:22:57 -0700251 data: Optional post data to send.
252 timeout: Optional timeout to pass to urlopen.
Gilad Arnold08516112014-02-14 13:14:03 -0800253 kwargs: Optional arguments to the function, e.g. artifact_url='foo/bar'.
Chris Sosa7cd23202013-10-15 17:22:57 -0700254
Gilad Arnold08516112014-02-14 13:14:03 -0800255 Returns:
256 The function output.
Chris Sosa7cd23202013-10-15 17:22:57 -0700257 """
258 request = '/'.join([self.devserver_url, rpc])
259 if kwargs:
260 # Join the kwargs to the URL.
261 request += '?' + '&'.join('%s=%s' % item for item in kwargs.iteritems())
262
263 output = None
264 try:
265 # Let's log output for all rpc's without timeouts because we only
266 # use timeouts to check to see if something is up and these checks tend
267 # to be small and so logging it will be extremely repetitive.
268 if not timeout:
269 logging.info('Making request using %s', request)
270
271 connection = urllib2.urlopen(request, data=data, timeout=timeout)
272 output = connection.read()
273 connection.close()
274 except urllib2.HTTPError:
275 raise
276
277 return output
278
279
Gilad Arnold08516112014-02-14 13:14:03 -0800280class AutoStartDevserverTestBase(DevserverTestBase):
281 """Test base class that automatically starts the devserver."""
282
283 def setUp(self):
284 """Initialize everything, then start the server."""
285 super(AutoStartDevserverTestBase, self).setUp()
286 self._StartServer()
287
288
289class DevserverBasicTests(AutoStartDevserverTestBase):
290 """Short running tests for the devserver (no remote deps).
Chris Sosa7cd23202013-10-15 17:22:57 -0700291
292 These are technically not unittests because they depend on being able to
293 start a devserver locally which technically requires external resources so
294 they are lumped with the remote tests here.
295 """
296
297 def testHandleUpdateV3(self):
298 self.VerifyHandleUpdate(label=LABEL)
299
300 def testApiBadSetNextUpdateRequest(self):
301 """Tests sending a bad setnextupdate request."""
302 # Send bad request and ensure it fails...
303 self.assertRaises(urllib2.URLError,
304 self._MakeRPC,
305 '/'.join([API_SET_NEXT_UPDATE, API_TEST_IP_ADDR]))
306
307 def testApiBadSetNextUpdateURL(self):
308 """Tests contacting a bad setnextupdate url."""
309 # Send bad request and ensure it fails...
310 self.assertRaises(urllib2.URLError,
311 self._MakeRPC, API_SET_NEXT_UPDATE)
312
313 def testApiBadHostInfoURL(self):
314 """Tests contacting a bad hostinfo url."""
315 # Host info should be invalid without a specified address.
316 self.assertRaises(urllib2.URLError,
317 self._MakeRPC, API_HOST_INFO)
318
319 def testApiHostInfoAndSetNextUpdate(self):
320 """Tests using the setnextupdate and hostinfo api commands."""
321 # Send setnextupdate command.
322 self._MakeRPC('/'.join([API_SET_NEXT_UPDATE, API_TEST_IP_ADDR]),
323 data=API_SET_UPDATE_REQUEST)
324
325 # Send hostinfo command and verify the setnextupdate worked.
326 response = self._MakeRPC('/'.join([API_HOST_INFO, API_TEST_IP_ADDR]))
327
328 self.assertEqual(
329 json.loads(response)['forced_update_label'], API_SET_UPDATE_REQUEST)
330
331 def testXBuddyLocalAlias(self):
332 """Extensive local image xbuddy unittest.
333
334 This test verifies all the local xbuddy logic by creating a new local folder
335 with the necessary update items and verifies we can use all of them.
336 """
337 update_data = 'TEST UPDATE'
338 image_data = 'TEST IMAGE'
339 stateful_data = 'STATEFUL STUFFS'
340 build_id = 'x86-generic/R32-9999.0.0-a1'
341 xbuddy_path = 'x86-generic/R32-9999.0.0-a1/test'
342 build_dir = os.path.join(self.test_data_path, build_id)
343 os.makedirs(build_dir)
344 test_image_file = os.path.join(build_dir,
345 devserver_constants.TEST_IMAGE_FILE)
346 update_file = os.path.join(build_dir, devserver_constants.UPDATE_FILE)
347 stateful_file = os.path.join(build_dir, devserver_constants.STATEFUL_FILE)
348
349 logging.info('Creating dummy files')
350
351 for item, filename, data in zip(
352 ['full_payload', 'test', 'stateful'],
353 [update_file, test_image_file, stateful_file],
354 [update_data, image_data, stateful_data]):
355 logging.info('Creating file %s', filename)
356 with open(filename, 'w') as fh:
357 fh.write(data)
358
359 xbuddy_path = '/'.join([build_id, item])
360 logging.info('Testing xbuddy path %s', xbuddy_path)
361 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]))
362 self.assertEqual(response, data)
363
364 expected_dir = '/'.join([self.devserver_url, STATIC, build_id])
365 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]), return_dir=True)
366 self.assertEqual(response, expected_dir)
367
Yu-Ju Hong51495eb2013-12-12 17:08:43 -0800368 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]),
369 relative_path=True)
370 self.assertEqual(response, build_id)
371
Chris Sosa7cd23202013-10-15 17:22:57 -0700372 xbuddy_path = '/'.join([build_id, 'test'])
373 logging.info('Testing for_update for %s', xbuddy_path)
374 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]), for_update=True)
375 expected_path = '/'.join([self.devserver_url, UPDATE, build_id])
376 self.assertTrue(response, expected_path)
377
378 logging.info('Verifying the actual payload data')
379 url = self.VerifyHandleUpdate(build_id, use_test_payload=False)
380 logging.info('Verify the actual content of the update payload')
381 connection = urllib2.urlopen(url)
382 contents = connection.read()
383 connection.close()
384 self.assertEqual(update_data, contents)
385
386 def testPidFile(self):
387 """Test that using a pidfile works correctly."""
388 with open(self.pidfile, 'r') as f:
389 pid = f.read()
390
391 # Let's assert some process information about the devserver.
392 self.assertTrue(pid.isdigit())
393 process = psutil.Process(int(pid))
394 self.assertTrue(process.is_running())
395 self.assertTrue('devserver.py' in process.cmdline)
396
397
Gilad Arnold08516112014-02-14 13:14:03 -0800398class DevserverExtendedTests(AutoStartDevserverTestBase):
Chris Sosa7cd23202013-10-15 17:22:57 -0700399 """Longer running integration tests that test interaction with Google Storage.
400
401 Note: due to the interaction with Google Storage, these tests both require
402 1) runner has access to the Google Storage bucket where builders store builds.
403 2) time. These tests actually download the artifacts needed.
404 """
405
406 def testStageAndUpdate(self):
407 """Tests core autotest workflow where we stage/update with a test payload.
408 """
409 build_id = 'x86-mario-release/R32-4810.0.0'
410 archive_url = 'gs://chromeos-image-archive/%s' % build_id
411
412 response = self._MakeRPC(IS_STAGED, archive_url=archive_url,
413 artifacts='full_payload,stateful')
414 self.assertEqual(response, 'False')
415
416 logging.info('Staging update artifacts')
417 self._MakeRPC(STAGE, archive_url=archive_url,
418 artifacts='full_payload,stateful')
419 logging.info('Staging complete. '
420 'Verifying files exist and are staged in the staging '
421 'directory.')
422 response = self._MakeRPC(IS_STAGED, archive_url=archive_url,
423 artifacts='full_payload,stateful')
424 self.assertEqual(response, 'True')
425 staged_dir = os.path.join(self.test_data_path, build_id)
426 self.assertTrue(os.path.isdir(staged_dir))
427 self.assertTrue(os.path.exists(
428 os.path.join(staged_dir, devserver_constants.UPDATE_FILE)))
429 self.assertTrue(os.path.exists(
430 os.path.join(staged_dir, devserver_constants.STATEFUL_FILE)))
431
432 logging.info('Verifying we can update using the stage update artifacts.')
433 self.VerifyHandleUpdate(build_id, use_test_payload=False)
434
435 def testStageAutotestAndGetPackages(self):
436 """Another autotest workflow test where we stage/update with a test payload.
437 """
438 build_id = 'x86-mario-release/R32-4810.0.0'
439 archive_url = 'gs://chromeos-image-archive/%s' % build_id
440 autotest_artifacts = 'autotest,test_suites,au_suite'
441 logging.info('Staging autotest artifacts (may take a while).')
442 self._MakeRPC(STAGE, archive_url=archive_url, artifacts=autotest_artifacts)
443
444 response = self._MakeRPC(IS_STAGED, archive_url=archive_url,
445 artifacts=autotest_artifacts)
446 self.assertEqual(response, 'True')
447
448 # Verify the files exist and are staged in the staging directory.
449 logging.info('Checking directories exist after we staged the files.')
450 staged_dir = os.path.join(self.test_data_path, build_id)
451 autotest_dir = os.path.join(staged_dir, 'autotest')
452 package_dir = os.path.join(autotest_dir, 'packages')
453 self.assertTrue(os.path.isdir(staged_dir))
454 self.assertTrue(os.path.isdir(autotest_dir))
455 self.assertTrue(os.path.isdir(package_dir))
456
457 control_files = self._MakeRPC(CONTROL_FILES, build=build_id,
458 suite_name='bvt')
459 logging.info('Checking for known control file in bvt suite.')
460 self.assertTrue('client/site_tests/platform_FilePerms/'
461 'control' in control_files)
462
463 def testRemoteXBuddyAlias(self):
464 """Another autotest workflow test where we stage/update with a test payload.
465 """
466 build_id = 'x86-mario-release/R32-4810.0.0'
467 xbuddy_path = 'remote/x86-mario/R32-4810.0.0/full_payload'
468 xbuddy_bad_path = 'remote/x86-mario/R32-9999.9999.9999'
469 logging.info('Staging artifacts using xbuddy.')
470 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]), return_dir=True)
471
472 logging.info('Verifying static url returned is valid.')
473 expected_static_url = '/'.join([self.devserver_url, STATIC, build_id])
474 self.assertEqual(response, expected_static_url)
475
476 logging.info('Checking for_update returns an update_url for what we just '
477 'staged.')
478 expected_update_url = '/'.join([self.devserver_url, UPDATE, build_id])
479 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]), for_update=True)
480 self.assertEqual(response, expected_update_url)
481
482 logging.info('Now give xbuddy a bad path.')
483 self.assertRaises(urllib2.HTTPError,
484 self._MakeRPC,
485 '/'.join([XBUDDY, xbuddy_bad_path]))
486
487
488if __name__ == '__main__':
489 logging_format = '%(levelname)-8s: %(message)s'
490 logging.basicConfig(level=logging.DEBUG, format=logging_format)
491 unittest.main()