results_cache.py revision 72725f0149c91eda72e6734717f5c857f4659d65
1 2# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5"""Module to deal with result cache.""" 6 7import getpass 8import glob 9import hashlib 10import os 11import pickle 12import re 13import tempfile 14import json 15import sys 16 17from cros_utils import command_executer 18from cros_utils import misc 19 20from image_checksummer import ImageChecksummer 21 22import results_report 23import test_flag 24 25SCRATCH_DIR = os.path.expanduser('~/cros_scratch') 26RESULTS_FILE = 'results.txt' 27MACHINE_FILE = 'machine.txt' 28AUTOTEST_TARBALL = 'autotest.tbz2' 29PERF_RESULTS_FILE = 'perf-results.txt' 30CACHE_KEYS_FILE = 'cache_keys.txt' 31 32 33class Result(object): 34 """ This class manages what exactly is stored inside the cache without knowing 35 what the key of the cache is. For runs with perf, it stores perf.data, 36 perf.report, etc. The key generation is handled by the ResultsCache class. 37 """ 38 39 def __init__(self, logger, label, log_level, machine, cmd_exec=None): 40 self.chromeos_root = label.chromeos_root 41 self._logger = logger 42 self.ce = cmd_exec or command_executer.GetCommandExecuter( 43 self._logger, 44 log_level=log_level) 45 self.temp_dir = None 46 self.label = label 47 self.results_dir = None 48 self.log_level = log_level 49 self.machine = machine 50 self.perf_data_files = [] 51 self.perf_report_files = [] 52 self.chrome_version = '' 53 54 def CopyFilesTo(self, dest_dir, files_to_copy): 55 file_index = 0 56 for file_to_copy in files_to_copy: 57 if not os.path.isdir(dest_dir): 58 command = 'mkdir -p %s' % dest_dir 59 self.ce.RunCommand(command) 60 dest_file = os.path.join(dest_dir, 61 ('%s.%s' % (os.path.basename(file_to_copy), 62 file_index))) 63 ret = self.ce.CopyFiles(file_to_copy, dest_file, recursive=False) 64 if ret: 65 raise Exception('Could not copy results file: %s' % file_to_copy) 66 67 def CopyResultsTo(self, dest_dir): 68 self.CopyFilesTo(dest_dir, self.perf_data_files) 69 self.CopyFilesTo(dest_dir, self.perf_report_files) 70 if len(self.perf_data_files) or len(self.perf_report_files): 71 self._logger.LogOutput('Perf results files stored in %s.' % dest_dir) 72 73 def GetNewKeyvals(self, keyvals_dict): 74 # Initialize 'units' dictionary. 75 units_dict = {} 76 for k in keyvals_dict: 77 units_dict[k] = '' 78 results_files = self.GetDataMeasurementsFiles() 79 for f in results_files: 80 # Make sure we can find the results file 81 if os.path.exists(f): 82 data_filename = f 83 else: 84 # Otherwise get the base filename and create the correct 85 # path for it. 86 f_dir, f_base = misc.GetRoot(f) 87 data_filename = os.path.join(self.chromeos_root, 'chroot/tmp', 88 self.temp_dir, f_base) 89 if data_filename.find('.json') > 0: 90 raw_dict = dict() 91 if os.path.exists(data_filename): 92 with open(data_filename, 'r') as data_file: 93 raw_dict = json.load(data_file) 94 95 for k1 in raw_dict: 96 field_dict = raw_dict[k1] 97 for k2 in field_dict: 98 result_dict = field_dict[k2] 99 key = k1 + '__' + k2 100 keyvals_dict[key] = result_dict['value'] 101 units_dict[key] = result_dict['units'] 102 else: 103 if os.path.exists(data_filename): 104 with open(data_filename, 'r') as data_file: 105 lines = data_file.readlines() 106 for line in lines: 107 tmp_dict = json.loads(line) 108 graph_name = tmp_dict['graph'] 109 graph_str = (graph_name + '__') if graph_name else '' 110 key = graph_str + tmp_dict['description'] 111 keyvals_dict[key] = tmp_dict['value'] 112 units_dict[key] = tmp_dict['units'] 113 114 return keyvals_dict, units_dict 115 116 def AppendTelemetryUnits(self, keyvals_dict, units_dict): 117 """keyvals_dict is the dict of key-value used to generate Crosperf reports. 118 119 units_dict is a dictionary of the units for the return values in 120 keyvals_dict. We need to associate the units with the return values, 121 for Telemetry tests, so that we can include the units in the reports. 122 This function takes each value in keyvals_dict, finds the corresponding 123 unit in the units_dict, and replaces the old value with a list of the 124 old value and the units. This later gets properly parsed in the 125 ResultOrganizer class, for generating the reports. 126 127 """ 128 129 results_dict = {} 130 for k in keyvals_dict: 131 # We don't want these lines in our reports; they add no useful data. 132 if k == '' or k == 'telemetry_Crosperf': 133 continue 134 val = keyvals_dict[k] 135 units = units_dict[k] 136 new_val = [val, units] 137 results_dict[k] = new_val 138 return results_dict 139 140 def GetKeyvals(self, show_all): 141 results_in_chroot = os.path.join(self.chromeos_root, 'chroot', 'tmp') 142 if not self.temp_dir: 143 self.temp_dir = tempfile.mkdtemp(dir=results_in_chroot) 144 command = 'cp -r {0}/* {1}'.format(self.results_dir, self.temp_dir) 145 self.ce.RunCommand(command, print_to_console=False) 146 147 command = ('python generate_test_report --no-color --csv %s' % 148 (os.path.join('/tmp', os.path.basename(self.temp_dir)))) 149 _, out, _ = self.ce.ChrootRunCommandWOutput(self.chromeos_root, 150 command, 151 print_to_console=False) 152 keyvals_dict = {} 153 tmp_dir_in_chroot = misc.GetInsideChrootPath(self.chromeos_root, 154 self.temp_dir) 155 for line in out.splitlines(): 156 tokens = re.split('=|,', line) 157 key = tokens[-2] 158 if key.startswith(tmp_dir_in_chroot): 159 key = key[len(tmp_dir_in_chroot) + 1:] 160 value = tokens[-1] 161 keyvals_dict[key] = value 162 163 # Check to see if there is a perf_measurements file and get the 164 # data from it if so. 165 keyvals_dict, units_dict = self.GetNewKeyvals(keyvals_dict) 166 if self.suite == 'telemetry_Crosperf': 167 # For telemtry_Crosperf results, append the units to the return 168 # results, for use in generating the reports. 169 keyvals_dict = self.AppendTelemetryUnits(keyvals_dict, units_dict) 170 return keyvals_dict 171 172 def GetResultsDir(self): 173 mo = re.search(r'Results placed in (\S+)', self.out) 174 if mo: 175 result = mo.group(1) 176 return result 177 raise Exception('Could not find results directory.') 178 179 def FindFilesInResultsDir(self, find_args): 180 if not self.results_dir: 181 return None 182 183 command = 'find %s %s' % (self.results_dir, find_args) 184 ret, out, _ = self.ce.RunCommandWOutput(command, print_to_console=False) 185 if ret: 186 raise Exception('Could not run find command!') 187 return out 188 189 def GetPerfDataFiles(self): 190 return self.FindFilesInResultsDir('-name perf.data').splitlines() 191 192 def GetPerfReportFiles(self): 193 return self.FindFilesInResultsDir('-name perf.data.report').splitlines() 194 195 def GetDataMeasurementsFiles(self): 196 result = self.FindFilesInResultsDir('-name perf_measurements').splitlines() 197 if not result: 198 result = self.FindFilesInResultsDir('-name results-chart.json').splitlines() 199 return result 200 201 def GeneratePerfReportFiles(self): 202 perf_report_files = [] 203 for perf_data_file in self.perf_data_files: 204 # Generate a perf.report and store it side-by-side with the perf.data 205 # file. 206 chroot_perf_data_file = misc.GetInsideChrootPath(self.chromeos_root, 207 perf_data_file) 208 perf_report_file = '%s.report' % perf_data_file 209 if os.path.exists(perf_report_file): 210 raise Exception('Perf report file already exists: %s' % 211 perf_report_file) 212 chroot_perf_report_file = misc.GetInsideChrootPath(self.chromeos_root, 213 perf_report_file) 214 perf_path = os.path.join(self.chromeos_root, 'chroot', 'usr/bin/perf') 215 216 perf_file = '/usr/sbin/perf' 217 if os.path.exists(perf_path): 218 perf_file = '/usr/bin/perf' 219 220 # The following is a hack, to use the perf.static binary that 221 # was given to us by Stephane Eranian, until he can figure out 222 # why "normal" perf cannot properly symbolize ChromeOS perf.data files. 223 # Get the directory containing the 'crosperf' script. 224 dirname, _ = misc.GetRoot(sys.argv[0]) 225 perf_path = os.path.join(dirname, '..', 'perf.static') 226 if os.path.exists(perf_path): 227 # copy the executable into the chroot so that it can be found. 228 src_path = perf_path 229 dst_path = os.path.join(self.chromeos_root, 'chroot', 230 'tmp/perf.static') 231 command = 'cp %s %s' % (src_path, dst_path) 232 self.ce.RunCommand(command) 233 perf_file = '/tmp/perf.static' 234 235 command = ('%s report ' 236 '-n ' 237 '--symfs /build/%s ' 238 '--vmlinux /build/%s/usr/lib/debug/boot/vmlinux ' 239 '--kallsyms /build/%s/boot/System.map-* ' 240 '-i %s --stdio ' 241 '> %s' % (perf_file, self.board, self.board, self.board, 242 chroot_perf_data_file, chroot_perf_report_file)) 243 self.ce.ChrootRunCommand(self.chromeos_root, command) 244 245 # Add a keyval to the dictionary for the events captured. 246 perf_report_files.append(misc.GetOutsideChrootPath( 247 self.chromeos_root, chroot_perf_report_file)) 248 return perf_report_files 249 250 def GatherPerfResults(self): 251 report_id = 0 252 for perf_report_file in self.perf_report_files: 253 with open(perf_report_file, 'r') as f: 254 report_contents = f.read() 255 for group in re.findall(r'Events: (\S+) (\S+)', report_contents): 256 num_events = group[0] 257 event_name = group[1] 258 key = 'perf_%s_%s' % (report_id, event_name) 259 value = str(misc.UnitToNumber(num_events)) 260 self.keyvals[key] = value 261 262 def PopulateFromRun(self, out, err, retval, show_all, test, suite): 263 self.board = self.label.board 264 self.out = out 265 self.err = err 266 self.retval = retval 267 self.test_name = test 268 self.suite = suite 269 self.chroot_results_dir = self.GetResultsDir() 270 self.results_dir = misc.GetOutsideChrootPath(self.chromeos_root, 271 self.chroot_results_dir) 272 self.perf_data_files = self.GetPerfDataFiles() 273 # Include all perf.report data in table. 274 self.perf_report_files = self.GeneratePerfReportFiles() 275 # TODO(asharif): Do something similar with perf stat. 276 277 # Grab keyvals from the directory. 278 self.ProcessResults(show_all) 279 280 def ProcessResults(self, show_all): 281 # Note that this function doesn't know anything about whether there is a 282 # cache hit or miss. It should process results agnostic of the cache hit 283 # state. 284 self.keyvals = self.GetKeyvals(show_all) 285 self.keyvals['retval'] = self.retval 286 # Generate report from all perf.data files. 287 # Now parse all perf report files and include them in keyvals. 288 self.GatherPerfResults() 289 290 def GetChromeVersionFromCache(self, cache_dir): 291 # Read chrome_version from keys file, if present. 292 chrome_version = '' 293 keys_file = os.path.join(cache_dir, CACHE_KEYS_FILE) 294 if os.path.exists(keys_file): 295 with open(keys_file, 'r') as f: 296 lines = f.readlines() 297 for l in lines: 298 if l.find('Google Chrome ') == 0: 299 chrome_version = l 300 if chrome_version[-1] == '\n': 301 chrome_version = chrome_version[:-1] 302 break 303 return chrome_version 304 305 def PopulateFromCacheDir(self, cache_dir, show_all, test, suite): 306 self.test_name = test 307 self.suite = suite 308 # Read in everything from the cache directory. 309 with open(os.path.join(cache_dir, RESULTS_FILE), 'r') as f: 310 self.out = pickle.load(f) 311 self.err = pickle.load(f) 312 self.retval = pickle.load(f) 313 314 # Untar the tarball to a temporary directory 315 self.temp_dir = tempfile.mkdtemp( 316 dir=os.path.join(self.chromeos_root, 'chroot', 'tmp')) 317 318 command = ('cd %s && tar xf %s' % 319 (self.temp_dir, os.path.join(cache_dir, AUTOTEST_TARBALL))) 320 ret = self.ce.RunCommand(command, print_to_console=False) 321 if ret: 322 raise Exception('Could not untar cached tarball') 323 self.results_dir = self.temp_dir 324 self.perf_data_files = self.GetPerfDataFiles() 325 self.perf_report_files = self.GetPerfReportFiles() 326 self.chrome_version = self.GetChromeVersionFromCache(cache_dir) 327 self.ProcessResults(show_all) 328 329 def CleanUp(self, rm_chroot_tmp): 330 if rm_chroot_tmp and self.results_dir: 331 dirname, basename = misc.GetRoot(self.results_dir) 332 if basename.find('test_that_results_') != -1: 333 command = 'rm -rf %s' % self.results_dir 334 else: 335 command = 'rm -rf %s' % dirname 336 self.ce.RunCommand(command) 337 if self.temp_dir: 338 command = 'rm -rf %s' % self.temp_dir 339 self.ce.RunCommand(command) 340 341 def StoreToCacheDir(self, cache_dir, machine_manager, key_list): 342 # Create the dir if it doesn't exist. 343 temp_dir = tempfile.mkdtemp() 344 345 # Store to the temp directory. 346 with open(os.path.join(temp_dir, RESULTS_FILE), 'w') as f: 347 pickle.dump(self.out, f) 348 pickle.dump(self.err, f) 349 pickle.dump(self.retval, f) 350 351 if not test_flag.GetTestMode(): 352 with open(os.path.join(temp_dir, CACHE_KEYS_FILE), 'w') as f: 353 f.write('%s\n' % self.label.name) 354 f.write('%s\n' % self.label.chrome_version) 355 f.write('%s\n' % self.machine.checksum_string) 356 for k in key_list: 357 f.write(k) 358 f.write('\n') 359 360 if self.results_dir: 361 tarball = os.path.join(temp_dir, AUTOTEST_TARBALL) 362 command = ('cd %s && ' 363 'tar ' 364 '--exclude=var/spool ' 365 '--exclude=var/log ' 366 '-cjf %s .' % (self.results_dir, tarball)) 367 ret = self.ce.RunCommand(command) 368 if ret: 369 raise Exception("Couldn't store autotest output directory.") 370 # Store machine info. 371 # TODO(asharif): Make machine_manager a singleton, and don't pass it into 372 # this function. 373 with open(os.path.join(temp_dir, MACHINE_FILE), 'w') as f: 374 f.write(machine_manager.machine_checksum_string[self.label.name]) 375 376 if os.path.exists(cache_dir): 377 command = 'rm -rf {0}'.format(cache_dir) 378 self.ce.RunCommand(command) 379 380 command = 'mkdir -p {0} && '.format(os.path.dirname(cache_dir)) 381 command += 'chmod g+x {0} && '.format(temp_dir) 382 command += 'mv {0} {1}'.format(temp_dir, cache_dir) 383 ret = self.ce.RunCommand(command) 384 if ret: 385 command = 'rm -rf {0}'.format(temp_dir) 386 self.ce.RunCommand(command) 387 raise Exception('Could not move dir %s to dir %s' % (temp_dir, cache_dir)) 388 389 @classmethod 390 def CreateFromRun(cls, 391 logger, 392 log_level, 393 label, 394 machine, 395 out, 396 err, 397 retval, 398 show_all, 399 test, 400 suite='telemetry_Crosperf'): 401 if suite == 'telemetry': 402 result = TelemetryResult(logger, label, log_level, machine) 403 else: 404 result = cls(logger, label, log_level, machine) 405 result.PopulateFromRun(out, err, retval, show_all, test, suite) 406 return result 407 408 @classmethod 409 def CreateFromCacheHit(cls, 410 logger, 411 log_level, 412 label, 413 machine, 414 cache_dir, 415 show_all, 416 test, 417 suite='telemetry_Crosperf'): 418 if suite == 'telemetry': 419 result = TelemetryResult(logger, label, log_level, machine) 420 else: 421 result = cls(logger, label, log_level, machine) 422 try: 423 result.PopulateFromCacheDir(cache_dir, show_all, test, suite) 424 425 except Exception as e: 426 logger.LogError('Exception while using cache: %s' % e) 427 return None 428 return result 429 430 431class TelemetryResult(Result): 432 433 def __init__(self, logger, label, log_level, machine, cmd_exec=None): 434 super(TelemetryResult, self).__init__(logger, label, log_level, machine, 435 cmd_exec) 436 437 def PopulateFromRun(self, out, err, retval, show_all, test, suite): 438 self.out = out 439 self.err = err 440 self.retval = retval 441 442 self.ProcessResults() 443 444 def ProcessResults(self): 445 # The output is: 446 # url,average_commit_time (ms),... 447 # www.google.com,33.4,21.2,... 448 # We need to convert to this format: 449 # {"www.google.com:average_commit_time (ms)": "33.4", 450 # "www.google.com:...": "21.2"} 451 # Added note: Occasionally the output comes back 452 # with "JSON.stringify(window.automation.GetResults())" on 453 # the first line, and then the rest of the output as 454 # described above. 455 456 lines = self.out.splitlines() 457 self.keyvals = {} 458 459 if lines: 460 if lines[0].startswith('JSON.stringify'): 461 lines = lines[1:] 462 463 if not lines: 464 return 465 labels = lines[0].split(',') 466 for line in lines[1:]: 467 fields = line.split(',') 468 if len(fields) != len(labels): 469 continue 470 for i in range(1, len(labels)): 471 key = '%s %s' % (fields[0], labels[i]) 472 value = fields[i] 473 self.keyvals[key] = value 474 self.keyvals['retval'] = self.retval 475 476 def PopulateFromCacheDir(self, cache_dir): 477 with open(os.path.join(cache_dir, RESULTS_FILE), 'r') as f: 478 self.out = pickle.load(f) 479 self.err = pickle.load(f) 480 self.retval = pickle.load(f) 481 482 self.chrome_version = \ 483 super(TelemetryResult, self).GetChromeVersionFromCache(cache_dir) 484 self.ProcessResults() 485 486 487class CacheConditions(object): 488 # Cache hit only if the result file exists. 489 CACHE_FILE_EXISTS = 0 490 491 # Cache hit if the checksum of cpuinfo and totalmem of 492 # the cached result and the new run match. 493 MACHINES_MATCH = 1 494 495 # Cache hit if the image checksum of the cached result and the new run match. 496 CHECKSUMS_MATCH = 2 497 498 # Cache hit only if the cached result was successful 499 RUN_SUCCEEDED = 3 500 501 # Never a cache hit. 502 FALSE = 4 503 504 # Cache hit if the image path matches the cached image path. 505 IMAGE_PATH_MATCH = 5 506 507 # Cache hit if the uuid of hard disk mataches the cached one 508 509 SAME_MACHINE_MATCH = 6 510 511 512class ResultsCache(object): 513 """ This class manages the key of the cached runs without worrying about what 514 is exactly stored (value). The value generation is handled by the Results 515 class. 516 """ 517 CACHE_VERSION = 6 518 519 def Init(self, chromeos_image, chromeos_root, test_name, iteration, test_args, 520 profiler_args, machine_manager, machine, board, cache_conditions, 521 logger_to_use, log_level, label, share_cache, suite, 522 show_all_results, run_local): 523 self.chromeos_image = chromeos_image 524 self.chromeos_root = chromeos_root 525 self.test_name = test_name 526 self.iteration = iteration 527 self.test_args = test_args 528 self.profiler_args = profiler_args 529 self.board = board 530 self.cache_conditions = cache_conditions 531 self.machine_manager = machine_manager 532 self.machine = machine 533 self._logger = logger_to_use 534 self.ce = command_executer.GetCommandExecuter(self._logger, 535 log_level=log_level) 536 self.label = label 537 self.share_cache = share_cache 538 self.suite = suite 539 self.log_level = log_level 540 self.show_all = show_all_results 541 self.run_local = run_local 542 543 def GetCacheDirForRead(self): 544 matching_dirs = [] 545 for glob_path in self.FormCacheDir(self.GetCacheKeyList(True)): 546 matching_dirs += glob.glob(glob_path) 547 548 if matching_dirs: 549 # Cache file found. 550 return matching_dirs[0] 551 else: 552 return None 553 554 def GetCacheDirForWrite(self, get_keylist=False): 555 cache_path = self.FormCacheDir(self.GetCacheKeyList(False))[0] 556 if get_keylist: 557 args_str = '%s_%s_%s' % (self.test_args, self.profiler_args, 558 self.run_local) 559 version, image = results_report.ParseChromeosImage( 560 self.label.chromeos_image) 561 keylist = [version, image, self.label.board, self.machine.name, 562 self.test_name, str(self.iteration), args_str] 563 return cache_path, keylist 564 return cache_path 565 566 def FormCacheDir(self, list_of_strings): 567 cache_key = ' '.join(list_of_strings) 568 cache_dir = misc.GetFilenameFromString(cache_key) 569 if self.label.cache_dir: 570 cache_home = os.path.abspath(os.path.expanduser(self.label.cache_dir)) 571 cache_path = [os.path.join(cache_home, cache_dir)] 572 else: 573 cache_path = [os.path.join(SCRATCH_DIR, cache_dir)] 574 575 if len(self.share_cache): 576 for path in [x.strip() for x in self.share_cache.split(',')]: 577 if os.path.exists(path): 578 cache_path.append(os.path.join(path, cache_dir)) 579 else: 580 self._logger.LogFatal('Unable to find shared cache: %s' % path) 581 582 return cache_path 583 584 def GetCacheKeyList(self, read): 585 if read and CacheConditions.MACHINES_MATCH not in self.cache_conditions: 586 machine_checksum = '*' 587 else: 588 machine_checksum = self.machine_manager.machine_checksum[self.label.name] 589 if read and CacheConditions.CHECKSUMS_MATCH not in self.cache_conditions: 590 checksum = '*' 591 elif self.label.image_type == 'trybot': 592 checksum = hashlib.md5(self.label.chromeos_image).hexdigest() 593 elif self.label.image_type == 'official': 594 checksum = '*' 595 else: 596 checksum = ImageChecksummer().Checksum(self.label, self.log_level) 597 598 if read and CacheConditions.IMAGE_PATH_MATCH not in self.cache_conditions: 599 image_path_checksum = '*' 600 else: 601 image_path_checksum = hashlib.md5(self.chromeos_image).hexdigest() 602 603 machine_id_checksum = '' 604 if read and CacheConditions.SAME_MACHINE_MATCH not in self.cache_conditions: 605 machine_id_checksum = '*' 606 else: 607 if self.machine and self.machine.name in self.label.remote: 608 machine_id_checksum = self.machine.machine_id_checksum 609 else: 610 for machine in self.machine_manager.GetMachines(self.label): 611 if machine.name == self.label.remote[0]: 612 machine_id_checksum = machine.machine_id_checksum 613 break 614 615 temp_test_args = '%s %s %s' % (self.test_args, self.profiler_args, 616 self.run_local) 617 test_args_checksum = hashlib.md5(''.join(temp_test_args)).hexdigest() 618 return (image_path_checksum, self.test_name, str(self.iteration), 619 test_args_checksum, checksum, machine_checksum, machine_id_checksum, 620 str(self.CACHE_VERSION)) 621 622 def ReadResult(self): 623 if CacheConditions.FALSE in self.cache_conditions: 624 cache_dir = self.GetCacheDirForWrite() 625 command = 'rm -rf {0}'.format(cache_dir) 626 self.ce.RunCommand(command) 627 return None 628 cache_dir = self.GetCacheDirForRead() 629 630 if not cache_dir: 631 return None 632 633 if not os.path.isdir(cache_dir): 634 return None 635 636 if self.log_level == 'verbose': 637 self._logger.LogOutput('Trying to read from cache dir: %s' % cache_dir) 638 result = Result.CreateFromCacheHit(self._logger, self.log_level, self.label, 639 self.machine, cache_dir, self.show_all, 640 self.test_name, self.suite) 641 if not result: 642 return None 643 644 if (result.retval == 0 or 645 CacheConditions.RUN_SUCCEEDED not in self.cache_conditions): 646 return result 647 648 return None 649 650 def StoreResult(self, result): 651 cache_dir, keylist = self.GetCacheDirForWrite(get_keylist=True) 652 result.StoreToCacheDir(cache_dir, self.machine_manager, keylist) 653 654 655class MockResultsCache(ResultsCache): 656 657 def Init(self, *args): 658 pass 659 660 def ReadResult(self): 661 return None 662 663 def StoreResult(self, result): 664 pass 665 666 667class MockResult(Result): 668 669 def PopulateFromRun(self, out, err, retval, show_all, test, suite): 670 self.out = out 671 self.err = err 672 self.retval = retval 673