blob: d78e3b8386ebb24bdbcfda044e6ce1f5fa9014fc [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
68
69
70class DevserverFailedToStart(Exception):
71 """Raised if we could not start the devserver."""
72
73
74class DevserverTestCommon(unittest.TestCase):
75 """Class containing common logic between devserver test classes."""
76
77 def setUp(self):
78 """Copies in testing files."""
79 self.test_data_path = tempfile.mkdtemp()
80 self.src_dir = os.path.dirname(__file__)
81
82 # Current location of testdata payload.
83 image_src = os.path.join(self.src_dir, TEST_IMAGE_PATH, TEST_IMAGE_NAME)
84
85 # Copy the payload to the location of the update label "devserver."
86 os.makedirs(os.path.join(self.test_data_path, LABEL))
87 shutil.copy(image_src, os.path.join(self.test_data_path, LABEL,
88 TEST_IMAGE_NAME))
89
90 # Copy the payload to the location of forced label.
91 os.makedirs(os.path.join(self.test_data_path, API_SET_UPDATE_REQUEST))
92 shutil.copy(image_src, os.path.join(self.test_data_path,
93 API_SET_UPDATE_REQUEST,
94 TEST_IMAGE_NAME))
95
96 self.pidfile = tempfile.mktemp('devserver_unittest')
97 self.logfile = tempfile.mktemp('devserver_unittest')
98
99 # Devserver url set in start server.
100 self.devserver_url = None
101 self.pid = self._StartServer()
102
103 def tearDown(self):
104 """Removes testing files."""
105 shutil.rmtree(self.test_data_path)
106 if self.pid:
107 os.kill(self.pid, signal.SIGKILL)
108
109 if os.path.exists(self.pidfile):
110 os.remove(self.pidfile)
111
112 # Helper methods begin here.
113
114 def _StartServerOnPort(self, port):
115 """Attempts to start devserver on |port|.
116
117 Returns:
118 The pid of the devserver.
119
120 Raises:
121 DevserverFailedToStart: If the devserver could not be started.
122 """
123 cmd = [
124 'python',
125 os.path.join(self.src_dir, 'devserver.py'),
126 'devserver.py',
127 '--static_dir', self.test_data_path,
128 '--pidfile', self.pidfile,
129 '--port', str(port),
130 '--logfile', self.logfile]
131
132 # Pipe all output. Use logfile to get devserver log.
133 subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
134
135 # wait for devserver to start
136 current_time = time.time()
137 deadline = current_time + DEVSERVER_START_TIMEOUT
138 while current_time < deadline:
139 current_time = time.time()
140 try:
141 self.devserver_url = 'http://127.0.0.1:%d' % port
142 self._MakeRPC(CHECK_HEALTH, timeout=0.1)
143 break
144 except Exception:
145 continue
146 else:
147 raise DevserverFailedToStart('Devserver failed to start within timeout.')
148
149 if not os.path.exists(self.pidfile):
150 raise DevserverFailedToStart('Devserver did not drop a pidfile.')
151 else:
152 pid_value = open(self.pidfile).read()
153 try:
154 return int(pid_value)
155 except ValueError:
156 raise DevserverFailedToStart('Devserver did not drop a pid in the '
157 'pidfile.')
158
159 def _StartServer(self):
160 """Starts devserver on a port, returns pid.
161
162 Raises:
163 DevserverFailedToStart: If all attempts to start the devserver fail.
164 """
165 # TODO(sosa): Fixing crbug.com/308686 will allow the devserver to do this
166 # for us and be cleaner.
167 for port in range(8080, 8090):
168 try:
169 return self._StartServerOnPort(port)
170 except DevserverFailedToStart:
171 logging.error('Devserver could not start on port %s', port)
172 continue
173 else:
174 logging.error(open(self.logfile).read())
175 raise DevserverFailedToStart('Devserver could not be started on all '
176 'ports.')
177
178 def VerifyHandleUpdate(self, label, use_test_payload=True):
179 """Verifies that we can send an update request to the devserver.
180
181 This method verifies (using a fake update_request blob) that the devserver
182 can interpret the payload and give us back the right payload.
183
184 Args:
185 label: Label that update is served from e.g. <board>-release/<version>
186 use_test_payload: If set to true, expects to serve payload under
187 testdata/ and does extra checks i.e. compares hash and content of
188 payload.
189 Returns:
190 url of the update payload if we verified the update.
191 """
192 update_label = '/'.join([UPDATE, label])
193 response = self._MakeRPC(update_label, data=UPDATE_REQUEST)
194 self.assertNotEqual('', response)
195
196 # Parse the response and check if it contains the right result.
197 dom = minidom.parseString(response)
198 update = dom.getElementsByTagName('updatecheck')[0]
199 expected_static_url = '/'.join([self.devserver_url, STATIC, label])
200 expected_hash = EXPECTED_HASH if use_test_payload else None
201 url = self.VerifyV3Response(update, expected_static_url,
202 expected_hash=expected_hash)
203
204 # Verify the image we download is correct since we already know what it is.
205 if use_test_payload:
206 connection = urllib2.urlopen(url)
207 contents = connection.read()
208 connection.close()
209 self.assertEqual('Developers, developers, developers!\n', contents)
210
211 return url
212
213 def VerifyV3Response(self, update, expected_static_url, expected_hash):
214 """Verifies the update DOM from a v3 response and returns the url."""
215 # Parse the response and check if it contains the right result.
216 urls = update.getElementsByTagName('urls')[0]
217 url = urls.getElementsByTagName('url')[0]
218
219 static_url = url.getAttribute('codebase')
220 # Static url's end in /.
221 self.assertEqual(expected_static_url + '/', static_url)
222
223 manifest = update.getElementsByTagName('manifest')[0]
224 packages = manifest.getElementsByTagName('packages')[0]
225 package = packages.getElementsByTagName('package')[0]
226 filename = package.getAttribute('name')
227 self.assertEqual(TEST_IMAGE_NAME, filename)
228
229 if expected_hash:
230 hash_value = package.getAttribute('hash')
231 self.assertEqual(EXPECTED_HASH, hash_value)
232
233 url = os.path.join(static_url, filename)
234 return url
235
236 def _MakeRPC(self, rpc, data=None, timeout=None, **kwargs):
237 """Makes a RPC to the devserver using the kwargs and returns output.
238
239 Args:
240 data: Optional post data to send.
241 timeout: Optional timeout to pass to urlopen.
242
243 For example: localhost:8080/stage with artifact_url=blah/blah.
244 """
245 request = '/'.join([self.devserver_url, rpc])
246 if kwargs:
247 # Join the kwargs to the URL.
248 request += '?' + '&'.join('%s=%s' % item for item in kwargs.iteritems())
249
250 output = None
251 try:
252 # Let's log output for all rpc's without timeouts because we only
253 # use timeouts to check to see if something is up and these checks tend
254 # to be small and so logging it will be extremely repetitive.
255 if not timeout:
256 logging.info('Making request using %s', request)
257
258 connection = urllib2.urlopen(request, data=data, timeout=timeout)
259 output = connection.read()
260 connection.close()
261 except urllib2.HTTPError:
262 raise
263
264 return output
265
266
267class DevserverUnittests(DevserverTestCommon):
268 """Short running integration/unittests for the devserver (no remote deps).
269
270 These are technically not unittests because they depend on being able to
271 start a devserver locally which technically requires external resources so
272 they are lumped with the remote tests here.
273 """
274
275 def testHandleUpdateV3(self):
276 self.VerifyHandleUpdate(label=LABEL)
277
278 def testApiBadSetNextUpdateRequest(self):
279 """Tests sending a bad setnextupdate request."""
280 # Send bad request and ensure it fails...
281 self.assertRaises(urllib2.URLError,
282 self._MakeRPC,
283 '/'.join([API_SET_NEXT_UPDATE, API_TEST_IP_ADDR]))
284
285 def testApiBadSetNextUpdateURL(self):
286 """Tests contacting a bad setnextupdate url."""
287 # Send bad request and ensure it fails...
288 self.assertRaises(urllib2.URLError,
289 self._MakeRPC, API_SET_NEXT_UPDATE)
290
291 def testApiBadHostInfoURL(self):
292 """Tests contacting a bad hostinfo url."""
293 # Host info should be invalid without a specified address.
294 self.assertRaises(urllib2.URLError,
295 self._MakeRPC, API_HOST_INFO)
296
297 def testApiHostInfoAndSetNextUpdate(self):
298 """Tests using the setnextupdate and hostinfo api commands."""
299 # Send setnextupdate command.
300 self._MakeRPC('/'.join([API_SET_NEXT_UPDATE, API_TEST_IP_ADDR]),
301 data=API_SET_UPDATE_REQUEST)
302
303 # Send hostinfo command and verify the setnextupdate worked.
304 response = self._MakeRPC('/'.join([API_HOST_INFO, API_TEST_IP_ADDR]))
305
306 self.assertEqual(
307 json.loads(response)['forced_update_label'], API_SET_UPDATE_REQUEST)
308
309 def testXBuddyLocalAlias(self):
310 """Extensive local image xbuddy unittest.
311
312 This test verifies all the local xbuddy logic by creating a new local folder
313 with the necessary update items and verifies we can use all of them.
314 """
315 update_data = 'TEST UPDATE'
316 image_data = 'TEST IMAGE'
317 stateful_data = 'STATEFUL STUFFS'
318 build_id = 'x86-generic/R32-9999.0.0-a1'
319 xbuddy_path = 'x86-generic/R32-9999.0.0-a1/test'
320 build_dir = os.path.join(self.test_data_path, build_id)
321 os.makedirs(build_dir)
322 test_image_file = os.path.join(build_dir,
323 devserver_constants.TEST_IMAGE_FILE)
324 update_file = os.path.join(build_dir, devserver_constants.UPDATE_FILE)
325 stateful_file = os.path.join(build_dir, devserver_constants.STATEFUL_FILE)
326
327 logging.info('Creating dummy files')
328
329 for item, filename, data in zip(
330 ['full_payload', 'test', 'stateful'],
331 [update_file, test_image_file, stateful_file],
332 [update_data, image_data, stateful_data]):
333 logging.info('Creating file %s', filename)
334 with open(filename, 'w') as fh:
335 fh.write(data)
336
337 xbuddy_path = '/'.join([build_id, item])
338 logging.info('Testing xbuddy path %s', xbuddy_path)
339 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]))
340 self.assertEqual(response, data)
341
342 expected_dir = '/'.join([self.devserver_url, STATIC, build_id])
343 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]), return_dir=True)
344 self.assertEqual(response, expected_dir)
345
346 xbuddy_path = '/'.join([build_id, 'test'])
347 logging.info('Testing for_update for %s', xbuddy_path)
348 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]), for_update=True)
349 expected_path = '/'.join([self.devserver_url, UPDATE, build_id])
350 self.assertTrue(response, expected_path)
351
352 logging.info('Verifying the actual payload data')
353 url = self.VerifyHandleUpdate(build_id, use_test_payload=False)
354 logging.info('Verify the actual content of the update payload')
355 connection = urllib2.urlopen(url)
356 contents = connection.read()
357 connection.close()
358 self.assertEqual(update_data, contents)
359
360 def testPidFile(self):
361 """Test that using a pidfile works correctly."""
362 with open(self.pidfile, 'r') as f:
363 pid = f.read()
364
365 # Let's assert some process information about the devserver.
366 self.assertTrue(pid.isdigit())
367 process = psutil.Process(int(pid))
368 self.assertTrue(process.is_running())
369 self.assertTrue('devserver.py' in process.cmdline)
370
371
372class DevserverIntegrationTests(DevserverTestCommon):
373 """Longer running integration tests that test interaction with Google Storage.
374
375 Note: due to the interaction with Google Storage, these tests both require
376 1) runner has access to the Google Storage bucket where builders store builds.
377 2) time. These tests actually download the artifacts needed.
378 """
379
380 def testStageAndUpdate(self):
381 """Tests core autotest workflow where we stage/update with a test payload.
382 """
383 build_id = 'x86-mario-release/R32-4810.0.0'
384 archive_url = 'gs://chromeos-image-archive/%s' % build_id
385
386 response = self._MakeRPC(IS_STAGED, archive_url=archive_url,
387 artifacts='full_payload,stateful')
388 self.assertEqual(response, 'False')
389
390 logging.info('Staging update artifacts')
391 self._MakeRPC(STAGE, archive_url=archive_url,
392 artifacts='full_payload,stateful')
393 logging.info('Staging complete. '
394 'Verifying files exist and are staged in the staging '
395 'directory.')
396 response = self._MakeRPC(IS_STAGED, archive_url=archive_url,
397 artifacts='full_payload,stateful')
398 self.assertEqual(response, 'True')
399 staged_dir = os.path.join(self.test_data_path, build_id)
400 self.assertTrue(os.path.isdir(staged_dir))
401 self.assertTrue(os.path.exists(
402 os.path.join(staged_dir, devserver_constants.UPDATE_FILE)))
403 self.assertTrue(os.path.exists(
404 os.path.join(staged_dir, devserver_constants.STATEFUL_FILE)))
405
406 logging.info('Verifying we can update using the stage update artifacts.')
407 self.VerifyHandleUpdate(build_id, use_test_payload=False)
408
409 def testStageAutotestAndGetPackages(self):
410 """Another autotest workflow test where we stage/update with a test payload.
411 """
412 build_id = 'x86-mario-release/R32-4810.0.0'
413 archive_url = 'gs://chromeos-image-archive/%s' % build_id
414 autotest_artifacts = 'autotest,test_suites,au_suite'
415 logging.info('Staging autotest artifacts (may take a while).')
416 self._MakeRPC(STAGE, archive_url=archive_url, artifacts=autotest_artifacts)
417
418 response = self._MakeRPC(IS_STAGED, archive_url=archive_url,
419 artifacts=autotest_artifacts)
420 self.assertEqual(response, 'True')
421
422 # Verify the files exist and are staged in the staging directory.
423 logging.info('Checking directories exist after we staged the files.')
424 staged_dir = os.path.join(self.test_data_path, build_id)
425 autotest_dir = os.path.join(staged_dir, 'autotest')
426 package_dir = os.path.join(autotest_dir, 'packages')
427 self.assertTrue(os.path.isdir(staged_dir))
428 self.assertTrue(os.path.isdir(autotest_dir))
429 self.assertTrue(os.path.isdir(package_dir))
430
431 control_files = self._MakeRPC(CONTROL_FILES, build=build_id,
432 suite_name='bvt')
433 logging.info('Checking for known control file in bvt suite.')
434 self.assertTrue('client/site_tests/platform_FilePerms/'
435 'control' in control_files)
436
437 def testRemoteXBuddyAlias(self):
438 """Another autotest workflow test where we stage/update with a test payload.
439 """
440 build_id = 'x86-mario-release/R32-4810.0.0'
441 xbuddy_path = 'remote/x86-mario/R32-4810.0.0/full_payload'
442 xbuddy_bad_path = 'remote/x86-mario/R32-9999.9999.9999'
443 logging.info('Staging artifacts using xbuddy.')
444 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]), return_dir=True)
445
446 logging.info('Verifying static url returned is valid.')
447 expected_static_url = '/'.join([self.devserver_url, STATIC, build_id])
448 self.assertEqual(response, expected_static_url)
449
450 logging.info('Checking for_update returns an update_url for what we just '
451 'staged.')
452 expected_update_url = '/'.join([self.devserver_url, UPDATE, build_id])
453 response = self._MakeRPC('/'.join([XBUDDY, xbuddy_path]), for_update=True)
454 self.assertEqual(response, expected_update_url)
455
456 logging.info('Now give xbuddy a bad path.')
457 self.assertRaises(urllib2.HTTPError,
458 self._MakeRPC,
459 '/'.join([XBUDDY, xbuddy_bad_path]))
460
461
462if __name__ == '__main__':
463 logging_format = '%(levelname)-8s: %(message)s'
464 logging.basicConfig(level=logging.DEBUG, format=logging_format)
465 unittest.main()