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