Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(4)

Side by Side Diff: bin/cros_au_test_harness.py

Issue 6597122: Refactor au_test_harness into modules and refactor to use worker design. (Closed) Base URL: http://git.chromium.org/git/crosutils.git@master
Patch Set: 80 char Created 9 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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()
OLDNEW
« bin/au_test_harness/vm_au_worker.py ('K') | « bin/cros_au_test_harness ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698