| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/python | |
| 2 | |
| 3 # Copyright (c) 2011 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 """This module runs a suite of Auto Update tests. | |
| 8 | |
| 9 The tests can be run on either a virtual machine or actual device depending | |
| 10 on parameters given. Specific tests can be run by invoking --test_prefix. | |
| 11 Verbose is useful for many of the tests if you want to see individual commands | |
| 12 being run during the update process. | |
| 13 """ | |
| 14 | |
| 15 import optparse | |
| 16 import os | |
| 17 import re | |
| 18 import shutil | |
| 19 import subprocess | |
| 20 import sys | |
| 21 import tempfile | |
| 22 import threading | |
| 23 import time | |
| 24 import unittest | |
| 25 import urllib | |
| 26 | |
| 27 sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) | |
| 28 from cros_build_lib import Die | |
| 29 from cros_build_lib import GetIPAddress | |
| 30 from cros_build_lib import Info | |
| 31 from cros_build_lib import ReinterpretPathForChroot | |
| 32 from cros_build_lib import RunCommand | |
| 33 from cros_build_lib import RunCommandCaptureOutput | |
| 34 from cros_build_lib import Warning | |
| 35 | |
| 36 import cros_test_proxy | |
| 37 | |
| 38 global dev_server_cache | |
| 39 | |
| 40 | |
| 41 class UpdateException(Exception): | |
| 42 """Exception thrown when _UpdateImage or _UpdateUsingPayload fail""" | |
| 43 def __init__(self, code, stdout): | |
| 44 self.code = code | |
| 45 self.stdout = stdout | |
| 46 | |
| 47 | |
| 48 class AUTest(object): | |
| 49 """Abstract interface that defines an Auto Update test.""" | |
| 50 verbose = False | |
| 51 | |
| 52 def setUp(self): | |
| 53 unittest.TestCase.setUp(self) | |
| 54 # Set these up as they are used often. | |
| 55 self.crosutils = os.path.join(os.path.dirname(__file__), '..') | |
| 56 self.crosutilsbin = os.path.join(os.path.dirname(__file__)) | |
| 57 self.download_folder = os.path.join(self.crosutils, 'latest_download') | |
| 58 self.vm_image_path = None | |
| 59 if not os.path.exists(self.download_folder): | |
| 60 os.makedirs(self.download_folder) | |
| 61 | |
| 62 # -------- Helper functions --------- | |
| 63 | |
| 64 def _PrepareRealBase(self, image_path): | |
| 65 self.PerformUpdate(image_path) | |
| 66 | |
| 67 def _PrepareVMBase(self, image_path): | |
| 68 # VM Constants. | |
| 69 FULL_VDISK_SIZE = 6072 | |
| 70 FULL_STATEFULFS_SIZE = 3074 | |
| 71 # Needed for VM delta updates. We need to use the qemu image rather | |
| 72 # than the base image on a first update. By tracking the first_update | |
| 73 # we can set src_image to the qemu form of the base image when | |
| 74 # performing generating the delta payload. | |
| 75 self._first_update = True | |
| 76 self.vm_image_path = '%s/chromiumos_qemu_image.bin' % os.path.dirname( | |
| 77 image_path) | |
| 78 if not os.path.exists(self.vm_image_path): | |
| 79 Info('Creating %s' % self.vm_image_path) | |
| 80 RunCommand(['%s/image_to_vm.sh' % self.crosutils, | |
| 81 '--full', | |
| 82 '--from=%s' % ReinterpretPathForChroot( | |
| 83 os.path.dirname(image_path)), | |
| 84 '--vdisk_size=%s' % FULL_VDISK_SIZE, | |
| 85 '--statefulfs_size=%s' % FULL_STATEFULFS_SIZE, | |
| 86 '--board=%s' % self.board, | |
| 87 '--test_image'], enter_chroot=True) | |
| 88 | |
| 89 Info('Using %s as base' % self.vm_image_path) | |
| 90 self.assertTrue(os.path.exists(self.vm_image_path)) | |
| 91 | |
| 92 def AppendUpdateFlags(self, cmd, image_path, src_image_path, proxy_port, | |
| 93 private_key_path): | |
| 94 """Appends common args to an update cmd defined by an array. | |
| 95 | |
| 96 Modifies cmd in places by appending appropriate items given args. | |
| 97 """ | |
| 98 if proxy_port: cmd.append('--proxy_port=%s' % proxy_port) | |
| 99 | |
| 100 # Get pregenerated update if we have one. | |
| 101 update_id = _GenerateUpdateId(target=image_path, src=src_image_path, | |
| 102 key=private_key_path) | |
| 103 cache_path = dev_server_cache[update_id] | |
| 104 if cache_path: | |
| 105 update_url = DevServerWrapper.GetDevServerURL(proxy_port, cache_path) | |
| 106 cmd.append('--update_url=%s' % update_url) | |
| 107 else: | |
| 108 cmd.append('--image=%s' % image_path) | |
| 109 if src_image_path: cmd.append('--src_image=%s' % src_image_path) | |
| 110 | |
| 111 def RunUpdateCmd(self, cmd): | |
| 112 """Runs the given update cmd given verbose options. | |
| 113 | |
| 114 Raises an UpdateException if the update fails. | |
| 115 """ | |
| 116 if self.verbose: | |
| 117 try: | |
| 118 RunCommand(cmd) | |
| 119 except Exception, e: | |
| 120 raise UpdateException(1, e.message) | |
| 121 else: | |
| 122 (code, stdout, stderr) = RunCommandCaptureOutput(cmd) | |
| 123 if code != 0: | |
| 124 raise UpdateException(code, stdout) | |
| 125 | |
| 126 def GetStatefulChangeFlag(self, stateful_change): | |
| 127 """Returns the flag to pass to image_to_vm for the stateful change.""" | |
| 128 stateful_change_flag = '' | |
| 129 if stateful_change: | |
| 130 stateful_change_flag = '--stateful_update_flag=%s' % stateful_change | |
| 131 | |
| 132 return stateful_change_flag | |
| 133 | |
| 134 def _ParseGenerateTestReportOutput(self, output): | |
| 135 """Returns the percentage of tests that passed based on output.""" | |
| 136 percent_passed = 0 | |
| 137 lines = output.split('\n') | |
| 138 | |
| 139 for line in lines: | |
| 140 if line.startswith("Total PASS:"): | |
| 141 # FORMAT: ^TOTAL PASS: num_passed/num_total (percent%)$ | |
| 142 percent_passed = line.split()[3].strip('()%') | |
| 143 Info('Percent of tests passed %s' % percent_passed) | |
| 144 break | |
| 145 | |
| 146 return int(percent_passed) | |
| 147 | |
| 148 def AssertEnoughTestsPassed(self, unittest, output, percent_required_to_pass): | |
| 149 """Helper function that asserts a sufficient number of tests passed. | |
| 150 | |
| 151 Args: | |
| 152 unittest: Handle to the unittest. | |
| 153 output: stdout from a test run. | |
| 154 percent_required_to_pass: percentage required to pass. This should be | |
| 155 fall between 0-100. | |
| 156 Returns: | |
| 157 percent that passed. | |
| 158 """ | |
| 159 Info('Output from VerifyImage():') | |
| 160 print >> sys.stderr, output | |
| 161 sys.stderr.flush() | |
| 162 percent_passed = self._ParseGenerateTestReportOutput(output) | |
| 163 Info('Percent passed: %d vs. Percent required: %d' % ( | |
| 164 percent_passed, percent_required_to_pass)) | |
| 165 unittest.assertTrue(percent_passed >= percent_required_to_pass) | |
| 166 return percent_passed | |
| 167 | |
| 168 def PerformUpdate(self, image_path, src_image_path='', stateful_change='old', | |
| 169 proxy_port=None, private_key_path=None): | |
| 170 """Performs an update using _UpdateImage and reports any error. | |
| 171 | |
| 172 Subclasses should not override this method but override _UpdateImage | |
| 173 instead. | |
| 174 | |
| 175 Args: | |
| 176 image_path: Path to the image to update with. This image must be a test | |
| 177 image. | |
| 178 src_image_path: Optional. If set, perform a delta update using the | |
| 179 image specified by the path as the source image. | |
| 180 stateful_change: How to modify the stateful partition. Values are: | |
| 181 'old': Don't modify stateful partition. Just update normally. | |
| 182 'clean': Uses clobber-state to wipe the stateful partition with the | |
| 183 exception of code needed for ssh. | |
| 184 proxy_port: Port to have the client connect to. For use with | |
| 185 CrosTestProxy. | |
| 186 Raises an UpdateException if _UpdateImage returns an error. | |
| 187 """ | |
| 188 try: | |
| 189 if not self.use_delta_updates: src_image_path = '' | |
| 190 if private_key_path: | |
| 191 key_to_use = private_key_path | |
| 192 else: | |
| 193 key_to_use = self.private_key | |
| 194 | |
| 195 self._UpdateImage(image_path, src_image_path, stateful_change, proxy_port, | |
| 196 key_to_use) | |
| 197 except UpdateException as err: | |
| 198 # If the update fails, print it out | |
| 199 Warning(err.stdout) | |
| 200 raise | |
| 201 | |
| 202 def AttemptUpdateWithPayloadExpectedFailure(self, payload, expected_msg): | |
| 203 """Attempt a payload update, expect it to fail with expected log""" | |
| 204 try: | |
| 205 self._UpdateUsingPayload(payload) | |
| 206 except UpdateException as err: | |
| 207 # Will raise ValueError if expected is not found. | |
| 208 if re.search(re.escape(expected_msg), err.stdout, re.MULTILINE): | |
| 209 return | |
| 210 else: | |
| 211 Warning("Didn't find '%s' in:" % expected_msg) | |
| 212 Warning(err.stdout) | |
| 213 | |
| 214 self.fail('We managed to update when failure was expected') | |
| 215 | |
| 216 def AttemptUpdateWithFilter(self, filter, proxy_port=8081): | |
| 217 """Update through a proxy, with a specified filter, and expect success.""" | |
| 218 | |
| 219 self.PrepareBase(self.target_image_path) | |
| 220 | |
| 221 # The devserver runs at port 8080 by default. We assume that here, and | |
| 222 # start our proxy at a different. We then tell our update tools to | |
| 223 # have the client connect to our proxy_port instead of 8080. | |
| 224 proxy = cros_test_proxy.CrosTestProxy(port_in=proxy_port, | |
| 225 address_out='127.0.0.1', | |
| 226 port_out=8080, | |
| 227 filter=filter) | |
| 228 proxy.serve_forever_in_thread() | |
| 229 | |
| 230 # This update is expected to fail... | |
| 231 try: | |
| 232 self.PerformUpdate(self.target_image_path, self.target_image_path, | |
| 233 proxy_port=proxy_port) | |
| 234 finally: | |
| 235 proxy.shutdown() | |
| 236 | |
| 237 # -------- Functions that subclasses should override --------- | |
| 238 | |
| 239 @classmethod | |
| 240 def ProcessOptions(cls, parser, options): | |
| 241 """Processes options. | |
| 242 | |
| 243 Static method that should be called from main. Subclasses should also | |
| 244 call their parent method if they override it. | |
| 245 """ | |
| 246 cls.verbose = options.verbose | |
| 247 cls.base_image_path = options.base_image | |
| 248 cls.target_image_path = options.target_image | |
| 249 cls.use_delta_updates = options.delta | |
| 250 cls.board = options.board | |
| 251 cls.private_key = options.private_key | |
| 252 cls.clean = options.clean | |
| 253 if options.quick_test: | |
| 254 cls.verify_suite = 'build_RootFilesystemSize' | |
| 255 else: | |
| 256 cls.verify_suite = 'suite_Smoke' | |
| 257 | |
| 258 # Sanity checks. | |
| 259 if not cls.base_image_path: | |
| 260 parser.error('Need path to base image for vm.') | |
| 261 elif not os.path.exists(cls.base_image_path): | |
| 262 Die('%s does not exist' % cls.base_image_path) | |
| 263 | |
| 264 if not cls.target_image_path: | |
| 265 parser.error('Need path to target image to update with.') | |
| 266 elif not os.path.exists(cls.target_image_path): | |
| 267 Die('%s does not exist' % cls.target_image_path) | |
| 268 | |
| 269 def PrepareBase(self, image_path): | |
| 270 """Prepares target with base_image_path.""" | |
| 271 pass | |
| 272 | |
| 273 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', | |
| 274 proxy_port=None, private_key_path=None): | |
| 275 """Implementation of an actual update. | |
| 276 | |
| 277 See PerformUpdate for description of args. Subclasses must override this | |
| 278 method with the correct update procedure for the class. | |
| 279 """ | |
| 280 pass | |
| 281 | |
| 282 def _UpdateUsingPayload(self, update_path, stateful_change='old', | |
| 283 proxy_port=None): | |
| 284 """Updates target with the pre-generated update stored in update_path. | |
| 285 | |
| 286 Subclasses must override this method with the correct update procedure for | |
| 287 the class. | |
| 288 | |
| 289 Args: | |
| 290 update_path: Path to the image to update with. This directory should | |
| 291 contain both update.gz, and stateful.image.gz | |
| 292 proxy_port: Port to have the client connect to. For use with | |
| 293 CrosTestProxy. | |
| 294 """ | |
| 295 pass | |
| 296 | |
| 297 def VerifyImage(self, percent_required_to_pass): | |
| 298 """Verifies the image with tests. | |
| 299 | |
| 300 Verifies that the test images passes the percent required. Subclasses must | |
| 301 override this method with the correct update procedure for the class. | |
| 302 | |
| 303 Args: | |
| 304 percent_required_to_pass: percentage required to pass. This should be | |
| 305 fall between 0-100. | |
| 306 | |
| 307 Returns: | |
| 308 Returns the percent that passed. | |
| 309 """ | |
| 310 pass | |
| 311 | |
| 312 # -------- Tests --------- | |
| 313 | |
| 314 def testUpdateKeepStateful(self): | |
| 315 """Tests if we can update normally. | |
| 316 | |
| 317 This test checks that we can update by updating the stateful partition | |
| 318 rather than wiping it. | |
| 319 """ | |
| 320 # Just make sure some tests pass on original image. Some old images | |
| 321 # don't pass many tests. | |
| 322 self.PrepareBase(self.base_image_path) | |
| 323 # TODO(sosa): move to 100% once we start testing using the autotest paired | |
| 324 # with the dev channel. | |
| 325 percent_passed = self.VerifyImage(10) | |
| 326 | |
| 327 # Update to - all tests should pass on new image. | |
| 328 self.PerformUpdate(self.target_image_path, self.base_image_path) | |
| 329 self.VerifyImage(100) | |
| 330 | |
| 331 # Update from - same percentage should pass that originally passed. | |
| 332 self.PerformUpdate(self.base_image_path, self.target_image_path) | |
| 333 self.VerifyImage(percent_passed) | |
| 334 | |
| 335 def testUpdateWipeStateful(self): | |
| 336 """Tests if we can update after cleaning the stateful partition. | |
| 337 | |
| 338 This test checks that we can update successfully after wiping the | |
| 339 stateful partition. | |
| 340 """ | |
| 341 # Just make sure some tests pass on original image. Some old images | |
| 342 # don't pass many tests. | |
| 343 self.PrepareBase(self.base_image_path) | |
| 344 # TODO(sosa): move to 100% once we start testing using the autotest paired | |
| 345 # with the dev channel. | |
| 346 percent_passed = self.VerifyImage(10) | |
| 347 | |
| 348 # Update to - all tests should pass on new image. | |
| 349 self.PerformUpdate(self.target_image_path, self.base_image_path, 'clean') | |
| 350 self.VerifyImage(100) | |
| 351 | |
| 352 # Update from - same percentage should pass that originally passed. | |
| 353 self.PerformUpdate(self.base_image_path, self.target_image_path, 'clean') | |
| 354 self.VerifyImage(percent_passed) | |
| 355 | |
| 356 # TODO(sosa): Get test to work with verbose. | |
| 357 def NotestPartialUpdate(self): | |
| 358 """Tests what happens if we attempt to update with a truncated payload.""" | |
| 359 # Preload with the version we are trying to test. | |
| 360 self.PrepareBase(self.target_image_path) | |
| 361 | |
| 362 # Image can be updated at: | |
| 363 # ~chrome-eng/chromeos/localmirror/autest-images | |
| 364 url = 'http://gsdview.appspot.com/chromeos-localmirror/' \ | |
| 365 'autest-images/truncated_image.gz' | |
| 366 payload = os.path.join(self.download_folder, 'truncated_image.gz') | |
| 367 | |
| 368 # Read from the URL and write to the local file | |
| 369 urllib.urlretrieve(url, payload) | |
| 370 | |
| 371 expected_msg = 'download_hash_data == update_check_response_hash failed' | |
| 372 self.AttemptUpdateWithPayloadExpectedFailure(payload, expected_msg) | |
| 373 | |
| 374 # TODO(sosa): Get test to work with verbose. | |
| 375 def NotestCorruptedUpdate(self): | |
| 376 """Tests what happens if we attempt to update with a corrupted payload.""" | |
| 377 # Preload with the version we are trying to test. | |
| 378 self.PrepareBase(self.target_image_path) | |
| 379 | |
| 380 # Image can be updated at: | |
| 381 # ~chrome-eng/chromeos/localmirror/autest-images | |
| 382 url = 'http://gsdview.appspot.com/chromeos-localmirror/' \ | |
| 383 'autest-images/corrupted_image.gz' | |
| 384 payload = os.path.join(self.download_folder, 'corrupted.gz') | |
| 385 | |
| 386 # Read from the URL and write to the local file | |
| 387 urllib.urlretrieve(url, payload) | |
| 388 | |
| 389 # This update is expected to fail... | |
| 390 expected_msg = 'zlib inflate() error:-3' | |
| 391 self.AttemptUpdateWithPayloadExpectedFailure(payload, expected_msg) | |
| 392 | |
| 393 def testInterruptedUpdate(self): | |
| 394 """Tests what happens if we interrupt payload delivery 3 times.""" | |
| 395 | |
| 396 class InterruptionFilter(cros_test_proxy.Filter): | |
| 397 """This filter causes the proxy to interrupt the download 3 times | |
| 398 | |
| 399 It does this by closing the first three connections to transfer | |
| 400 2M total in the outbound connection after they transfer the | |
| 401 2M. | |
| 402 """ | |
| 403 def __init__(self): | |
| 404 """Defines variable shared across all connections""" | |
| 405 self.close_count = 0 | |
| 406 | |
| 407 def setup(self): | |
| 408 """Called once at the start of each connection.""" | |
| 409 self.data_size = 0 | |
| 410 | |
| 411 def OutBound(self, data): | |
| 412 """Called once per packet for outgoing data. | |
| 413 | |
| 414 The first three connections transferring more than 2M | |
| 415 outbound will be closed. | |
| 416 """ | |
| 417 if self.close_count < 3: | |
| 418 if self.data_size > (2 * 1024 * 1024): | |
| 419 self.close_count += 1 | |
| 420 return None | |
| 421 | |
| 422 self.data_size += len(data) | |
| 423 return data | |
| 424 | |
| 425 self.AttemptUpdateWithFilter(InterruptionFilter(), proxy_port=8082) | |
| 426 | |
| 427 def testDelayedUpdate(self): | |
| 428 """Tests what happens if some data is delayed during update delivery""" | |
| 429 | |
| 430 class DelayedFilter(cros_test_proxy.Filter): | |
| 431 """Causes intermittent delays in data transmission. | |
| 432 | |
| 433 It does this by inserting 3 20 second delays when transmitting | |
| 434 data after 2M has been sent. | |
| 435 """ | |
| 436 def setup(self): | |
| 437 """Called once at the start of each connection.""" | |
| 438 self.data_size = 0 | |
| 439 self.delay_count = 0 | |
| 440 | |
| 441 def OutBound(self, data): | |
| 442 """Called once per packet for outgoing data. | |
| 443 | |
| 444 The first three packets after we reach 2M transferred | |
| 445 are delayed by 20 seconds. | |
| 446 """ | |
| 447 if self.delay_count < 3: | |
| 448 if self.data_size > (2 * 1024 * 1024): | |
| 449 self.delay_count += 1 | |
| 450 time.sleep(20) | |
| 451 | |
| 452 self.data_size += len(data) | |
| 453 return data | |
| 454 | |
| 455 self.AttemptUpdateWithFilter(DelayedFilter(), proxy_port=8083) | |
| 456 | |
| 457 def SimpleTest(self): | |
| 458 """A simple update that updates once from a base image to a target. | |
| 459 | |
| 460 We explicitly don't use test prefix so that isn't run by default. Can be | |
| 461 run using test_prefix option. | |
| 462 """ | |
| 463 self.PrepareBase(self.base_image_path) | |
| 464 self.PerformUpdate(self.target_image_path, self.base_image_path) | |
| 465 self.VerifyImage(100) | |
| 466 | |
| 467 | |
| 468 class RealAUTest(unittest.TestCase, AUTest): | |
| 469 """Test harness for updating real images.""" | |
| 470 | |
| 471 def setUp(self): | |
| 472 AUTest.setUp(self) | |
| 473 | |
| 474 @classmethod | |
| 475 def ProcessOptions(cls, parser, options): | |
| 476 """Processes non-vm-specific options.""" | |
| 477 AUTest.ProcessOptions(parser, options) | |
| 478 cls.remote = options.remote | |
| 479 | |
| 480 if not cls.remote: | |
| 481 parser.error('We require a remote address for real tests.') | |
| 482 | |
| 483 def PrepareBase(self, image_path): | |
| 484 """Auto-update to base image to prepare for test.""" | |
| 485 self._PrepareRealBase(image_path) | |
| 486 | |
| 487 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', | |
| 488 proxy_port=None, private_key_path=None): | |
| 489 """Updates a remote image using image_to_live.sh.""" | |
| 490 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | |
| 491 cmd = ['%s/image_to_live.sh' % self.crosutils, | |
| 492 '--remote=%s' % self.remote, | |
| 493 stateful_change_flag, | |
| 494 '--verify', | |
| 495 ] | |
| 496 self.AppendUpdateFlags(cmd, image_path, src_image_path, proxy_port, | |
| 497 private_key_path) | |
| 498 self.RunUpdateCmd(cmd) | |
| 499 | |
| 500 def _UpdateUsingPayload(self, update_path, stateful_change='old', | |
| 501 proxy_port=None): | |
| 502 """Updates a remote image using image_to_live.sh.""" | |
| 503 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | |
| 504 cmd = ['%s/image_to_live.sh' % self.crosutils, | |
| 505 '--payload=%s' % update_path, | |
| 506 '--remote=%s' % self.remote, | |
| 507 stateful_change_flag, | |
| 508 '--verify', | |
| 509 ] | |
| 510 if proxy_port: cmd.append('--proxy_port=%s' % proxy_port) | |
| 511 self.RunUpdateCmd(cmd) | |
| 512 | |
| 513 def VerifyImage(self, percent_required_to_pass): | |
| 514 """Verifies an image using run_remote_tests.sh with verification suite.""" | |
| 515 output = RunCommand([ | |
| 516 '%s/run_remote_tests.sh' % self.crosutils, | |
| 517 '--remote=%s' % self.remote, | |
| 518 self.verify_suite, | |
| 519 ], error_ok=True, enter_chroot=False, redirect_stdout=True) | |
| 520 return self.AssertEnoughTestsPassed(self, output, percent_required_to_pass) | |
| 521 | |
| 522 | |
| 523 class VirtualAUTest(unittest.TestCase, AUTest): | |
| 524 """Test harness for updating virtual machines.""" | |
| 525 | |
| 526 # Class variables used to acquire individual VM variables per test. | |
| 527 _vm_lock = threading.Lock() | |
| 528 _next_port = 9222 | |
| 529 | |
| 530 def _KillExistingVM(self, pid_file): | |
| 531 if os.path.exists(pid_file): | |
| 532 Warning('Existing %s found. Deleting and killing process' % | |
| 533 pid_file) | |
| 534 RunCommand(['./cros_stop_vm', '--kvm_pid=%s' % pid_file], | |
| 535 cwd=self.crosutilsbin) | |
| 536 | |
| 537 assert not os.path.exists(pid_file) | |
| 538 | |
| 539 def _AcquireUniquePortAndPidFile(self): | |
| 540 """Acquires unique ssh port and pid file for VM.""" | |
| 541 with VirtualAUTest._vm_lock: | |
| 542 self._ssh_port = VirtualAUTest._next_port | |
| 543 self._kvm_pid_file = '/tmp/kvm.%d' % self._ssh_port | |
| 544 VirtualAUTest._next_port += 1 | |
| 545 | |
| 546 def setUp(self): | |
| 547 """Unit test overriden method. Is called before every test.""" | |
| 548 AUTest.setUp(self) | |
| 549 self._AcquireUniquePortAndPidFile() | |
| 550 self._KillExistingVM(self._kvm_pid_file) | |
| 551 | |
| 552 def tearDown(self): | |
| 553 self._KillExistingVM(self._kvm_pid_file) | |
| 554 | |
| 555 @classmethod | |
| 556 def ProcessOptions(cls, parser, options): | |
| 557 """Processes vm-specific options.""" | |
| 558 AUTest.ProcessOptions(parser, options) | |
| 559 | |
| 560 # Communicate flags to tests. | |
| 561 cls.graphics_flag = '' | |
| 562 if options.no_graphics: cls.graphics_flag = '--no_graphics' | |
| 563 if not cls.board: parser.error('Need board to convert base image to vm.') | |
| 564 | |
| 565 def PrepareBase(self, image_path): | |
| 566 """Creates an update-able VM based on base image.""" | |
| 567 self._PrepareVMBase(image_path) | |
| 568 | |
| 569 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', | |
| 570 proxy_port='', private_key_path=None): | |
| 571 """Updates VM image with image_path.""" | |
| 572 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | |
| 573 if src_image_path and self._first_update: | |
| 574 src_image_path = self.vm_image_path | |
| 575 self._first_update = False | |
| 576 | |
| 577 cmd = ['%s/cros_run_vm_update' % self.crosutilsbin, | |
| 578 '--vm_image_path=%s' % self.vm_image_path, | |
| 579 '--snapshot', | |
| 580 self.graphics_flag, | |
| 581 '--persist', | |
| 582 '--kvm_pid=%s' % self._kvm_pid_file, | |
| 583 '--ssh_port=%s' % self._ssh_port, | |
| 584 stateful_change_flag, | |
| 585 ] | |
| 586 | |
| 587 self.AppendUpdateFlags(cmd, image_path, src_image_path, proxy_port, | |
| 588 private_key_path) | |
| 589 self.RunUpdateCmd(cmd) | |
| 590 | |
| 591 def _UpdateUsingPayload(self, update_path, stateful_change='old', | |
| 592 proxy_port=None): | |
| 593 """Updates a vm image using cros_run_vm_update.""" | |
| 594 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | |
| 595 cmd = ['%s/cros_run_vm_update' % self.crosutilsbin, | |
| 596 '--payload=%s' % update_path, | |
| 597 '--vm_image_path=%s' % self.vm_image_path, | |
| 598 '--snapshot', | |
| 599 self.graphics_flag, | |
| 600 '--persist', | |
| 601 '--kvm_pid=%s' % self._kvm_pid_file, | |
| 602 '--ssh_port=%s' % self._ssh_port, | |
| 603 stateful_change_flag, | |
| 604 ] | |
| 605 if proxy_port: cmd.append('--proxy_port=%s' % proxy_port) | |
| 606 self.RunUpdateCmd(cmd) | |
| 607 | |
| 608 def VerifyImage(self, percent_required_to_pass): | |
| 609 """Runs vm smoke suite to verify image.""" | |
| 610 # image_to_live already verifies lsb-release matching. This is just | |
| 611 # for additional steps. | |
| 612 | |
| 613 commandWithArgs = ['%s/cros_run_vm_test' % self.crosutilsbin, | |
| 614 '--image_path=%s' % self.vm_image_path, | |
| 615 '--snapshot', | |
| 616 '--persist', | |
| 617 '--kvm_pid=%s' % self._kvm_pid_file, | |
| 618 '--ssh_port=%s' % self._ssh_port, | |
| 619 self.verify_suite, | |
| 620 ] | |
| 621 | |
| 622 if self.graphics_flag: | |
| 623 commandWithArgs.append(self.graphics_flag) | |
| 624 | |
| 625 output = RunCommand(commandWithArgs, error_ok=True, enter_chroot=False, | |
| 626 redirect_stdout=True) | |
| 627 return self.AssertEnoughTestsPassed(self, output, percent_required_to_pass) | |
| 628 | |
| 629 | |
| 630 class PregenerateAUDeltas(unittest.TestCase, AUTest): | |
| 631 """Magical class that emulates an AUTest to store deltas we will generate. | |
| 632 | |
| 633 This class emulates an AUTest such that when it runs as a TestCase it runs | |
| 634 through the exact up | |
| 635 """ | |
| 636 delta_list = {} | |
| 637 | |
| 638 def setUp(self): | |
| 639 AUTest.setUp(self) | |
| 640 | |
| 641 def tearDown(self): | |
| 642 pass | |
| 643 | |
| 644 @classmethod | |
| 645 def ProcessOptions(cls, parser, options): | |
| 646 AUTest.ProcessOptions(parser, options) | |
| 647 cls.au_type = options.type | |
| 648 | |
| 649 def PrepareBase(self, image_path): | |
| 650 if self.au_type == 'vm': | |
| 651 self._PrepareVMBase(image_path) | |
| 652 else: | |
| 653 self._PrepareRealBase(image_path) | |
| 654 | |
| 655 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', | |
| 656 proxy_port=None, private_key_path=None): | |
| 657 if self.au_type == 'vm' and src_image_path and self._first_update: | |
| 658 src_image_path = self.vm_image_path | |
| 659 self._first_update = False | |
| 660 | |
| 661 # Generate a value that combines delta with private key path. | |
| 662 val = src_image_path | |
| 663 if private_key_path: val = '%s+%s' % (val, private_key_path) | |
| 664 if not self.delta_list.has_key(image_path): | |
| 665 self.delta_list[image_path] = set([val]) | |
| 666 else: | |
| 667 self.delta_list[image_path].add(val) | |
| 668 | |
| 669 def AttemptUpdateWithPayloadExpectedFailure(self, payload, expected_msg): | |
| 670 pass | |
| 671 | |
| 672 def VerifyImage(self, percent_required_to_pass): | |
| 673 pass | |
| 674 | |
| 675 | |
| 676 class ParallelJob(threading.Thread): | |
| 677 """Small wrapper for threading. Thread that releases a semaphores on exit.""" | |
| 678 | |
| 679 def __init__(self, starting_semaphore, ending_semaphore, target, args): | |
| 680 """Initializes an instance of a job. | |
| 681 | |
| 682 Args: | |
| 683 starting_semaphore: Semaphore used by caller to wait on such that | |
| 684 there isn't more than a certain number of threads running. Should | |
| 685 be initialized to a value for the number of threads wanting to be run | |
| 686 at a time. | |
| 687 ending_semaphore: Semaphore is released every time a job ends. Should be | |
| 688 initialized to 0 before starting first job. Should be acquired once for | |
| 689 each job. Threading.Thread.join() has a bug where if the run function | |
| 690 terminates too quickly join() will hang forever. | |
| 691 target: The func to run. | |
| 692 args: Args to pass to the fun. | |
| 693 """ | |
| 694 threading.Thread.__init__(self, target=target, args=args) | |
| 695 self._target = target | |
| 696 self._args = args | |
| 697 self._starting_semaphore = starting_semaphore | |
| 698 self._ending_semaphore = ending_semaphore | |
| 699 self._output = None | |
| 700 self._completed = False | |
| 701 | |
| 702 def run(self): | |
| 703 """Thread override. Runs the method specified and sets output.""" | |
| 704 try: | |
| 705 self._output = self._target(*self._args) | |
| 706 finally: | |
| 707 # Our own clean up. | |
| 708 self._Cleanup() | |
| 709 self._completed = True | |
| 710 # From threading.py to avoid a refcycle. | |
| 711 del self._target, self._args | |
| 712 | |
| 713 def GetOutput(self): | |
| 714 """Returns the output of the method run.""" | |
| 715 assert self._completed, 'GetOutput called before thread was run.' | |
| 716 return self._output | |
| 717 | |
| 718 def _Cleanup(self): | |
| 719 """Releases semaphores for a waiting caller.""" | |
| 720 Info('Completed job %s' % self) | |
| 721 self._starting_semaphore.release() | |
| 722 self._ending_semaphore.release() | |
| 723 | |
| 724 def __str__(self): | |
| 725 return '%s(%s)' % (self._target, self._args) | |
| 726 | |
| 727 | |
| 728 class DevServerWrapper(threading.Thread): | |
| 729 """A Simple wrapper around a devserver instance.""" | |
| 730 | |
| 731 def __init__(self): | |
| 732 self.proc = None | |
| 733 threading.Thread.__init__(self) | |
| 734 | |
| 735 def run(self): | |
| 736 # Kill previous running instance of devserver if it exists. | |
| 737 RunCommand(['sudo', 'pkill', '-f', 'devserver.py'], error_ok=True, | |
| 738 print_cmd=False) | |
| 739 RunCommand(['sudo', | |
| 740 'start_devserver', | |
| 741 '--archive_dir=./static', | |
| 742 '--client_prefix=ChromeOSUpdateEngine', | |
| 743 '--production', | |
| 744 ], enter_chroot=True, print_cmd=False) | |
| 745 | |
| 746 def Stop(self): | |
| 747 """Kills the devserver instance.""" | |
| 748 RunCommand(['sudo', 'pkill', '-f', 'devserver.py'], error_ok=True, | |
| 749 print_cmd=False) | |
| 750 | |
| 751 @classmethod | |
| 752 def GetDevServerURL(cls, port, sub_dir): | |
| 753 """Returns the dev server url for a given port and sub directory.""" | |
| 754 ip_addr = GetIPAddress() | |
| 755 if not port: port = 8080 | |
| 756 url = 'http://%(ip)s:%(port)s/%(dir)s' % {'ip': ip_addr, | |
| 757 'port': str(port), | |
| 758 'dir': sub_dir} | |
| 759 return url | |
| 760 | |
| 761 | |
| 762 def _GenerateUpdateId(target, src, key): | |
| 763 """Returns a simple representation id of target and src paths.""" | |
| 764 update_id = target | |
| 765 if src: update_id = '->'.join([update_id, src]) | |
| 766 if key: update_id = '+'.join([update_id, key]) | |
| 767 return update_id | |
| 768 | |
| 769 | |
| 770 def _RunParallelJobs(number_of_sumultaneous_jobs, jobs, jobs_args, | |
| 771 print_status): | |
| 772 | |
| 773 """Runs set number of specified jobs in parallel. | |
| 774 | |
| 775 Args: | |
| 776 number_of_simultaneous_jobs: Max number of threads to be run in parallel. | |
| 777 jobs: Array of methods to run. | |
| 778 jobs_args: Array of args associated with method calls. | |
| 779 print_status: True if you'd like this to print out .'s as it runs jobs. | |
| 780 Returns: | |
| 781 Returns an array of results corresponding to each thread. | |
| 782 """ | |
| 783 def _TwoTupleize(x, y): | |
| 784 return (x, y) | |
| 785 | |
| 786 threads = [] | |
| 787 job_start_semaphore = threading.Semaphore(number_of_sumultaneous_jobs) | |
| 788 join_semaphore = threading.Semaphore(0) | |
| 789 assert len(jobs) == len(jobs_args), 'Length of args array is wrong.' | |
| 790 | |
| 791 # Create the parallel jobs. | |
| 792 for job, args in map(_TwoTupleize, jobs, jobs_args): | |
| 793 thread = ParallelJob(job_start_semaphore, join_semaphore, target=job, | |
| 794 args=args) | |
| 795 threads.append(thread) | |
| 796 | |
| 797 # Cache sudo access. | |
| 798 RunCommand(['sudo', 'echo', 'Starting test harness'], | |
| 799 print_cmd=False, redirect_stdout=True, redirect_stderr=True) | |
| 800 | |
| 801 # We use a semaphore to ensure we don't run more jobs that required. | |
| 802 # After each thread finishes, it releases (increments semaphore). | |
| 803 # Acquire blocks of num jobs reached and continues when a thread finishes. | |
| 804 for next_thread in threads: | |
| 805 job_start_semaphore.acquire(blocking=True) | |
| 806 Info('Starting job %s' % next_thread) | |
| 807 next_thread.start() | |
| 808 | |
| 809 # Wait on the rest of the threads to finish. | |
| 810 Info('Waiting for threads to complete.') | |
| 811 for thread in threads: | |
| 812 while not join_semaphore.acquire(blocking=False): | |
| 813 time.sleep(5) | |
| 814 if print_status: | |
| 815 print >> sys.stderr, '.', | |
| 816 | |
| 817 return [thread.GetOutput() for thread in threads] | |
| 818 | |
| 819 | |
| 820 def _PrepareTestSuite(parser, options, test_class): | |
| 821 """Returns a prepared test suite given by the options and test class.""" | |
| 822 test_class.ProcessOptions(parser, options) | |
| 823 test_loader = unittest.TestLoader() | |
| 824 test_loader.testMethodPrefix = options.test_prefix | |
| 825 return test_loader.loadTestsFromTestCase(test_class) | |
| 826 | |
| 827 | |
| 828 def _PregenerateUpdates(parser, options): | |
| 829 """Determines all deltas that will be generated and generates them. | |
| 830 | |
| 831 This method effectively pre-generates the dev server cache for all tests. | |
| 832 | |
| 833 Args: | |
| 834 parser: parser from main. | |
| 835 options: options from parsed parser. | |
| 836 Returns: | |
| 837 Dictionary of Update Identifiers->Relative cache locations. | |
| 838 Raises: | |
| 839 UpdateException if we fail to generate an update. | |
| 840 """ | |
| 841 def _GenerateVMUpdate(target, src, private_key_path): | |
| 842 """Generates an update using the devserver.""" | |
| 843 command = ['./enter_chroot.sh', | |
| 844 '--nogit_config', | |
| 845 '--', | |
| 846 'sudo', | |
| 847 'start_devserver', | |
| 848 '--pregenerate_update', | |
| 849 '--exit', | |
| 850 ] | |
| 851 # Add actual args to command. | |
| 852 command.append('--image=%s' % ReinterpretPathForChroot(target)) | |
| 853 if src: command.append('--src_image=%s' % ReinterpretPathForChroot(src)) | |
| 854 if options.type == 'vm': command.append('--for_vm') | |
| 855 if private_key_path: | |
| 856 command.append('--private_key=%s' % | |
| 857 ReinterpretPathForChroot(private_key_path)) | |
| 858 | |
| 859 return RunCommandCaptureOutput(command, combine_stdout_stderr=True, | |
| 860 print_cmd=True) | |
| 861 | |
| 862 # Get the list of deltas by mocking out update method in test class. | |
| 863 test_suite = _PrepareTestSuite(parser, options, PregenerateAUDeltas) | |
| 864 test_result = unittest.TextTestRunner(verbosity=0).run(test_suite) | |
| 865 if not test_result.wasSuccessful(): | |
| 866 raise UpdateException(1, 'Error finding updates to generate.') | |
| 867 | |
| 868 Info('The following delta updates are required.') | |
| 869 update_ids = [] | |
| 870 jobs = [] | |
| 871 args = [] | |
| 872 for target, srcs in PregenerateAUDeltas.delta_list.items(): | |
| 873 for src_key in srcs: | |
| 874 (src, _ , key) = src_key.partition('+') | |
| 875 # TODO(sosa): Add private key as part of caching name once devserver can | |
| 876 # handle it its own cache. | |
| 877 update_id = _GenerateUpdateId(target=target, src=src, key=key) | |
| 878 print >> sys.stderr, 'AU: %s' % update_id | |
| 879 update_ids.append(update_id) | |
| 880 jobs.append(_GenerateVMUpdate) | |
| 881 args.append((target, src, key)) | |
| 882 | |
| 883 raw_results = _RunParallelJobs(options.jobs, jobs, args, print_status=True) | |
| 884 results = [] | |
| 885 | |
| 886 # Looking for this line in the output. | |
| 887 key_line_re = re.compile('^PREGENERATED_UPDATE=([\w/.]+)') | |
| 888 for result in raw_results: | |
| 889 (return_code, output, _) = result | |
| 890 if return_code != 0: | |
| 891 Warning(output) | |
| 892 raise UpdateException(return_code, 'Failed to generate all updates.') | |
| 893 else: | |
| 894 for line in output.splitlines(): | |
| 895 match = key_line_re.search(line) | |
| 896 if match: | |
| 897 # Convert blah/blah/update.gz -> update/blah/blah. | |
| 898 path_to_update_gz = match.group(1).rstrip() | |
| 899 (path_to_update_dir, _, _) = path_to_update_gz.rpartition( | |
| 900 '/update.gz') | |
| 901 results.append('/'.join(['update', path_to_update_dir])) | |
| 902 break | |
| 903 | |
| 904 # Make sure all generation of updates returned cached locations. | |
| 905 if len(raw_results) != len(results): | |
| 906 raise UpdateException(1, 'Insufficient number cache directories returned.') | |
| 907 | |
| 908 # Build the dictionary from our id's and returned cache paths. | |
| 909 cache_dictionary = {} | |
| 910 for index, id in enumerate(update_ids): | |
| 911 cache_dictionary[id] = results[index] | |
| 912 | |
| 913 return cache_dictionary | |
| 914 | |
| 915 | |
| 916 def _RunTestsInParallel(parser, options, test_class): | |
| 917 """Runs the tests given by the options and test_class in parallel.""" | |
| 918 threads = [] | |
| 919 args = [] | |
| 920 test_suite = _PrepareTestSuite(parser, options, test_class) | |
| 921 for test in test_suite: | |
| 922 test_name = test.id() | |
| 923 test_case = unittest.TestLoader().loadTestsFromName(test_name) | |
| 924 threads.append(unittest.TextTestRunner().run) | |
| 925 args.append(test_case) | |
| 926 | |
| 927 results = _RunParallelJobs(options.jobs, threads, args, print_status=False) | |
| 928 for test_result in results: | |
| 929 if not test_result.wasSuccessful(): | |
| 930 Die('Test harness was not successful') | |
| 931 | |
| 932 | |
| 933 def InsertPublicKeyIntoImage(image_path, key_path): | |
| 934 """Inserts public key into image @ static update_engine location.""" | |
| 935 from_dir = os.path.dirname(image_path) | |
| 936 image = os.path.basename(image_path) | |
| 937 crosutils_dir = os.path.abspath(__file__).rsplit('/', 2)[0] | |
| 938 target_key_path = 'usr/share/update_engine/update-payload-key.pub.pem' | |
| 939 | |
| 940 # Temporary directories for this function. | |
| 941 rootfs_dir = tempfile.mkdtemp(suffix='rootfs', prefix='tmp') | |
| 942 stateful_dir = tempfile.mkdtemp(suffix='stateful', prefix='tmp') | |
| 943 | |
| 944 Info('Copying %s into %s' % (key_path, image_path)) | |
| 945 try: | |
| 946 RunCommand(['./mount_gpt_image.sh', | |
| 947 '--from=%s' % from_dir, | |
| 948 '--image=%s' % image, | |
| 949 '--rootfs_mountpt=%s' % rootfs_dir, | |
| 950 '--stateful_mountpt=%s' % stateful_dir, | |
| 951 ], print_cmd=False, redirect_stdout=True, | |
| 952 redirect_stderr=True, cwd=crosutils_dir) | |
| 953 path = os.path.join(rootfs_dir, target_key_path) | |
| 954 dir_path = os.path.dirname(path) | |
| 955 RunCommand(['sudo', 'mkdir', '--parents', dir_path], print_cmd=False) | |
| 956 RunCommand(['sudo', 'cp', '--force', '-p', key_path, path], | |
| 957 print_cmd=False) | |
| 958 finally: | |
| 959 # Unmount best effort regardless. | |
| 960 RunCommand(['./mount_gpt_image.sh', | |
| 961 '--unmount', | |
| 962 '--rootfs_mountpt=%s' % rootfs_dir, | |
| 963 '--stateful_mountpt=%s' % stateful_dir, | |
| 964 ], print_cmd=False, redirect_stdout=True, redirect_stderr=True, | |
| 965 cwd=crosutils_dir) | |
| 966 # Clean up our directories. | |
| 967 os.rmdir(rootfs_dir) | |
| 968 os.rmdir(stateful_dir) | |
| 969 | |
| 970 RunCommand(['bin/cros_make_image_bootable', | |
| 971 ReinterpretPathForChroot(from_dir), | |
| 972 image], | |
| 973 print_cmd=False, redirect_stdout=True, redirect_stderr=True, | |
| 974 enter_chroot=True, cwd=crosutils_dir) | |
| 975 | |
| 976 | |
| 977 def CleanPreviousWork(options): | |
| 978 """Cleans up previous work from the devserver cache and local image cache.""" | |
| 979 Info('Cleaning up previous work.') | |
| 980 # Wipe devserver cache. | |
| 981 RunCommandCaptureOutput( | |
| 982 ['sudo', 'start_devserver', '--clear_cache', '--exit', ], | |
| 983 enter_chroot=True, print_cmd=False, combine_stdout_stderr=True) | |
| 984 | |
| 985 # Clean previous vm images if they exist. | |
| 986 if options.type == 'vm': | |
| 987 target_vm_image_path = '%s/chromiumos_qemu_image.bin' % os.path.dirname( | |
| 988 options.target_image) | |
| 989 base_vm_image_path = '%s/chromiumos_qemu_image.bin' % os.path.dirname( | |
| 990 options.base_image) | |
| 991 if os.path.exists(target_vm_image_path): os.remove(target_vm_image_path) | |
| 992 if os.path.exists(base_vm_image_path): os.remove(base_vm_image_path) | |
| 993 | |
| 994 | |
| 995 def main(): | |
| 996 parser = optparse.OptionParser() | |
| 997 parser.add_option('-b', '--base_image', | |
| 998 help='path to the base image.') | |
| 999 parser.add_option('-r', '--board', | |
| 1000 help='board for the images.') | |
| 1001 parser.add_option('--clean', default=False, dest='clean', action='store_true', | |
| 1002 help='Clean all previous state') | |
| 1003 parser.add_option('--no_delta', action='store_false', default=True, | |
| 1004 dest='delta', | |
| 1005 help='Disable using delta updates.') | |
| 1006 parser.add_option('--no_graphics', action='store_true', | |
| 1007 help='Disable graphics for the vm test.') | |
| 1008 parser.add_option('-j', '--jobs', default=8, type=int, | |
| 1009 help='Number of simultaneous jobs') | |
| 1010 parser.add_option('--public_key', default=None, | |
| 1011 help='Public key to use on images and updates.') | |
| 1012 parser.add_option('--private_key', default=None, | |
| 1013 help='Private key to use on images and updates.') | |
| 1014 parser.add_option('-q', '--quick_test', default=False, action='store_true', | |
| 1015 help='Use a basic test to verify image.') | |
| 1016 parser.add_option('-m', '--remote', | |
| 1017 help='Remote address for real test.') | |
| 1018 parser.add_option('-t', '--target_image', | |
| 1019 help='path to the target image.') | |
| 1020 parser.add_option('--test_prefix', default='test', | |
| 1021 help='Only runs tests with specific prefix i.e. ' | |
| 1022 'testFullUpdateWipeStateful.') | |
| 1023 parser.add_option('-p', '--type', default='vm', | |
| 1024 help='type of test to run: [vm, real]. Default: vm.') | |
| 1025 parser.add_option('--verbose', default=True, action='store_true', | |
| 1026 help='Print out rather than capture output as much as ' | |
| 1027 'possible.') | |
| 1028 (options, leftover_args) = parser.parse_args() | |
| 1029 | |
| 1030 if leftover_args: parser.error('Found unsupported flags: %s' % leftover_args) | |
| 1031 | |
| 1032 assert options.target_image and os.path.exists(options.target_image), \ | |
| 1033 'Target image path does not exist' | |
| 1034 if not options.base_image: | |
| 1035 Info('Base image not specified. Using target image as base image.') | |
| 1036 options.base_image = options.target_image | |
| 1037 | |
| 1038 # Sanity checks on keys and insert them onto the image. The caches must be | |
| 1039 # cleaned so we know that the vm images and payloads match the possibly new | |
| 1040 # key. | |
| 1041 if options.private_key or options.public_key: | |
| 1042 error_msg = ('Could not find %s key. Both private and public keys must be ' | |
| 1043 'specified if either is specified.') | |
| 1044 assert options.private_key and os.path.exists(options.private_key), \ | |
| 1045 error_msg % 'private' | |
| 1046 assert options.public_key and os.path.exists(options.public_key), \ | |
| 1047 error_msg % 'public' | |
| 1048 InsertPublicKeyIntoImage(options.target_image, options.public_key) | |
| 1049 InsertPublicKeyIntoImage(options.base_image, options.public_key) | |
| 1050 options.clean = True | |
| 1051 | |
| 1052 # Clean up previous work if requested. | |
| 1053 if options.clean: CleanPreviousWork(options) | |
| 1054 | |
| 1055 # Figure out the test_class. | |
| 1056 if options.type == 'vm': test_class = VirtualAUTest | |
| 1057 elif options.type == 'real': test_class = RealAUTest | |
| 1058 else: parser.error('Could not parse harness type %s.' % options.type) | |
| 1059 | |
| 1060 # Generate cache of updates to use during test harness. | |
| 1061 global dev_server_cache | |
| 1062 dev_server_cache = _PregenerateUpdates(parser, options) | |
| 1063 my_server = DevServerWrapper() | |
| 1064 my_server.start() | |
| 1065 try: | |
| 1066 if options.type == 'vm': | |
| 1067 _RunTestsInParallel(parser, options, test_class) | |
| 1068 else: | |
| 1069 # TODO(sosa) - Take in a machine pool for a real test. | |
| 1070 # Can't run in parallel with only one remote device. | |
| 1071 test_suite = _PrepareTestSuite(parser, options, test_class) | |
| 1072 test_result = unittest.TextTestRunner(verbosity=2).run(test_suite) | |
| 1073 if not test_result.wasSuccessful(): Die('Test harness failed.') | |
| 1074 finally: | |
| 1075 my_server.Stop() | |
| 1076 | |
| 1077 | |
| 1078 if __name__ == '__main__': | |
| 1079 main() | |
| OLD | NEW |