OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 import shutil |
2 | 3 |
3 # Copyright (c) 2011 The Chromium OS Authors. All rights reserved. | 4 # 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 # Use of this source code is governed by a BSD-style license that can be |
5 # found in the LICENSE file. | 6 # found in the LICENSE file. |
6 | 7 |
7 """This module runs a suite of Auto Update tests. | 8 """This module runs a suite of Auto Update tests. |
8 | 9 |
9 The tests can be run on either a virtual machine or actual device depending | 10 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 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 Verbose is useful for many of the tests if you want to see individual commands |
12 being run during the update process. | 13 being run during the update process. |
13 """ | 14 """ |
14 | 15 |
15 import optparse | 16 import optparse |
16 import os | 17 import os |
17 import re | 18 import re |
18 import subprocess | 19 import subprocess |
19 import sys | 20 import sys |
| 21 import tempfile |
20 import threading | 22 import threading |
21 import time | 23 import time |
22 import unittest | 24 import unittest |
23 import urllib | 25 import urllib |
24 | 26 |
25 sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) | 27 sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) |
26 from cros_build_lib import Die | 28 from cros_build_lib import Die |
27 from cros_build_lib import GetIPAddress | 29 from cros_build_lib import GetIPAddress |
28 from cros_build_lib import Info | 30 from cros_build_lib import Info |
29 from cros_build_lib import ReinterpretPathForChroot | 31 from cros_build_lib import ReinterpretPathForChroot |
(...skipping 16 matching lines...) Expand all Loading... |
46 class AUTest(object): | 48 class AUTest(object): |
47 """Abstract interface that defines an Auto Update test.""" | 49 """Abstract interface that defines an Auto Update test.""" |
48 verbose = False | 50 verbose = False |
49 | 51 |
50 def setUp(self): | 52 def setUp(self): |
51 unittest.TestCase.setUp(self) | 53 unittest.TestCase.setUp(self) |
52 # Set these up as they are used often. | 54 # Set these up as they are used often. |
53 self.crosutils = os.path.join(os.path.dirname(__file__), '..') | 55 self.crosutils = os.path.join(os.path.dirname(__file__), '..') |
54 self.crosutilsbin = os.path.join(os.path.dirname(__file__)) | 56 self.crosutilsbin = os.path.join(os.path.dirname(__file__)) |
55 self.download_folder = os.path.join(self.crosutils, 'latest_download') | 57 self.download_folder = os.path.join(self.crosutils, 'latest_download') |
| 58 self.vm_image_path = None |
56 if not os.path.exists(self.download_folder): | 59 if not os.path.exists(self.download_folder): |
57 os.makedirs(self.download_folder) | 60 os.makedirs(self.download_folder) |
58 | 61 |
59 # -------- Helper functions --------- | 62 # -------- Helper functions --------- |
60 | 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 |
61 def GetStatefulChangeFlag(self, stateful_change): | 126 def GetStatefulChangeFlag(self, stateful_change): |
62 """Returns the flag to pass to image_to_vm for the stateful change.""" | 127 """Returns the flag to pass to image_to_vm for the stateful change.""" |
63 stateful_change_flag = '' | 128 stateful_change_flag = '' |
64 if stateful_change: | 129 if stateful_change: |
65 stateful_change_flag = '--stateful_update_flag=%s' % stateful_change | 130 stateful_change_flag = '--stateful_update_flag=%s' % stateful_change |
66 | 131 |
67 return stateful_change_flag | 132 return stateful_change_flag |
68 | 133 |
69 def _ParseGenerateTestReportOutput(self, output): | 134 def _ParseGenerateTestReportOutput(self, output): |
70 """Returns the percentage of tests that passed based on output.""" | 135 """Returns the percentage of tests that passed based on output.""" |
(...skipping 23 matching lines...) Expand all Loading... |
94 Info('Output from VerifyImage():') | 159 Info('Output from VerifyImage():') |
95 print >> sys.stderr, output | 160 print >> sys.stderr, output |
96 sys.stderr.flush() | 161 sys.stderr.flush() |
97 percent_passed = self._ParseGenerateTestReportOutput(output) | 162 percent_passed = self._ParseGenerateTestReportOutput(output) |
98 Info('Percent passed: %d vs. Percent required: %d' % ( | 163 Info('Percent passed: %d vs. Percent required: %d' % ( |
99 percent_passed, percent_required_to_pass)) | 164 percent_passed, percent_required_to_pass)) |
100 unittest.assertTrue(percent_passed >= percent_required_to_pass) | 165 unittest.assertTrue(percent_passed >= percent_required_to_pass) |
101 return percent_passed | 166 return percent_passed |
102 | 167 |
103 def PerformUpdate(self, image_path, src_image_path='', stateful_change='old', | 168 def PerformUpdate(self, image_path, src_image_path='', stateful_change='old', |
104 proxy_port=None): | 169 proxy_port=None, private_key_path=None): |
105 """Performs an update using _UpdateImage and reports any error. | 170 """Performs an update using _UpdateImage and reports any error. |
106 | 171 |
107 Subclasses should not override this method but override _UpdateImage | 172 Subclasses should not override this method but override _UpdateImage |
108 instead. | 173 instead. |
109 | 174 |
110 Args: | 175 Args: |
111 image_path: Path to the image to update with. This image must be a test | 176 image_path: Path to the image to update with. This image must be a test |
112 image. | 177 image. |
113 src_image_path: Optional. If set, perform a delta update using the | 178 src_image_path: Optional. If set, perform a delta update using the |
114 image specified by the path as the source image. | 179 image specified by the path as the source image. |
115 stateful_change: How to modify the stateful partition. Values are: | 180 stateful_change: How to modify the stateful partition. Values are: |
116 'old': Don't modify stateful partition. Just update normally. | 181 'old': Don't modify stateful partition. Just update normally. |
117 'clean': Uses clobber-state to wipe the stateful partition with the | 182 'clean': Uses clobber-state to wipe the stateful partition with the |
118 exception of code needed for ssh. | 183 exception of code needed for ssh. |
119 proxy_port: Port to have the client connect to. For use with | 184 proxy_port: Port to have the client connect to. For use with |
120 CrosTestProxy. | 185 CrosTestProxy. |
121 Raises an UpdateException if _UpdateImage returns an error. | 186 Raises an UpdateException if _UpdateImage returns an error. |
122 """ | 187 """ |
123 try: | 188 try: |
124 if not self.use_delta_updates: | 189 if not self.use_delta_updates: src_image_path = '' |
125 src_image_path = '' | 190 if private_key_path: |
| 191 key_to_use = private_key_path |
| 192 else: |
| 193 key_to_use = self.private_key |
126 | 194 |
127 self._UpdateImage(image_path, src_image_path, stateful_change, proxy_port) | 195 self._UpdateImage(image_path, src_image_path, stateful_change, proxy_port, |
| 196 key_to_use) |
128 except UpdateException as err: | 197 except UpdateException as err: |
129 # If the update fails, print it out | 198 # If the update fails, print it out |
130 Warning(err.stdout) | 199 Warning(err.stdout) |
131 raise | 200 raise |
132 | 201 |
133 def AttemptUpdateWithPayloadExpectedFailure(self, payload, expected_msg): | 202 def AttemptUpdateWithPayloadExpectedFailure(self, payload, expected_msg): |
134 """Attempt a payload update, expect it to fail with expected log""" | 203 """Attempt a payload update, expect it to fail with expected log""" |
135 try: | 204 try: |
136 self._UpdateUsingPayload(payload) | 205 self._UpdateUsingPayload(payload) |
137 except UpdateException as err: | 206 except UpdateException as err: |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
171 def ProcessOptions(cls, parser, options): | 240 def ProcessOptions(cls, parser, options): |
172 """Processes options. | 241 """Processes options. |
173 | 242 |
174 Static method that should be called from main. Subclasses should also | 243 Static method that should be called from main. Subclasses should also |
175 call their parent method if they override it. | 244 call their parent method if they override it. |
176 """ | 245 """ |
177 cls.verbose = options.verbose | 246 cls.verbose = options.verbose |
178 cls.base_image_path = options.base_image | 247 cls.base_image_path = options.base_image |
179 cls.target_image_path = options.target_image | 248 cls.target_image_path = options.target_image |
180 cls.use_delta_updates = options.delta | 249 cls.use_delta_updates = options.delta |
| 250 cls.board = options.board |
| 251 cls.private_key = options.private_key |
181 if options.quick_test: | 252 if options.quick_test: |
182 cls.verify_suite = 'build_RootFilesystemSize' | 253 cls.verify_suite = 'build_RootFilesystemSize' |
183 else: | 254 else: |
184 cls.verify_suite = 'suite_Smoke' | 255 cls.verify_suite = 'suite_Smoke' |
185 | 256 |
186 # Sanity checks. | 257 # Sanity checks. |
187 if not cls.base_image_path: | 258 if not cls.base_image_path: |
188 parser.error('Need path to base image for vm.') | 259 parser.error('Need path to base image for vm.') |
189 elif not os.path.exists(cls.base_image_path): | 260 elif not os.path.exists(cls.base_image_path): |
190 Die('%s does not exist' % cls.base_image_path) | 261 Die('%s does not exist' % cls.base_image_path) |
191 | 262 |
192 if not cls.target_image_path: | 263 if not cls.target_image_path: |
193 parser.error('Need path to target image to update with.') | 264 parser.error('Need path to target image to update with.') |
194 elif not os.path.exists(cls.target_image_path): | 265 elif not os.path.exists(cls.target_image_path): |
195 Die('%s does not exist' % cls.target_image_path) | 266 Die('%s does not exist' % cls.target_image_path) |
196 | 267 |
197 def PrepareBase(self, image_path): | 268 def PrepareBase(self, image_path): |
198 """Prepares target with base_image_path.""" | 269 """Prepares target with base_image_path.""" |
199 pass | 270 pass |
200 | 271 |
201 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', | 272 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', |
202 proxy_port=None): | 273 proxy_port=None, private_key_path=None): |
203 """Implementation of an actual update. | 274 """Implementation of an actual update. |
204 | 275 |
205 See PerformUpdate for description of args. Subclasses must override this | 276 See PerformUpdate for description of args. Subclasses must override this |
206 method with the correct update procedure for the class. | 277 method with the correct update procedure for the class. |
207 """ | 278 """ |
208 pass | 279 pass |
209 | 280 |
210 def _UpdateUsingPayload(self, update_path, stateful_change='old', | 281 def _UpdateUsingPayload(self, update_path, stateful_change='old', |
211 proxy_port=None): | 282 proxy_port=None): |
212 """Updates target with the pre-generated update stored in update_path. | 283 """Updates target with the pre-generated update stored in update_path. |
(...skipping 190 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
403 def ProcessOptions(cls, parser, options): | 474 def ProcessOptions(cls, parser, options): |
404 """Processes non-vm-specific options.""" | 475 """Processes non-vm-specific options.""" |
405 AUTest.ProcessOptions(parser, options) | 476 AUTest.ProcessOptions(parser, options) |
406 cls.remote = options.remote | 477 cls.remote = options.remote |
407 | 478 |
408 if not cls.remote: | 479 if not cls.remote: |
409 parser.error('We require a remote address for real tests.') | 480 parser.error('We require a remote address for real tests.') |
410 | 481 |
411 def PrepareBase(self, image_path): | 482 def PrepareBase(self, image_path): |
412 """Auto-update to base image to prepare for test.""" | 483 """Auto-update to base image to prepare for test.""" |
413 self.PerformUpdate(image_path) | 484 _PrepareRealBase(image_path) |
414 | 485 |
415 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', | 486 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', |
416 proxy_port=None): | 487 proxy_port=None, private_key_path=None): |
417 """Updates a remote image using image_to_live.sh.""" | 488 """Updates a remote image using image_to_live.sh.""" |
418 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | 489 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) |
419 cmd = ['%s/image_to_live.sh' % self.crosutils, | 490 cmd = ['%s/image_to_live.sh' % self.crosutils, |
420 '--image=%s' % image_path, | |
421 '--remote=%s' % self.remote, | 491 '--remote=%s' % self.remote, |
422 stateful_change_flag, | 492 stateful_change_flag, |
423 '--verify', | 493 '--verify', |
424 '--src_image=%s' % src_image_path | |
425 ] | 494 ] |
426 | 495 self.AppendUpdateFlags(cmd, image_path, src_image_path, proxy_port, |
427 if proxy_port: | 496 private_key_path) |
428 cmd.append('--proxy_port=%s' % proxy_port) | 497 self.RunUpdateCmd(cmd) |
429 | |
430 if self.verbose: | |
431 try: | |
432 RunCommand(cmd) | |
433 except Exception, e: | |
434 raise UpdateException(1, e.message) | |
435 else: | |
436 (code, stdout, stderr) = RunCommandCaptureOutput(cmd) | |
437 if code != 0: | |
438 raise UpdateException(code, stdout) | |
439 | 498 |
440 def _UpdateUsingPayload(self, update_path, stateful_change='old', | 499 def _UpdateUsingPayload(self, update_path, stateful_change='old', |
441 proxy_port=None): | 500 proxy_port=None): |
442 """Updates a remote image using image_to_live.sh.""" | 501 """Updates a remote image using image_to_live.sh.""" |
443 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | 502 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) |
444 cmd = ['%s/image_to_live.sh' % self.crosutils, | 503 cmd = ['%s/image_to_live.sh' % self.crosutils, |
445 '--payload=%s' % update_path, | 504 '--payload=%s' % update_path, |
446 '--remote=%s' % self.remote, | 505 '--remote=%s' % self.remote, |
447 stateful_change_flag, | 506 stateful_change_flag, |
448 '--verify', | 507 '--verify', |
449 ] | 508 ] |
450 | 509 if proxy_port: cmd.append('--proxy_port=%s' % proxy_port) |
451 if proxy_port: | 510 self.RunUpdateCmd(cmd) |
452 cmd.append('--proxy_port=%s' % proxy_port) | |
453 | |
454 if self.verbose: | |
455 try: | |
456 RunCommand(cmd) | |
457 except Exception, e: | |
458 raise UpdateException(1, e.message) | |
459 else: | |
460 (code, stdout, stderr) = RunCommandCaptureOutput(cmd) | |
461 if code != 0: | |
462 raise UpdateException(code, stdout) | |
463 | 511 |
464 def VerifyImage(self, percent_required_to_pass): | 512 def VerifyImage(self, percent_required_to_pass): |
465 """Verifies an image using run_remote_tests.sh with verification suite.""" | 513 """Verifies an image using run_remote_tests.sh with verification suite.""" |
466 output = RunCommand([ | 514 output = RunCommand([ |
467 '%s/run_remote_tests.sh' % self.crosutils, | 515 '%s/run_remote_tests.sh' % self.crosutils, |
468 '--remote=%s' % self.remote, | 516 '--remote=%s' % self.remote, |
469 self.verify_suite, | 517 self.verify_suite, |
470 ], error_ok=True, enter_chroot=False, redirect_stdout=True) | 518 ], error_ok=True, enter_chroot=False, redirect_stdout=True) |
471 return self.AssertEnoughTestsPassed(self, output, percent_required_to_pass) | 519 return self.AssertEnoughTestsPassed(self, output, percent_required_to_pass) |
472 | 520 |
473 | 521 |
474 class VirtualAUTest(unittest.TestCase, AUTest): | 522 class VirtualAUTest(unittest.TestCase, AUTest): |
475 """Test harness for updating virtual machines.""" | 523 """Test harness for updating virtual machines.""" |
476 | 524 |
477 # VM Constants. | |
478 _FULL_VDISK_SIZE = 6072 | |
479 _FULL_STATEFULFS_SIZE = 3074 | |
480 | |
481 # Class variables used to acquire individual VM variables per test. | 525 # Class variables used to acquire individual VM variables per test. |
482 _vm_lock = threading.Lock() | 526 _vm_lock = threading.Lock() |
483 _next_port = 9222 | 527 _next_port = 9222 |
484 | 528 |
485 def _KillExistingVM(self, pid_file): | 529 def _KillExistingVM(self, pid_file): |
486 if os.path.exists(pid_file): | 530 if os.path.exists(pid_file): |
487 Warning('Existing %s found. Deleting and killing process' % | 531 Warning('Existing %s found. Deleting and killing process' % |
488 pid_file) | 532 pid_file) |
489 RunCommand(['./cros_stop_vm', '--kvm_pid=%s' % pid_file], | 533 RunCommand(['./cros_stop_vm', '--kvm_pid=%s' % pid_file], |
490 cwd=self.crosutilsbin) | 534 cwd=self.crosutilsbin) |
491 | 535 |
492 assert not os.path.exists(pid_file) | 536 assert not os.path.exists(pid_file) |
493 | 537 |
494 def _AcquireUniquePortAndPidFile(self): | 538 def _AcquireUniquePortAndPidFile(self): |
495 """Acquires unique ssh port and pid file for VM.""" | 539 """Acquires unique ssh port and pid file for VM.""" |
496 with VirtualAUTest._vm_lock: | 540 with VirtualAUTest._vm_lock: |
497 self._ssh_port = VirtualAUTest._next_port | 541 self._ssh_port = VirtualAUTest._next_port |
498 self._kvm_pid_file = '/tmp/kvm.%d' % self._ssh_port | 542 self._kvm_pid_file = '/tmp/kvm.%d' % self._ssh_port |
499 VirtualAUTest._next_port += 1 | 543 VirtualAUTest._next_port += 1 |
500 | 544 |
501 def setUp(self): | 545 def setUp(self): |
502 """Unit test overriden method. Is called before every test.""" | 546 """Unit test overriden method. Is called before every test.""" |
503 AUTest.setUp(self) | 547 AUTest.setUp(self) |
504 self.vm_image_path = None | |
505 self._AcquireUniquePortAndPidFile() | 548 self._AcquireUniquePortAndPidFile() |
506 self._KillExistingVM(self._kvm_pid_file) | 549 self._KillExistingVM(self._kvm_pid_file) |
507 | 550 |
508 def tearDown(self): | 551 def tearDown(self): |
509 self._KillExistingVM(self._kvm_pid_file) | 552 self._KillExistingVM(self._kvm_pid_file) |
510 | 553 |
511 @classmethod | 554 @classmethod |
512 def ProcessOptions(cls, parser, options): | 555 def ProcessOptions(cls, parser, options): |
513 """Processes vm-specific options.""" | 556 """Processes vm-specific options.""" |
514 AUTest.ProcessOptions(parser, options) | 557 AUTest.ProcessOptions(parser, options) |
515 cls.board = options.board | |
516 | 558 |
517 # Communicate flags to tests. | 559 # Communicate flags to tests. |
518 cls.graphics_flag = '' | 560 cls.graphics_flag = '' |
519 if options.no_graphics: cls.graphics_flag = '--no_graphics' | 561 if options.no_graphics: cls.graphics_flag = '--no_graphics' |
520 | 562 if not cls.board: parser.error('Need board to convert base image to vm.') |
521 if not cls.board: | |
522 parser.error('Need board to convert base image to vm.') | |
523 | 563 |
524 def PrepareBase(self, image_path): | 564 def PrepareBase(self, image_path): |
525 """Creates an update-able VM based on base image.""" | 565 """Creates an update-able VM based on base image.""" |
526 # Needed for VM delta updates. We need to use the qemu image rather | 566 self._PrepareVMBase(image_path) |
527 # than the base image on a first update. By tracking the first_update | |
528 # we can set src_image to the qemu form of the base image when | |
529 # performing generating the delta payload. | |
530 self._first_update = True | |
531 self.vm_image_path = '%s/chromiumos_qemu_image.bin' % os.path.dirname( | |
532 image_path) | |
533 if not os.path.exists(self.vm_image_path): | |
534 Info('Creating %s' % vm_image_path) | |
535 RunCommand(['%s/image_to_vm.sh' % self.crosutils, | |
536 '--full', | |
537 '--from=%s' % ReinterpretPathForChroot( | |
538 os.path.dirname(image_path)), | |
539 '--vdisk_size=%s' % self._FULL_VDISK_SIZE, | |
540 '--statefulfs_size=%s' % self._FULL_STATEFULFS_SIZE, | |
541 '--board=%s' % self.board, | |
542 '--test_image'], enter_chroot=True) | |
543 | |
544 Info('Using %s as base' % self.vm_image_path) | |
545 self.assertTrue(os.path.exists(self.vm_image_path)) | |
546 | 567 |
547 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', | 568 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', |
548 proxy_port=''): | 569 proxy_port='', private_key_path=None): |
549 """Updates VM image with image_path.""" | 570 """Updates VM image with image_path.""" |
550 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | 571 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) |
551 if src_image_path and self._first_update: | 572 if src_image_path and self._first_update: |
552 src_image_path = self.vm_image_path | 573 src_image_path = self.vm_image_path |
553 self._first_update = False | 574 self._first_update = False |
554 | 575 |
555 # Check image payload cache first. | 576 cmd = ['%s/cros_run_vm_update' % self.crosutilsbin, |
556 update_id = _GenerateUpdateId(target=image_path, src=src_image_path) | 577 '--vm_image_path=%s' % self.vm_image_path, |
557 cache_path = dev_server_cache[update_id] | 578 '--snapshot', |
558 if cache_path: | 579 self.graphics_flag, |
559 Info('Using cache %s' % cache_path) | 580 '--persist', |
560 update_url = DevServerWrapper.GetDevServerURL(proxy_port, cache_path) | 581 '--kvm_pid=%s' % self._kvm_pid_file, |
561 cmd = ['%s/cros_run_vm_update' % self.crosutilsbin, | 582 '--ssh_port=%s' % self._ssh_port, |
562 '--vm_image_path=%s' % self.vm_image_path, | 583 stateful_change_flag, |
563 '--snapshot', | 584 ] |
564 self.graphics_flag, | |
565 '--persist', | |
566 '--kvm_pid=%s' % self._kvm_pid_file, | |
567 '--ssh_port=%s' % self._ssh_port, | |
568 stateful_change_flag, | |
569 '--update_url=%s' % update_url, | |
570 ] | |
571 else: | |
572 cmd = ['%s/cros_run_vm_update' % self.crosutilsbin, | |
573 '--update_image_path=%s' % image_path, | |
574 '--vm_image_path=%s' % self.vm_image_path, | |
575 '--snapshot', | |
576 self.graphics_flag, | |
577 '--persist', | |
578 '--kvm_pid=%s' % self._kvm_pid_file, | |
579 '--ssh_port=%s' % self._ssh_port, | |
580 stateful_change_flag, | |
581 '--src_image=%s' % src_image_path, | |
582 '--proxy_port=%s' % proxy_port | |
583 ] | |
584 | 585 |
585 if self.verbose: | 586 self.AppendUpdateFlags(cmd, image_path, src_image_path, proxy_port, |
586 try: | 587 private_key_path) |
587 RunCommand(cmd) | 588 self.RunUpdateCmd(cmd) |
588 except Exception, e: | |
589 raise UpdateException(1, e.message) | |
590 else: | |
591 (code, stdout, stderr) = RunCommandCaptureOutput(cmd) | |
592 if code != 0: | |
593 raise UpdateException(code, stdout) | |
594 | 589 |
595 def _UpdateUsingPayload(self, update_path, stateful_change='old', | 590 def _UpdateUsingPayload(self, update_path, stateful_change='old', |
596 proxy_port=None): | 591 proxy_port=None): |
597 """Updates a vm image using cros_run_vm_update.""" | 592 """Updates a vm image using cros_run_vm_update.""" |
598 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | 593 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) |
599 cmd = ['%s/cros_run_vm_update' % self.crosutilsbin, | 594 cmd = ['%s/cros_run_vm_update' % self.crosutilsbin, |
600 '--payload=%s' % update_path, | 595 '--payload=%s' % update_path, |
601 '--vm_image_path=%s' % self.vm_image_path, | 596 '--vm_image_path=%s' % self.vm_image_path, |
602 '--snapshot', | 597 '--snapshot', |
603 self.graphics_flag, | 598 self.graphics_flag, |
604 '--persist', | 599 '--persist', |
605 '--kvm_pid=%s' % self._kvm_pid_file, | 600 '--kvm_pid=%s' % self._kvm_pid_file, |
606 '--ssh_port=%s' % self._ssh_port, | 601 '--ssh_port=%s' % self._ssh_port, |
607 stateful_change_flag, | 602 stateful_change_flag, |
608 ] | 603 ] |
609 | 604 if proxy_port: cmd.append('--proxy_port=%s' % proxy_port) |
610 if proxy_port: | 605 self.RunUpdateCmd(cmd) |
611 cmd.append('--proxy_port=%s' % proxy_port) | |
612 | |
613 if self.verbose: | |
614 try: | |
615 RunCommand(cmd) | |
616 except Exception, e: | |
617 raise UpdateException(1, e.message) | |
618 else: | |
619 (code, stdout, stderr) = RunCommandCaptureOutput(cmd) | |
620 if code != 0: | |
621 raise UpdateException(code, stdout) | |
622 | 606 |
623 def VerifyImage(self, percent_required_to_pass): | 607 def VerifyImage(self, percent_required_to_pass): |
624 """Runs vm smoke suite to verify image.""" | 608 """Runs vm smoke suite to verify image.""" |
625 # image_to_live already verifies lsb-release matching. This is just | 609 # image_to_live already verifies lsb-release matching. This is just |
626 # for additional steps. | 610 # for additional steps. |
627 | 611 |
628 commandWithArgs = ['%s/cros_run_vm_test' % self.crosutilsbin, | 612 commandWithArgs = ['%s/cros_run_vm_test' % self.crosutilsbin, |
629 '--image_path=%s' % self.vm_image_path, | 613 '--image_path=%s' % self.vm_image_path, |
630 '--snapshot', | 614 '--snapshot', |
631 '--persist', | 615 '--persist', |
632 '--kvm_pid=%s' % self._kvm_pid_file, | 616 '--kvm_pid=%s' % self._kvm_pid_file, |
633 '--ssh_port=%s' % self._ssh_port, | 617 '--ssh_port=%s' % self._ssh_port, |
634 self.verify_suite, | 618 self.verify_suite, |
635 ] | 619 ] |
636 | 620 |
637 if self.graphics_flag: | 621 if self.graphics_flag: |
638 commandWithArgs.append(self.graphics_flag) | 622 commandWithArgs.append(self.graphics_flag) |
639 | 623 |
640 output = RunCommand(commandWithArgs, error_ok=True, enter_chroot=False, | 624 output = RunCommand(commandWithArgs, error_ok=True, enter_chroot=False, |
641 redirect_stdout=True) | 625 redirect_stdout=True) |
642 return self.AssertEnoughTestsPassed(self, output, percent_required_to_pass) | 626 return self.AssertEnoughTestsPassed(self, output, percent_required_to_pass) |
643 | 627 |
644 | 628 |
645 class GenerateVirtualAUDeltasTest(VirtualAUTest): | 629 class PregenerateAUDeltas(unittest.TestCase, AUTest): |
646 """Class the overrides VirtualAUTest and stores deltas we will generate.""" | 630 """Magical class that emulates an AUTest to store deltas we will generate. |
| 631 |
| 632 This class emulates an AUTest such that when it runs as a TestCase it runs |
| 633 through the exact up |
| 634 """ |
647 delta_list = {} | 635 delta_list = {} |
648 | 636 |
649 def setUp(self): | 637 def setUp(self): |
650 AUTest.setUp(self) | 638 AUTest.setUp(self) |
651 | 639 |
652 def tearDown(self): | 640 def tearDown(self): |
653 pass | 641 pass |
654 | 642 |
| 643 @classmethod |
| 644 def ProcessOptions(cls, parser, options): |
| 645 AUTest.ProcessOptions(parser, options) |
| 646 cls.au_type = options.type |
| 647 |
| 648 def PrepareBase(self, image_path): |
| 649 if self.au_type == 'vm': |
| 650 self._PrepareVMBase(image_path) |
| 651 else: |
| 652 self._PrepareRealBase(image_path) |
| 653 |
655 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', | 654 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', |
656 proxy_port=None): | 655 proxy_port=None, private_key_path=None): |
657 if src_image_path and self._first_update: | 656 if self.au_type == 'vm' and src_image_path and self._first_update: |
658 src_image_path = self.vm_image_path | 657 src_image_path = self.vm_image_path |
659 self._first_update = False | 658 self._first_update = False |
660 | 659 |
| 660 # Generate a value that combines delta with private key path. |
| 661 val = '%s+%s' % (src_image_path, private_key_path) |
661 if not self.delta_list.has_key(image_path): | 662 if not self.delta_list.has_key(image_path): |
662 self.delta_list[image_path] = set([src_image_path]) | 663 self.delta_list[image_path] = set([val]) |
663 else: | 664 else: |
664 self.delta_list[image_path].add(src_image_path) | 665 self.delta_list[image_path].add(val) |
665 | 666 |
666 def AttemptUpdateWithPayloadExpectedFailure(self, payload, expected_msg): | 667 def AttemptUpdateWithPayloadExpectedFailure(self, payload, expected_msg): |
667 pass | 668 pass |
668 | 669 |
669 def VerifyImage(self, percent_required_to_pass): | 670 def VerifyImage(self, percent_required_to_pass): |
670 pass | 671 pass |
671 | 672 |
672 | 673 |
673 class ParallelJob(threading.Thread): | 674 class ParallelJob(threading.Thread): |
674 """Small wrapper for threading. Thread that releases a semaphores on exit.""" | 675 """Small wrapper for threading. Thread that releases a semaphores on exit.""" |
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
749 def GetDevServerURL(cls, port, sub_dir): | 750 def GetDevServerURL(cls, port, sub_dir): |
750 """Returns the dev server url for a given port and sub directory.""" | 751 """Returns the dev server url for a given port and sub directory.""" |
751 ip_addr = GetIPAddress() | 752 ip_addr = GetIPAddress() |
752 if not port: port = 8080 | 753 if not port: port = 8080 |
753 url = 'http://%(ip)s:%(port)s/%(dir)s' % {'ip': ip_addr, | 754 url = 'http://%(ip)s:%(port)s/%(dir)s' % {'ip': ip_addr, |
754 'port': str(port), | 755 'port': str(port), |
755 'dir': sub_dir} | 756 'dir': sub_dir} |
756 return url | 757 return url |
757 | 758 |
758 | 759 |
759 def _GenerateUpdateId(target, src): | 760 def _GenerateUpdateId(target, src, key): |
760 """Returns a simple representation id of target and src paths.""" | 761 """Returns a simple representation id of target and src paths.""" |
761 if src: | 762 update_id = target |
762 return '%s->%s' % (target, src) | 763 if src: update_id = '->'.join([update_id, src]) |
763 else: | 764 if key: update_id = '+'.join([update_id, key]) |
764 return target | 765 return update_id |
765 | 766 |
766 | 767 |
767 def _RunParallelJobs(number_of_sumultaneous_jobs, jobs, jobs_args, print_status)
: | 768 def _RunParallelJobs(number_of_sumultaneous_jobs, jobs, jobs_args, |
| 769 print_status): |
768 | 770 |
769 """Runs set number of specified jobs in parallel. | 771 """Runs set number of specified jobs in parallel. |
770 | 772 |
771 Args: | 773 Args: |
772 number_of_simultaneous_jobs: Max number of threads to be run in parallel. | 774 number_of_simultaneous_jobs: Max number of threads to be run in parallel. |
773 jobs: Array of methods to run. | 775 jobs: Array of methods to run. |
774 jobs_args: Array of args associated with method calls. | 776 jobs_args: Array of args associated with method calls. |
775 print_status: True if you'd like this to print out .'s as it runs jobs. | 777 print_status: True if you'd like this to print out .'s as it runs jobs. |
776 Returns: | 778 Returns: |
777 Returns an array of results corresponding to each thread. | 779 Returns an array of results corresponding to each thread. |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
827 This method effectively pre-generates the dev server cache for all tests. | 829 This method effectively pre-generates the dev server cache for all tests. |
828 | 830 |
829 Args: | 831 Args: |
830 parser: parser from main. | 832 parser: parser from main. |
831 options: options from parsed parser. | 833 options: options from parsed parser. |
832 Returns: | 834 Returns: |
833 Dictionary of Update Identifiers->Relative cache locations. | 835 Dictionary of Update Identifiers->Relative cache locations. |
834 Raises: | 836 Raises: |
835 UpdateException if we fail to generate an update. | 837 UpdateException if we fail to generate an update. |
836 """ | 838 """ |
837 def _GenerateVMUpdate(target, src): | 839 def _GenerateVMUpdate(target, src, private_key_path): |
838 """Generates an update using the devserver.""" | 840 """Generates an update using the devserver.""" |
839 target = ReinterpretPathForChroot(target) | 841 command = ['./enter_chroot.sh', |
840 if src: | 842 '--nogit_config', |
841 src = ReinterpretPathForChroot(src) | 843 '--', |
| 844 'sudo', |
| 845 './start_devserver', |
| 846 '--pregenerate_update', |
| 847 '--exit', |
| 848 ] |
| 849 # Add actual args to command. |
| 850 command.append('--image=%s' % ReinterpretPathForChroot(target)) |
| 851 if src: command.append('--src_image=%s' % ReinterpretPathForChroot(src)) |
| 852 if options.type == 'vm': command.append('--for_vm') |
| 853 if private_key_path: |
| 854 command.append('--private_key=%s' % |
| 855 ReinterpretPathForChroot(private_key_path)) |
842 | 856 |
843 return RunCommandCaptureOutput(['./enter_chroot.sh', | 857 return RunCommandCaptureOutput(command, combine_stdout_stderr=True, |
844 '--nogit_config', | 858 print_cmd=True) |
845 '--', | |
846 'sudo', | |
847 './start_devserver', | |
848 '--pregenerate_update', | |
849 '--exit', | |
850 '--image=%s' % target, | |
851 '--src_image=%s' % src, | |
852 '--for_vm', | |
853 ], combine_stdout_stderr=True, | |
854 print_cmd=False) | |
855 | 859 |
856 # Get the list of deltas by mocking out update method in test class. | 860 # Get the list of deltas by mocking out update method in test class. |
857 test_suite = _PrepareTestSuite(parser, options, GenerateVirtualAUDeltasTest) | 861 test_suite = _PrepareTestSuite(parser, options, PregenerateAUDeltas) |
858 test_result = unittest.TextTestRunner(verbosity=0).run(test_suite) | 862 test_result = unittest.TextTestRunner(verbosity=0).run(test_suite) |
| 863 if not test_result.wasSuccessful(): |
| 864 raise UpdateException(1, 'Error finding updates to generate.') |
859 | 865 |
860 Info('The following delta updates are required.') | 866 Info('The following delta updates are required.') |
861 update_ids = [] | 867 update_ids = [] |
862 jobs = [] | 868 jobs = [] |
863 args = [] | 869 args = [] |
864 for target, srcs in GenerateVirtualAUDeltasTest.delta_list.items(): | 870 for target, srcs in PregenerateAUDeltas.delta_list.items(): |
865 for src in srcs: | 871 for src_key in srcs: |
866 update_id = _GenerateUpdateId(target=target, src=src) | 872 (src, key) = src_key.split('+') |
| 873 # TODO(sosa): Add private key as part of caching name once devserver can |
| 874 # handle it its own cache. |
| 875 update_id = _GenerateUpdateId(target=target, src=src, key=key) |
867 print >> sys.stderr, 'AU: %s' % update_id | 876 print >> sys.stderr, 'AU: %s' % update_id |
868 update_ids.append(update_id) | 877 update_ids.append(update_id) |
869 jobs.append(_GenerateVMUpdate) | 878 jobs.append(_GenerateVMUpdate) |
870 args.append((target, src)) | 879 args.append((target, src, key)) |
871 | 880 |
872 raw_results = _RunParallelJobs(options.jobs, jobs, args, print_status=True) | 881 raw_results = _RunParallelJobs(options.jobs, jobs, args, print_status=True) |
873 results = [] | 882 results = [] |
874 | 883 |
875 # Looking for this line in the output. | 884 # Looking for this line in the output. |
876 key_line_re = re.compile('^PREGENERATED_UPDATE=([\w/.]+)') | 885 key_line_re = re.compile('^PREGENERATED_UPDATE=([\w/.]+)') |
877 for result in raw_results: | 886 for result in raw_results: |
878 (return_code, output, _) = result | 887 (return_code, output, _) = result |
879 if return_code != 0: | 888 if return_code != 0: |
880 Warning(output) | 889 Warning(output) |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
912 test_case = unittest.TestLoader().loadTestsFromName(test_name) | 921 test_case = unittest.TestLoader().loadTestsFromName(test_name) |
913 threads.append(unittest.TextTestRunner().run) | 922 threads.append(unittest.TextTestRunner().run) |
914 args.append(test_case) | 923 args.append(test_case) |
915 | 924 |
916 results = _RunParallelJobs(options.jobs, threads, args, print_status=False) | 925 results = _RunParallelJobs(options.jobs, threads, args, print_status=False) |
917 for test_result in results: | 926 for test_result in results: |
918 if not test_result.wasSuccessful(): | 927 if not test_result.wasSuccessful(): |
919 Die('Test harness was not successful') | 928 Die('Test harness was not successful') |
920 | 929 |
921 | 930 |
| 931 def _InsertPrivateKeyIntoImage(image_path, private_key_path): |
| 932 from_dir = os.path.dirname(image_path) |
| 933 image = os.path.basename(image_path) |
| 934 crosutils_dir = os.path.abspath(__file__).rsplit('/', 2)[0] |
| 935 target_key_path = 'usr/share/update_engine/update-payload-key.pub.pem' |
| 936 |
| 937 # Temporary directories for this function. |
| 938 rootfs_dir = tempfile.mkdtemp(suffix='rootfs', prefix='tmp') |
| 939 stateful_dir = tempfile.mkdtemp(suffix='stateful', prefix='tmp') |
| 940 |
| 941 Info('Copying %s into %s' % (private_key_path, image_path)) |
| 942 try: |
| 943 RunCommand(['%s/mount_gpt_image.sh' % crosutils_dir, |
| 944 '--from=%s' % from_dir, |
| 945 '--image=%s' % image, |
| 946 '--rootfs_mountpt=%s' % rootfs_dir, |
| 947 '--stateful_mountpt=%s' % stateful_dir, |
| 948 ], print_cmd=False, redirect_stdout=True, |
| 949 redirect_stderr=True) |
| 950 path = os.path.join(rootfs_dir, target_key_path) |
| 951 dir_path = os.path.dirname(path) |
| 952 RunCommand(['sudo', 'mkdir', '--parents', dir_path], print_cmd=False) |
| 953 RunCommand(['sudo', 'cp', '--force', '-p', private_key_path, path], |
| 954 print_cmd=False) |
| 955 finally: |
| 956 # Unmount best effort regardless. |
| 957 RunCommand(['%s/mount_gpt_image.sh' % crosutils_dir, |
| 958 '--unmount', |
| 959 '--rootfs_mountpt=%s' % rootfs_dir, |
| 960 '--stateful_mountpt=%s' % stateful_dir, |
| 961 ], print_cmd=False, redirect_stdout=True, redirect_stderr=True) |
| 962 # Clean up our directories. |
| 963 os.rmdir(rootfs_dir) |
| 964 os.rmdir(stateful_dir) |
| 965 |
| 966 |
922 def main(): | 967 def main(): |
923 parser = optparse.OptionParser() | 968 parser = optparse.OptionParser() |
924 parser.add_option('-b', '--base_image', | 969 parser.add_option('-b', '--base_image', |
925 help='path to the base image.') | 970 help='path to the base image.') |
926 parser.add_option('-r', '--board', | 971 parser.add_option('-r', '--board', |
927 help='board for the images.') | 972 help='board for the images.') |
928 parser.add_option('--no_delta', action='store_false', default=True, | 973 parser.add_option('--no_delta', action='store_false', default=True, |
929 dest='delta', | 974 dest='delta', |
930 help='Disable using delta updates.') | 975 help='Disable using delta updates.') |
931 parser.add_option('--no_graphics', action='store_true', | 976 parser.add_option('--no_graphics', action='store_true', |
932 help='Disable graphics for the vm test.') | 977 help='Disable graphics for the vm test.') |
933 parser.add_option('-j', '--jobs', default=8, type=int, | 978 parser.add_option('-j', '--jobs', default=8, type=int, |
934 help='Number of simultaneous jobs') | 979 help='Number of simultaneous jobs') |
| 980 parser.add_option('--public_key', default=None, |
| 981 help='Public key to use on images and updates.') |
| 982 parser.add_option('--private_key', default=None, |
| 983 help='Private key to use on images and updates.') |
935 parser.add_option('-q', '--quick_test', default=False, action='store_true', | 984 parser.add_option('-q', '--quick_test', default=False, action='store_true', |
936 help='Use a basic test to verify image.') | 985 help='Use a basic test to verify image.') |
937 parser.add_option('-m', '--remote', | 986 parser.add_option('-m', '--remote', |
938 help='Remote address for real test.') | 987 help='Remote address for real test.') |
939 parser.add_option('-t', '--target_image', | 988 parser.add_option('-t', '--target_image', |
940 help='path to the target image.') | 989 help='path to the target image.') |
941 parser.add_option('--test_prefix', default='test', | 990 parser.add_option('--test_prefix', default='test', |
942 help='Only runs tests with specific prefix i.e. ' | 991 help='Only runs tests with specific prefix i.e. ' |
943 'testFullUpdateWipeStateful.') | 992 'testFullUpdateWipeStateful.') |
944 parser.add_option('-p', '--type', default='vm', | 993 parser.add_option('-p', '--type', default='vm', |
945 help='type of test to run: [vm, real]. Default: vm.') | 994 help='type of test to run: [vm, real]. Default: vm.') |
946 parser.add_option('--verbose', default=True, action='store_true', | 995 parser.add_option('--verbose', default=True, action='store_true', |
947 help='Print out rather than capture output as much as ' | 996 help='Print out rather than capture output as much as ' |
948 'possible.') | 997 'possible.') |
949 (options, leftover_args) = parser.parse_args() | 998 (options, leftover_args) = parser.parse_args() |
950 | 999 |
951 if leftover_args: | 1000 if leftover_args: |
952 parser.error('Found extra options we do not support: %s' % leftover_args) | 1001 parser.error('Found extra options we do not support: %s' % leftover_args) |
953 | 1002 |
| 1003 assert options.target_image and os.path.exists(options.target_image), \ |
| 1004 'Target image path does not exist' |
| 1005 if not options.base_image: |
| 1006 Warning('Base image not specified. Using target image as base image.') |
| 1007 options.base_image = options.target_image |
| 1008 |
954 # Figure out the test_class. | 1009 # Figure out the test_class. |
955 if options.type == 'vm': test_class = VirtualAUTest | 1010 if options.type == 'vm': test_class = VirtualAUTest |
956 elif options.type == 'real': test_class = RealAUTest | 1011 elif options.type == 'real': test_class = RealAUTest |
957 else: parser.error('Could not parse harness type %s.' % options.type) | 1012 else: parser.error('Could not parse harness type %s.' % options.type) |
958 | 1013 |
| 1014 if options.private_key or options.public_key: |
| 1015 assert os.path.exists(options.private_key), 'Could not find private key.' |
| 1016 assert os.path.exists(options.public_key), 'Could not find public key.' |
| 1017 _InsertPrivateKeyIntoImage(options.target_image, options.public_key) |
| 1018 _InsertPrivateKeyIntoImage(options.base_image, options.public_key) |
| 1019 |
959 # TODO(sosa): Caching doesn't really make sense on non-vm images (yet). | 1020 # TODO(sosa): Caching doesn't really make sense on non-vm images (yet). |
960 global dev_server_cache | 1021 global dev_server_cache |
961 if options.type == 'vm' and options.jobs > 1: | 1022 if options.type == 'vm' and options.jobs > 1: |
962 dev_server_cache = _PregenerateUpdates(parser, options) | 1023 dev_server_cache = _PregenerateUpdates(parser, options) |
963 my_server = DevServerWrapper() | 1024 my_server = DevServerWrapper() |
964 my_server.start() | 1025 my_server.start() |
965 try: | 1026 try: |
966 _RunTestsInParallel(parser, options, test_class) | 1027 _RunTestsInParallel(parser, options, test_class) |
967 finally: | 1028 finally: |
968 my_server.Stop() | 1029 my_server.Stop() |
969 | 1030 |
970 else: | 1031 else: |
971 dev_server_cache = None | 1032 dev_server_cache = None |
972 test_suite = _PrepareTestSuite(parser, options, test_class) | 1033 test_suite = _PrepareTestSuite(parser, options, test_class) |
973 test_result = unittest.TextTestRunner(verbosity=2).run(test_suite) | 1034 test_result = unittest.TextTestRunner(verbosity=2).run(test_suite) |
974 if not test_result.wasSuccessful(): | 1035 if not test_result.wasSuccessful(): |
975 Die('Test harness was not successful.') | 1036 Die('Test harness was not successful.') |
976 | 1037 |
977 | 1038 |
978 if __name__ == '__main__': | 1039 if __name__ == '__main__': |
979 main() | 1040 main() |
OLD | NEW |