OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 | 2 |
3 # Copyright (c) 2011 The Chromium OS Authors. All rights reserved. | 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 | 4 # Use of this source code is governed by a BSD-style license that can be |
5 # found in the LICENSE file. | 5 # found in the LICENSE file. |
6 | 6 |
7 """This module runs a suite of Auto Update tests. | 7 """This module runs a suite of Auto Update tests. |
8 | 8 |
9 The tests can be run on either a virtual machine or actual device depending | 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. | 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 | 11 Verbose is useful for many of the tests if you want to see individual commands |
12 being run during the update process. | 12 being run during the update process. |
13 """ | 13 """ |
14 | 14 |
15 import optparse | 15 import optparse |
16 import os | 16 import os |
17 import re | 17 import re |
18 import sys | 18 import sys |
19 import thread | 19 import threading |
20 import time | 20 import time |
21 import unittest | 21 import unittest |
22 import urllib | 22 import urllib |
23 | 23 |
24 sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) | 24 sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) |
25 from cros_build_lib import Die | 25 from cros_build_lib import Die |
26 from cros_build_lib import Info | 26 from cros_build_lib import Info |
27 from cros_build_lib import ReinterpretPathForChroot | 27 from cros_build_lib import ReinterpretPathForChroot |
28 from cros_build_lib import RunCommand | 28 from cros_build_lib import RunCommand |
29 from cros_build_lib import RunCommandCaptureOutput | 29 from cros_build_lib import RunCommandCaptureOutput |
(...skipping 97 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
127 raise | 127 raise |
128 | 128 |
129 def AttemptUpdateWithPayloadExpectedFailure(self, payload, expected_msg): | 129 def AttemptUpdateWithPayloadExpectedFailure(self, payload, expected_msg): |
130 """Attempt a payload update, expect it to fail with expected log""" | 130 """Attempt a payload update, expect it to fail with expected log""" |
131 try: | 131 try: |
132 self._UpdateUsingPayload(payload) | 132 self._UpdateUsingPayload(payload) |
133 except UpdateException as err: | 133 except UpdateException as err: |
134 # Will raise ValueError if expected is not found. | 134 # Will raise ValueError if expected is not found. |
135 if re.search(re.escape(expected_msg), err.stdout, re.MULTILINE): | 135 if re.search(re.escape(expected_msg), err.stdout, re.MULTILINE): |
136 return | 136 return |
| 137 else: |
| 138 Warning("Didn't find '%s' in:" % expected_msg) |
| 139 Warning(err.stdout) |
137 | 140 |
138 Warning("Didn't find '%s' in:" % expected_msg) | |
139 Warning(err.stdout) | |
140 self.fail('We managed to update when failure was expected') | 141 self.fail('We managed to update when failure was expected') |
141 | 142 |
142 def AttemptUpdateWithFilter(self, filter, proxy_port=8081): | 143 def AttemptUpdateWithFilter(self, filter, proxy_port=8081): |
143 """Update through a proxy, with a specified filter, and expect success.""" | 144 """Update through a proxy, with a specified filter, and expect success.""" |
144 | 145 |
145 self.PrepareBase(self.target_image_path) | 146 self.PrepareBase(self.target_image_path) |
146 | 147 |
147 # The devserver runs at port 8080 by default. We assume that here, and | 148 # The devserver runs at port 8080 by default. We assume that here, and |
148 # start our proxy at a different. We then tell our update tools to | 149 # start our proxy at a different. We then tell our update tools to |
149 # have the client connect to our proxy_port instead of 8080. | 150 # have the client connect to our proxy_port instead of 8080. |
150 proxy = cros_test_proxy.CrosTestProxy(port_in=proxy_port, | 151 proxy = cros_test_proxy.CrosTestProxy(port_in=proxy_port, |
151 address_out='127.0.0.1', | 152 address_out='127.0.0.1', |
152 port_out=8080, | 153 port_out=8080, |
153 filter=filter) | 154 filter=filter) |
154 proxy.serve_forever_in_thread() | 155 proxy.serve_forever_in_thread() |
155 | 156 |
156 # This update is expected to fail... | 157 # This update is expected to fail... |
157 try: | 158 try: |
158 self.PerformUpdate(self.target_image_path, self.base_image_path, | 159 self.PerformUpdate(self.target_image_path, self.target_image_path, |
159 proxy_port=proxy_port) | 160 proxy_port=proxy_port) |
160 finally: | 161 finally: |
161 proxy.shutdown() | 162 proxy.shutdown() |
162 | 163 |
163 # -------- Functions that subclasses should override --------- | 164 # -------- Functions that subclasses should override --------- |
164 | 165 |
165 @classmethod | 166 @classmethod |
166 def ProcessOptions(cls, parser, options): | 167 def ProcessOptions(cls, parser, options): |
167 """Processes options. | 168 """Processes options. |
168 | 169 |
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
241 rather than wiping it. | 242 rather than wiping it. |
242 """ | 243 """ |
243 # Just make sure some tests pass on original image. Some old images | 244 # Just make sure some tests pass on original image. Some old images |
244 # don't pass many tests. | 245 # don't pass many tests. |
245 self.PrepareBase(self.base_image_path) | 246 self.PrepareBase(self.base_image_path) |
246 # TODO(sosa): move to 100% once we start testing using the autotest paired | 247 # TODO(sosa): move to 100% once we start testing using the autotest paired |
247 # with the dev channel. | 248 # with the dev channel. |
248 percent_passed = self.VerifyImage(10) | 249 percent_passed = self.VerifyImage(10) |
249 | 250 |
250 # Update to - all tests should pass on new image. | 251 # Update to - all tests should pass on new image. |
251 Info('Updating from base image on vm to target image.') | |
252 self.PerformUpdate(self.target_image_path, self.base_image_path) | 252 self.PerformUpdate(self.target_image_path, self.base_image_path) |
253 self.VerifyImage(100) | 253 self.VerifyImage(100) |
254 | 254 |
255 # Update from - same percentage should pass that originally passed. | 255 # Update from - same percentage should pass that originally passed. |
256 Info('Updating from updated image on vm back to base image.') | |
257 self.PerformUpdate(self.base_image_path, self.target_image_path) | 256 self.PerformUpdate(self.base_image_path, self.target_image_path) |
258 self.VerifyImage(percent_passed) | 257 self.VerifyImage(percent_passed) |
259 | 258 |
260 def testUpdateWipeStateful(self): | 259 def testUpdateWipeStateful(self): |
261 """Tests if we can update after cleaning the stateful partition. | 260 """Tests if we can update after cleaning the stateful partition. |
262 | 261 |
263 This test checks that we can update successfully after wiping the | 262 This test checks that we can update successfully after wiping the |
264 stateful partition. | 263 stateful partition. |
265 """ | 264 """ |
266 # Just make sure some tests pass on original image. Some old images | 265 # Just make sure some tests pass on original image. Some old images |
267 # don't pass many tests. | 266 # don't pass many tests. |
268 self.PrepareBase(self.base_image_path) | 267 self.PrepareBase(self.base_image_path) |
269 # TODO(sosa): move to 100% once we start testing using the autotest paired | 268 # TODO(sosa): move to 100% once we start testing using the autotest paired |
270 # with the dev channel. | 269 # with the dev channel. |
271 percent_passed = self.VerifyImage(10) | 270 percent_passed = self.VerifyImage(10) |
272 | 271 |
273 # Update to - all tests should pass on new image. | 272 # Update to - all tests should pass on new image. |
274 Info('Updating from base image on vm to target image and wiping stateful.') | |
275 self.PerformUpdate(self.target_image_path, self.base_image_path, 'clean') | 273 self.PerformUpdate(self.target_image_path, self.base_image_path, 'clean') |
276 self.VerifyImage(100) | 274 self.VerifyImage(100) |
277 | 275 |
278 # Update from - same percentage should pass that originally passed. | 276 # Update from - same percentage should pass that originally passed. |
279 Info('Updating from updated image back to base image and wiping stateful.') | |
280 self.PerformUpdate(self.base_image_path, self.target_image_path, 'clean') | 277 self.PerformUpdate(self.base_image_path, self.target_image_path, 'clean') |
281 self.VerifyImage(percent_passed) | 278 self.VerifyImage(percent_passed) |
282 | 279 |
283 # TODO(sosa): Get test to work with verbose. | 280 # TODO(sosa): Get test to work with verbose. |
284 def NotestPartialUpdate(self): | 281 def NotestPartialUpdate(self): |
285 """Tests what happens if we attempt to update with a truncated payload.""" | 282 """Tests what happens if we attempt to update with a truncated payload.""" |
286 # Preload with the version we are trying to test. | 283 # Preload with the version we are trying to test. |
287 self.PrepareBase(self.target_image_path) | 284 self.PrepareBase(self.target_image_path) |
288 | 285 |
289 # Image can be updated at: | 286 # Image can be updated at: |
(...skipping 211 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
501 | 498 |
502 # Communicate flags to tests. | 499 # Communicate flags to tests. |
503 cls.graphics_flag = '' | 500 cls.graphics_flag = '' |
504 if options.no_graphics: cls.graphics_flag = '--no_graphics' | 501 if options.no_graphics: cls.graphics_flag = '--no_graphics' |
505 | 502 |
506 if not cls.board: | 503 if not cls.board: |
507 parser.error('Need board to convert base image to vm.') | 504 parser.error('Need board to convert base image to vm.') |
508 | 505 |
509 def PrepareBase(self, image_path): | 506 def PrepareBase(self, image_path): |
510 """Creates an update-able VM based on base image.""" | 507 """Creates an update-able VM based on base image.""" |
| 508 # Needed for VM delta updates. We need to use the qemu image rather |
| 509 # than the base image on a first update. By tracking the first_update |
| 510 # we can set src_image to the qemu form of the base image when |
| 511 # performing generating the delta payload. |
| 512 self._first_update = True |
511 self.vm_image_path = '%s/chromiumos_qemu_image.bin' % os.path.dirname( | 513 self.vm_image_path = '%s/chromiumos_qemu_image.bin' % os.path.dirname( |
512 image_path) | 514 image_path) |
513 | |
514 Info('Creating: %s' % self.vm_image_path) | |
515 | |
516 if not os.path.exists(self.vm_image_path): | 515 if not os.path.exists(self.vm_image_path): |
517 Info('Qemu image %s not found, creating one.' % self.vm_image_path) | 516 Info('Creating %s' % vm_image_path) |
518 RunCommand(['%s/image_to_vm.sh' % self.crosutils, | 517 RunCommand(['%s/image_to_vm.sh' % self.crosutils, |
519 '--full', | 518 '--full', |
520 '--from=%s' % ReinterpretPathForChroot( | 519 '--from=%s' % ReinterpretPathForChroot( |
521 os.path.dirname(image_path)), | 520 os.path.dirname(image_path)), |
522 '--vdisk_size=%s' % self._FULL_VDISK_SIZE, | 521 '--vdisk_size=%s' % self._FULL_VDISK_SIZE, |
523 '--statefulfs_size=%s' % self._FULL_STATEFULFS_SIZE, | 522 '--statefulfs_size=%s' % self._FULL_STATEFULFS_SIZE, |
524 '--board=%s' % self.board, | 523 '--board=%s' % self.board, |
525 '--test_image'], enter_chroot=True) | 524 '--test_image'], enter_chroot=True) |
526 else: | |
527 Info('Using existing VM image %s' % self.vm_image_path) | |
528 | 525 |
529 Info('Testing for %s' % self.vm_image_path) | 526 Info('Using %s as base' % self.vm_image_path) |
530 self.assertTrue(os.path.exists(self.vm_image_path)) | 527 self.assertTrue(os.path.exists(self.vm_image_path)) |
531 | 528 |
532 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', | 529 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', |
533 proxy_port=None): | 530 proxy_port=None): |
534 """Updates VM image with image_path.""" | 531 """Updates VM image with image_path.""" |
535 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) | 532 stateful_change_flag = self.GetStatefulChangeFlag(stateful_change) |
536 if src_image_path == self.base_image_path: | 533 if src_image_path and self._first_update: |
537 src_image_path = self.vm_image_path | 534 src_image_path = self.vm_image_path |
| 535 self._first_update = False |
538 | 536 |
539 cmd = ['%s/cros_run_vm_update' % self.crosutilsbin, | 537 cmd = ['%s/cros_run_vm_update' % self.crosutilsbin, |
540 '--update_image_path=%s' % image_path, | 538 '--update_image_path=%s' % image_path, |
541 '--vm_image_path=%s' % self.vm_image_path, | 539 '--vm_image_path=%s' % self.vm_image_path, |
542 '--snapshot', | 540 '--snapshot', |
543 self.graphics_flag, | 541 self.graphics_flag, |
544 '--persist', | 542 '--persist', |
545 '--kvm_pid=%s' % self._KVM_PID_FILE, | 543 '--kvm_pid=%s' % self._KVM_PID_FILE, |
546 stateful_change_flag, | 544 stateful_change_flag, |
547 '--src_image=%s' % src_image_path, | 545 '--src_image=%s' % src_image_path, |
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
601 ] | 599 ] |
602 | 600 |
603 if self.graphics_flag: | 601 if self.graphics_flag: |
604 commandWithArgs.append(self.graphics_flag) | 602 commandWithArgs.append(self.graphics_flag) |
605 | 603 |
606 output = RunCommand(commandWithArgs, error_ok=True, enter_chroot=False, | 604 output = RunCommand(commandWithArgs, error_ok=True, enter_chroot=False, |
607 redirect_stdout=True) | 605 redirect_stdout=True) |
608 return self.AssertEnoughTestsPassed(self, output, percent_required_to_pass) | 606 return self.AssertEnoughTestsPassed(self, output, percent_required_to_pass) |
609 | 607 |
610 | 608 |
| 609 class GenerateVirtualAUDeltasTest(VirtualAUTest): |
| 610 """Class the overrides VirtualAUTest and stores deltas we will generate.""" |
| 611 delta_list = {} |
| 612 |
| 613 def setUp(self): |
| 614 AUTest.setUp(self) |
| 615 |
| 616 def _UpdateImage(self, image_path, src_image_path='', stateful_change='old', |
| 617 proxy_port=None): |
| 618 if src_image_path and self._first_update: |
| 619 src_image_path = self.vm_image_path |
| 620 self._first_update = False |
| 621 |
| 622 image_path = ReinterpretPathForChroot(image_path) |
| 623 if src_image_path: |
| 624 src_image_path = ReinterpretPathForChroot(src_image_path) |
| 625 if not self.delta_list.has_key(image_path): |
| 626 self.delta_list[image_path] = set([src_image_path]) |
| 627 else: |
| 628 self.delta_list[image_path].add(src_image_path) |
| 629 |
| 630 def AttemptUpdateWithPayloadExpectedFailure(self, payload, expected_msg): |
| 631 pass |
| 632 |
| 633 def VerifyImage(self, percent_required_to_pass): |
| 634 pass |
| 635 |
| 636 |
| 637 class ParallelJob(threading.Thread): |
| 638 """Small wrapper for threading.Thread that releases a semaphore on exit.""" |
| 639 def __init__(self, semaphore, target, args): |
| 640 threading.Thread.__init__(self, target=target, args=args) |
| 641 self._target = target |
| 642 self._args = args |
| 643 self._semaphore = semaphore |
| 644 self._output = None |
| 645 self._completed = False |
| 646 |
| 647 def run(self): |
| 648 try: |
| 649 threading.Thread.run(self) |
| 650 finally: |
| 651 self._Cleanup() |
| 652 self._completed = True |
| 653 |
| 654 def GetOutput(self): |
| 655 assert self._completed, 'GetOutput called before thread was run.' |
| 656 return self._output |
| 657 |
| 658 def _Cleanup(self): |
| 659 self._semaphore.release() |
| 660 |
| 661 def __str__(self): |
| 662 return '%s(%s)' % (self._target, self._args) |
| 663 |
| 664 |
| 665 def _RunParallelJobs(number_of_sumultaneous_jobs, jobs, jobs_args): |
| 666 """Runs set number of specified jobs in parallel. |
| 667 |
| 668 Args: |
| 669 number_of_simultaneous_jobs: Max number of threads to be run in parallel. |
| 670 jobs: Array of methods to run. |
| 671 jobs_args: Array of args associated with method calls. |
| 672 Returns: |
| 673 Returns an array of results corresponding to each thread. |
| 674 """ |
| 675 def _TwoTupleize(x, y): |
| 676 return (x, y) |
| 677 |
| 678 threads = [] |
| 679 job_pool_semaphore = threading.Semaphore(number_of_sumultaneous_jobs) |
| 680 assert len(jobs) == len(jobs_args), 'Length of args array is wrong.' |
| 681 |
| 682 # Create the parallel jobs. |
| 683 for job, args in map(_TwoTupleize, jobs, jobs_args): |
| 684 thread = ParallelJob(job_pool_semaphore, target=job, args=args) |
| 685 threads.append(thread) |
| 686 |
| 687 # We use a semaphore to ensure we don't run more jobs that required. |
| 688 # After each thread finishes, it releases (increments semaphore). |
| 689 # Acquire blocks of num jobs reached and continues when a thread finishes. |
| 690 for next_thread in threads: |
| 691 job_pool_semaphore.acquire(blocking=True) |
| 692 Info('Starting %s' % next_thread) |
| 693 next_thread.start() |
| 694 |
| 695 # Wait on the rest of the threads to finish. |
| 696 for thread in threads: |
| 697 thread.join() |
| 698 |
| 699 return [thread.GetOutput() for thread in threads] |
| 700 |
| 701 |
| 702 def _PregenerateUpdates(parser, options): |
| 703 """Determines all deltas that will be generated and generates them. |
| 704 |
| 705 This method effectively pre-generates the dev server cache for all tests. |
| 706 |
| 707 Args: |
| 708 parser: parser from main. |
| 709 options: options from parsed parser. |
| 710 Returns: |
| 711 Array of output from generating updates. |
| 712 """ |
| 713 def _GenerateVMUpdate(target, src): |
| 714 """Generates an update using the devserver.""" |
| 715 RunCommand(['./start_devserver', |
| 716 '--pregenerate_update', |
| 717 '--exit', |
| 718 '--image=%s' % target, |
| 719 '--src_image=%s' % src, |
| 720 '--for_vm' |
| 721 ]) |
| 722 |
| 723 # Get the list of deltas by mocking out update method in test class. |
| 724 GenerateVirtualAUDeltasTest.ProcessOptions(parser, options) |
| 725 test_loader = unittest.TestLoader() |
| 726 test_loader.testMethodPrefix = options.test_prefix |
| 727 test_suite = test_loader.loadTestsFromTestCase(GenerateVirtualAUDeltasTest) |
| 728 test_result = unittest.TextTestRunner(verbosity=0).run(test_suite) |
| 729 |
| 730 Info('The following delta updates are required.') |
| 731 jobs = [] |
| 732 args = [] |
| 733 for target, srcs in GenerateVirtualAUDeltasTest.delta_list.items(): |
| 734 for src in srcs: |
| 735 if src: |
| 736 print >> sys.stderr, 'DELTA AU %s -> %s' % (src, target) |
| 737 else: |
| 738 print >> sys.stderr, 'FULL AU %s' % target |
| 739 |
| 740 jobs.append(_GenerateVMUpdate) |
| 741 args.append((target, src)) |
| 742 |
| 743 results = _RunParallelJobs(options.jobs, jobs, args) |
| 744 return results |
| 745 |
| 746 |
611 def main(): | 747 def main(): |
612 parser = optparse.OptionParser() | 748 parser = optparse.OptionParser() |
613 parser.add_option('-b', '--base_image', | 749 parser.add_option('-b', '--base_image', |
614 help='path to the base image.') | 750 help='path to the base image.') |
615 parser.add_option('-r', '--board', | 751 parser.add_option('-r', '--board', |
616 help='board for the images.') | 752 help='board for the images.') |
617 parser.add_option('--no_delta', action='store_false', default=True, | 753 parser.add_option('--no_delta', action='store_false', default=True, |
618 dest='delta', | 754 dest='delta', |
619 help='Disable using delta updates.') | 755 help='Disable using delta updates.') |
620 parser.add_option('--no_graphics', action='store_true', | 756 parser.add_option('--no_graphics', action='store_true', |
621 help='Disable graphics for the vm test.') | 757 help='Disable graphics for the vm test.') |
| 758 parser.add_option('-j', '--jobs', default=8, type=int, |
| 759 help='Number of simultaneous jobs') |
| 760 parser.add_option('-q', '--quick_test', default=False, action='store_true', |
| 761 help='Use a basic test to verify image.') |
622 parser.add_option('-m', '--remote', | 762 parser.add_option('-m', '--remote', |
623 help='Remote address for real test.') | 763 help='Remote address for real test.') |
624 parser.add_option('-q', '--quick_test', default=False, action='store_true', | |
625 help='Use a basic test to verify image.') | |
626 parser.add_option('-t', '--target_image', | 764 parser.add_option('-t', '--target_image', |
627 help='path to the target image.') | 765 help='path to the target image.') |
628 parser.add_option('--test_prefix', default='test', | 766 parser.add_option('--test_prefix', default='test', |
629 help='Only runs tests with specific prefix i.e. ' | 767 help='Only runs tests with specific prefix i.e. ' |
630 'testFullUpdateWipeStateful.') | 768 'testFullUpdateWipeStateful.') |
631 parser.add_option('-p', '--type', default='vm', | 769 parser.add_option('-p', '--type', default='vm', |
632 help='type of test to run: [vm, real]. Default: vm.') | 770 help='type of test to run: [vm, real]. Default: vm.') |
633 parser.add_option('--verbose', default=True, action='store_true', | 771 parser.add_option('--verbose', default=True, action='store_true', |
634 help='Print out rather than capture output as much as ' | 772 help='Print out rather than capture output as much as ' |
635 'possible.') | 773 'possible.') |
636 (options, leftover_args) = parser.parse_args() | 774 (options, leftover_args) = parser.parse_args() |
637 | 775 |
638 if leftover_args: | 776 if leftover_args: |
639 parser.error('Found extra options we do not support: %s' % leftover_args) | 777 parser.error('Found extra options we do not support: %s' % leftover_args) |
640 | 778 |
641 if options.type == 'vm': test_class = VirtualAUTest | 779 if options.type == 'vm': test_class = VirtualAUTest |
642 elif options.type == 'real': test_class = RealAUTest | 780 elif options.type == 'real': test_class = RealAUTest |
643 else: parser.error('Could not parse harness type %s.' % options.type) | 781 else: parser.error('Could not parse harness type %s.' % options.type) |
644 | 782 |
| 783 # TODO(sosa): Caching doesn't really make sense on non-vm images (yet). |
| 784 if options.type == 'vm': |
| 785 _PregenerateUpdates(parser, options) |
| 786 |
| 787 # Run the test suite. |
645 test_class.ProcessOptions(parser, options) | 788 test_class.ProcessOptions(parser, options) |
646 | |
647 test_loader = unittest.TestLoader() | 789 test_loader = unittest.TestLoader() |
648 test_loader.testMethodPrefix = options.test_prefix | 790 test_loader.testMethodPrefix = options.test_prefix |
649 test_suite = test_loader.loadTestsFromTestCase(test_class) | 791 test_suite = test_loader.loadTestsFromTestCase(test_class) |
650 test_result = unittest.TextTestRunner(verbosity=2).run(test_suite) | 792 test_result = unittest.TextTestRunner(verbosity=2).run(test_suite) |
651 | |
652 if not test_result.wasSuccessful(): | 793 if not test_result.wasSuccessful(): |
653 Die('Test harness was not successful') | 794 Die('Test harness was not successful.') |
654 | 795 |
655 | 796 |
656 if __name__ == '__main__': | 797 if __name__ == '__main__': |
657 main() | 798 main() |
OLD | NEW |