| OLD | NEW |
| (Empty) |
| 1 # Copyright (c) 2011 The Chromium OS Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 """Module that contains the interface for au_test_harness workers. | |
| 6 | |
| 7 An au test harnss worker is a class that contains the logic for performing | |
| 8 and validating updates on a target. This should be subclassed to handle | |
| 9 various types of target. Types of targets include VM's, real devices, etc. | |
| 10 """ | |
| 11 | |
| 12 import inspect | |
| 13 import threading | |
| 14 import os | |
| 15 import sys | |
| 16 | |
| 17 import cros_build_lib as cros_lib | |
| 18 | |
| 19 import dev_server_wrapper | |
| 20 import update_exception | |
| 21 | |
| 22 | |
| 23 class AUWorker(object): | |
| 24 """Interface for a worker that updates and verifies images.""" | |
| 25 # Mapping between cached payloads to directory locations. | |
| 26 update_cache = None | |
| 27 | |
| 28 # --- INTERFACE --- | |
| 29 | |
| 30 def __init__(self, options, test_results_root): | |
| 31 """Processes options for the specific-type of worker.""" | |
| 32 self.board = options.board | |
| 33 self.private_key = options.private_key | |
| 34 self.test_results_root = test_results_root | |
| 35 self.use_delta_updates = options.delta | |
| 36 self.verbose = options.verbose | |
| 37 self.vm_image_path = None | |
| 38 if options.quick_test: | |
| 39 self.verify_suite = 'build_RootFilesystemSize' | |
| 40 else: | |
| 41 self.verify_suite = 'suite_Smoke' | |
| 42 | |
| 43 # Set these up as they are used often. | |
| 44 self.crosutils = os.path.join(os.path.dirname(__file__), '..', '..') | |
| 45 self.crosutilsbin = os.path.join(os.path.dirname(__file__), '..') | |
| 46 | |
| 47 def CleanUp(self): | |
| 48 """Called at the end of every test.""" | |
| 49 pass | |
| 50 | |
| 51 def UpdateImage(self, image_path, src_image_path='', stateful_change='old', | |
| 52 proxy_port=None, private_key_path=None): | |
| 53 """Implementation of an actual update. | |
| 54 | |
| 55 See PerformUpdate for description of args. Subclasses must override this | |
| 56 method with the correct update procedure for the class. | |
| 57 """ | |
| 58 pass | |
| 59 | |
| 60 def UpdateUsingPayload(self, update_path, stateful_change='old', | |
| 61 proxy_port=None): | |
| 62 """Updates target with the pre-generated update stored in update_path. | |
| 63 | |
| 64 Subclasses must override this method with the correct update procedure for | |
| 65 the class. | |
| 66 | |
| 67 Args: | |
| 68 update_path: Path to the image to update with. This directory should | |
| 69 contain both update.gz, and stateful.image.gz | |
| 70 proxy_port: Port to have the client connect to. For use with | |
| 71 CrosTestProxy. | |
| 72 """ | |
| 73 pass | |
| 74 | |
| 75 def VerifyImage(self, unittest, percent_required_to_pass=100): | |
| 76 """Verifies the image with tests. | |
| 77 | |
| 78 Verifies that the test images passes the percent required. Subclasses must | |
| 79 override this method with the correct update procedure for the class. | |
| 80 | |
| 81 Args: | |
| 82 unittest: pointer to a unittest to fail if we cannot verify the image. | |
| 83 percent_required_to_pass: percentage required to pass. This should be | |
| 84 fall between 0-100. | |
| 85 | |
| 86 Returns: | |
| 87 Returns the percent that passed. | |
| 88 """ | |
| 89 pass | |
| 90 | |
| 91 # --- INTERFACE TO AU_TEST --- | |
| 92 | |
| 93 def PerformUpdate(self, image_path, src_image_path='', stateful_change='old', | |
| 94 proxy_port=None, private_key_path=None): | |
| 95 """Performs an update using _UpdateImage and reports any error. | |
| 96 | |
| 97 Subclasses should not override this method but override _UpdateImage | |
| 98 instead. | |
| 99 | |
| 100 Args: | |
| 101 image_path: Path to the image to update with. This image must be a test | |
| 102 image. | |
| 103 src_image_path: Optional. If set, perform a delta update using the | |
| 104 image specified by the path as the source image. | |
| 105 stateful_change: How to modify the stateful partition. Values are: | |
| 106 'old': Don't modify stateful partition. Just update normally. | |
| 107 'clean': Uses clobber-state to wipe the stateful partition with the | |
| 108 exception of code needed for ssh. | |
| 109 proxy_port: Port to have the client connect to. For use with | |
| 110 CrosTestProxy. | |
| 111 private_key_path: Path to a private key to use with update payload. | |
| 112 Raises an update_exception.UpdateException if _UpdateImage returns an error. | |
| 113 """ | |
| 114 try: | |
| 115 if not self.use_delta_updates: src_image_path = '' | |
| 116 if private_key_path: | |
| 117 key_to_use = private_key_path | |
| 118 else: | |
| 119 key_to_use = self.private_key | |
| 120 | |
| 121 self.UpdateImage(image_path, src_image_path, stateful_change, | |
| 122 proxy_port, key_to_use) | |
| 123 except update_exception.UpdateException as err: | |
| 124 # If the update fails, print it out | |
| 125 Warning(err.stdout) | |
| 126 raise | |
| 127 | |
| 128 @classmethod | |
| 129 def SetUpdateCache(cls, update_cache): | |
| 130 """Sets the global update cache for getting paths to devserver payloads.""" | |
| 131 cls.update_cache = update_cache | |
| 132 | |
| 133 # --- METHODS FOR SUB CLASS USE --- | |
| 134 | |
| 135 def PrepareRealBase(self, image_path): | |
| 136 """Prepares a remote device for worker test by updating it to the image.""" | |
| 137 self.UpdateImage(image_path) | |
| 138 | |
| 139 def PrepareVMBase(self, image_path): | |
| 140 """Prepares a VM image for worker test by creating the VM file from the img. | |
| 141 """ | |
| 142 # VM Constants. | |
| 143 FULL_VDISK_SIZE = 6072 | |
| 144 FULL_STATEFULFS_SIZE = 3074 | |
| 145 # Needed for VM delta updates. We need to use the qemu image rather | |
| 146 # than the base image on a first update. By tracking the first_update | |
| 147 # we can set src_image to the qemu form of the base image when | |
| 148 # performing generating the delta payload. | |
| 149 self._first_update = True | |
| 150 self.vm_image_path = '%s/chromiumos_qemu_image.bin' % os.path.dirname( | |
| 151 image_path) | |
| 152 if not os.path.exists(self.vm_image_path): | |
| 153 cros_lib.Info('Creating %s' % self.vm_image_path) | |
| 154 cros_lib.RunCommand(['./image_to_vm.sh', | |
| 155 '--full', | |
| 156 '--from=%s' % cros_lib.ReinterpretPathForChroot( | |
| 157 os.path.dirname(image_path)), | |
| 158 '--vdisk_size=%s' % FULL_VDISK_SIZE, | |
| 159 '--statefulfs_size=%s' % FULL_STATEFULFS_SIZE, | |
| 160 '--board=%s' % self.board, | |
| 161 '--test_image' | |
| 162 ], enter_chroot=True, cwd=self.crosutils) | |
| 163 | |
| 164 cros_lib.Info('Using %s as base' % self.vm_image_path) | |
| 165 assert os.path.exists(self.vm_image_path) | |
| 166 | |
| 167 def GetStatefulChangeFlag(self, stateful_change): | |
| 168 """Returns the flag to pass to image_to_vm for the stateful change.""" | |
| 169 stateful_change_flag = '' | |
| 170 if stateful_change: | |
| 171 stateful_change_flag = '--stateful_update_flag=%s' % stateful_change | |
| 172 | |
| 173 return stateful_change_flag | |
| 174 | |
| 175 def AppendUpdateFlags(self, cmd, image_path, src_image_path, proxy_port, | |
| 176 private_key_path): | |
| 177 """Appends common args to an update cmd defined by an array. | |
| 178 | |
| 179 Modifies cmd in places by appending appropriate items given args. | |
| 180 """ | |
| 181 if proxy_port: cmd.append('--proxy_port=%s' % proxy_port) | |
| 182 | |
| 183 # Get pregenerated update if we have one. | |
| 184 update_id = dev_server_wrapper.GenerateUpdateId(image_path, src_image_path, | |
| 185 private_key_path) | |
| 186 cache_path = self.update_cache[update_id] | |
| 187 if cache_path: | |
| 188 update_url = dev_server_wrapper.DevServerWrapper.GetDevServerURL( | |
| 189 proxy_port, cache_path) | |
| 190 cmd.append('--update_url=%s' % update_url) | |
| 191 else: | |
| 192 cmd.append('--image=%s' % image_path) | |
| 193 if src_image_path: cmd.append('--src_image=%s' % src_image_path) | |
| 194 | |
| 195 def RunUpdateCmd(self, cmd, log_directory=None): | |
| 196 """Runs the given update cmd given verbose options. | |
| 197 | |
| 198 Raises an update_exception.UpdateException if the update fails. | |
| 199 """ | |
| 200 if self.verbose: | |
| 201 try: | |
| 202 if log_directory: | |
| 203 cros_lib.RunCommand(cmd, log_to_file=os.path.join(log_directory, | |
| 204 'update.log')) | |
| 205 else: | |
| 206 cros_lib.RunCommand(cmd) | |
| 207 except Exception as e: | |
| 208 Warning(str(e)) | |
| 209 raise update_exception.UpdateException(1, str(e)) | |
| 210 else: | |
| 211 (code, stdout, stderr) = cros_lib.RunCommandCaptureOutput(cmd) | |
| 212 if code != 0: | |
| 213 Warning(stdout) | |
| 214 raise update_exception.UpdateException(code, stdout) | |
| 215 | |
| 216 def AssertEnoughTestsPassed(self, unittest, output, percent_required_to_pass): | |
| 217 """Helper function that asserts a sufficient number of tests passed. | |
| 218 | |
| 219 Args: | |
| 220 output: stdout from a test run. | |
| 221 percent_required_to_pass: percentage required to pass. This should be | |
| 222 fall between 0-100. | |
| 223 Returns: | |
| 224 percent that passed. | |
| 225 """ | |
| 226 cros_lib.Info('Output from VerifyImage():') | |
| 227 print >> sys.stderr, output | |
| 228 sys.stderr.flush() | |
| 229 percent_passed = self._ParseGenerateTestReportOutput(output) | |
| 230 cros_lib.Info('Percent passed: %d vs. Percent required: %d' % ( | |
| 231 percent_passed, percent_required_to_pass)) | |
| 232 unittest.assertTrue(percent_passed >= percent_required_to_pass) | |
| 233 return percent_passed | |
| 234 | |
| 235 def InitializeResultsDirectory(self): | |
| 236 """Called by a test to initialize a results directory for this worker.""" | |
| 237 # Use the name of the test. | |
| 238 test_name = inspect.stack()[1][3] | |
| 239 self.results_directory = os.path.join(self.test_results_root, test_name) | |
| 240 self.results_count = 0 | |
| 241 | |
| 242 def GetNextResultsPath(self, label): | |
| 243 """Returns a path for the results directory for this label. | |
| 244 | |
| 245 Prefixes directory returned for worker with time called i.e. 1_label, | |
| 246 2_label, etc. The directory returned is outside the chroot so if passing | |
| 247 to an script that is called with enther_chroot, make sure to use | |
| 248 ReinterpretPathForChroot. | |
| 249 """ | |
| 250 self.results_count += 1 | |
| 251 dir = os.path.join(self.results_directory, '%s_%s' % (self.results_count, | |
| 252 label)) | |
| 253 if not os.path.exists(dir): | |
| 254 os.makedirs(dir) | |
| 255 | |
| 256 return dir | |
| 257 | |
| 258 # --- PRIVATE HELPER FUNCTIONS --- | |
| 259 | |
| 260 def _ParseGenerateTestReportOutput(self, output): | |
| 261 """Returns the percentage of tests that passed based on output.""" | |
| 262 percent_passed = 0 | |
| 263 lines = output.split('\n') | |
| 264 | |
| 265 for line in lines: | |
| 266 if line.startswith("Total PASS:"): | |
| 267 # FORMAT: ^TOTAL PASS: num_passed/num_total (percent%)$ | |
| 268 percent_passed = line.split()[3].strip('()%') | |
| 269 cros_lib.Info('Percent of tests passed %s' % percent_passed) | |
| 270 break | |
| 271 | |
| 272 return int(percent_passed) | |
| OLD | NEW |