952 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			952 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
| #!/usr/bin/python2
 | |
| # Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
 | |
| # Use of this source code is governed by a BSD-style license that can be
 | |
| # found in the LICENSE file.
 | |
| 
 | |
| 
 | |
| """Parses and displays the contents of one or more autoserv result directories.
 | |
| 
 | |
| This script parses the contents of one or more autoserv results folders and
 | |
| generates test reports.
 | |
| """
 | |
| 
 | |
| import datetime
 | |
| import glob
 | |
| import logging
 | |
| import operator
 | |
| import optparse
 | |
| import os
 | |
| import re
 | |
| import sys
 | |
| 
 | |
| import common
 | |
| from autotest_lib.utils import terminal
 | |
| 
 | |
| 
 | |
| _STDOUT_IS_TTY = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
 | |
| 
 | |
| 
 | |
| def Die(message_format, *args, **kwargs):
 | |
|     """Log a message and kill the current process.
 | |
| 
 | |
|     @param message_format: string for logging.error.
 | |
| 
 | |
|     """
 | |
|     logging.error(message_format, *args, **kwargs)
 | |
|     sys.exit(1)
 | |
| 
 | |
| 
 | |
| class CrashWaiver:
 | |
|     """Represents a crash that we want to ignore for now."""
 | |
|     def __init__(self, signals, deadline, url, person):
 | |
|         self.signals = signals
 | |
|         self.deadline = datetime.datetime.strptime(deadline, '%Y-%b-%d')
 | |
|         self.issue_url = url
 | |
|         self.suppressor = person
 | |
| 
 | |
| # List of crashes which are okay to ignore. This list should almost always be
 | |
| # empty. If you add an entry, include the bug URL and your name, something like
 | |
| #     'crashy':CrashWaiver(
 | |
| #       ['sig 11'], '2011-Aug-18', 'http://crosbug/123456', 'developer'),
 | |
| 
 | |
| _CRASH_ALLOWLIST = {
 | |
| }
 | |
| 
 | |
| 
 | |
| class ResultCollector(object):
 | |
|     """Collects status and performance data from an autoserv results dir."""
 | |
| 
 | |
|     def __init__(self, collect_perf=True, collect_attr=False,
 | |
|                  collect_info=False, escape_error=False,
 | |
|                  allow_chrome_crashes=False):
 | |
|         """Initialize ResultsCollector class.
 | |
| 
 | |
|         @param collect_perf: Should perf keyvals be collected?
 | |
|         @param collect_attr: Should attr keyvals be collected?
 | |
|         @param collect_info: Should info keyvals be collected?
 | |
|         @param escape_error: Escape error message text for tools.
 | |
|         @param allow_chrome_crashes: Treat Chrome crashes as non-fatal.
 | |
| 
 | |
|         """
 | |
|         self._collect_perf = collect_perf
 | |
|         self._collect_attr = collect_attr
 | |
|         self._collect_info = collect_info
 | |
|         self._escape_error = escape_error
 | |
|         self._allow_chrome_crashes = allow_chrome_crashes
 | |
| 
 | |
|     def _CollectPerf(self, testdir):
 | |
|         """Parses keyval file under testdir and return the perf keyval pairs.
 | |
| 
 | |
|         @param testdir: autoserv test result directory path.
 | |
| 
 | |
|         @return dict of perf keyval pairs.
 | |
| 
 | |
|         """
 | |
|         if not self._collect_perf:
 | |
|             return {}
 | |
|         return self._CollectKeyval(testdir, 'perf')
 | |
| 
 | |
|     def _CollectAttr(self, testdir):
 | |
|         """Parses keyval file under testdir and return the attr keyval pairs.
 | |
| 
 | |
|         @param testdir: autoserv test result directory path.
 | |
| 
 | |
|         @return dict of attr keyval pairs.
 | |
| 
 | |
|         """
 | |
|         if not self._collect_attr:
 | |
|             return {}
 | |
|         return self._CollectKeyval(testdir, 'attr')
 | |
| 
 | |
|     def _CollectKeyval(self, testdir, keyword):
 | |
|         """Parses keyval file under testdir.
 | |
| 
 | |
|         If testdir contains a result folder, process the keyval file and return
 | |
|         a dictionary of perf keyval pairs.
 | |
| 
 | |
|         @param testdir: The autoserv test result directory.
 | |
|         @param keyword: The keyword of keyval, either 'perf' or 'attr'.
 | |
| 
 | |
|         @return If the perf option is disabled or the there's no keyval file
 | |
|                 under testdir, returns an empty dictionary. Otherwise, returns
 | |
|                 a dictionary of parsed keyvals. Duplicate keys are uniquified
 | |
|                 by their instance number.
 | |
| 
 | |
|         """
 | |
|         keyval = {}
 | |
|         keyval_file = os.path.join(testdir, 'results', 'keyval')
 | |
|         if not os.path.isfile(keyval_file):
 | |
|             return keyval
 | |
| 
 | |
|         instances = {}
 | |
| 
 | |
|         for line in open(keyval_file):
 | |
|             match = re.search(r'^(.+){%s}=(.+)$' % keyword, line)
 | |
|             if match:
 | |
|                 key = match.group(1)
 | |
|                 val = match.group(2)
 | |
| 
 | |
|                 # If the same key name was generated multiple times, uniquify
 | |
|                 # all instances other than the first one by adding the instance
 | |
|                 # count to the key name.
 | |
|                 key_inst = key
 | |
|                 instance = instances.get(key, 0)
 | |
|                 if instance:
 | |
|                     key_inst = '%s{%d}' % (key, instance)
 | |
|                 instances[key] = instance + 1
 | |
| 
 | |
|                 keyval[key_inst] = val
 | |
| 
 | |
|         return keyval
 | |
| 
 | |
|     def _CollectCrashes(self, status_raw):
 | |
|         """Parses status_raw file for crashes.
 | |
| 
 | |
|         Saves crash details if crashes are discovered.  If an allowlist is
 | |
|         present, only records allowed crashes.
 | |
| 
 | |
|         @param status_raw: The contents of the status.log or status file from
 | |
|                 the test.
 | |
| 
 | |
|         @return a list of crash entries to be reported.
 | |
| 
 | |
|         """
 | |
|         crashes = []
 | |
|         regex = re.compile(
 | |
|                 'Received crash notification for ([-\w]+).+ (sig \d+)')
 | |
|         chrome_regex = re.compile(r'^supplied_[cC]hrome|^chrome$')
 | |
|         for match in regex.finditer(status_raw):
 | |
|             w = _CRASH_ALLOWLIST.get(match.group(1))
 | |
|             if (self._allow_chrome_crashes and
 | |
|                     chrome_regex.match(match.group(1))):
 | |
|                 print '@@@STEP_WARNINGS@@@'
 | |
|                 print '%s crashed with %s' % (match.group(1), match.group(2))
 | |
|             elif (w is not None and match.group(2) in w.signals and
 | |
|                         w.deadline > datetime.datetime.now()):
 | |
|                 print 'Ignoring crash in %s for waiver that expires %s' % (
 | |
|                         match.group(1), w.deadline.strftime('%Y-%b-%d'))
 | |
|             else:
 | |
|                 crashes.append('%s %s' % match.groups())
 | |
|         return crashes
 | |
| 
 | |
|     def _CollectInfo(self, testdir, custom_info):
 | |
|         """Parses *_info files under testdir/sysinfo/var/log.
 | |
| 
 | |
|         If the sysinfo/var/log/*info files exist, save information that shows
 | |
|         hw, ec and bios version info.
 | |
| 
 | |
|         This collection of extra info is disabled by default (this funtion is
 | |
|         a no-op).  It is enabled only if the --info command-line option is
 | |
|         explicitly supplied.  Normal job parsing does not supply this option.
 | |
| 
 | |
|         @param testdir: The autoserv test result directory.
 | |
|         @param custom_info: Dictionary to collect detailed ec/bios info.
 | |
| 
 | |
|         @return a dictionary of info that was discovered.
 | |
| 
 | |
|         """
 | |
|         if not self._collect_info:
 | |
|             return {}
 | |
|         info = custom_info
 | |
| 
 | |
|         sysinfo_dir = os.path.join(testdir, 'sysinfo', 'var', 'log')
 | |
|         for info_file, info_keys in {'ec_info.txt': ['fw_version'],
 | |
|                                      'bios_info.txt': ['fwid',
 | |
|                                                        'hwid']}.iteritems():
 | |
|             info_file_path = os.path.join(sysinfo_dir, info_file)
 | |
|             if not os.path.isfile(info_file_path):
 | |
|                 continue
 | |
|             # Some example raw text that might be matched include:
 | |
|             #
 | |
|             # fw_version           | snow_v1.1.332-cf20b3e
 | |
|             # fwid = Google_Snow.2711.0.2012_08_06_1139 # Active firmware ID
 | |
|             # hwid = DAISY TEST A-A 9382                # Hardware ID
 | |
|             info_regex = re.compile(r'^(%s)\s*[|=]\s*(.*)' %
 | |
|                                     '|'.join(info_keys))
 | |
|             with open(info_file_path, 'r') as f:
 | |
|                 for line in f:
 | |
|                     line = line.strip()
 | |
|                     line = line.split('#')[0]
 | |
|                     match = info_regex.match(line)
 | |
|                     if match:
 | |
|                         info[match.group(1)] = str(match.group(2)).strip()
 | |
|         return info
 | |
| 
 | |
|     def _CollectEndTimes(self, status_raw, status_re='', is_end=True):
 | |
|         """Helper to match and collect timestamp and localtime.
 | |
| 
 | |
|         Preferred to locate timestamp and localtime with an
 | |
|         'END GOOD test_name...' line.  However, aborted tests occasionally fail
 | |
|         to produce this line and then need to scrape timestamps from the 'START
 | |
|         test_name...' line.
 | |
| 
 | |
|         @param status_raw: multi-line text to search.
 | |
|         @param status_re: status regex to seek (e.g. GOOD|FAIL)
 | |
|         @param is_end: if True, search for 'END' otherwise 'START'.
 | |
| 
 | |
|         @return Tuple of timestamp, localtime retrieved from the test status
 | |
|                 log.
 | |
| 
 | |
|         """
 | |
|         timestamp = ''
 | |
|         localtime = ''
 | |
| 
 | |
|         localtime_re = r'\w+\s+\w+\s+[:\w]+'
 | |
|         match_filter = (
 | |
|                 r'^\s*%s\s+(?:%s).*timestamp=(\d*).*localtime=(%s).*$' % (
 | |
|                 'END' if is_end else 'START', status_re, localtime_re))
 | |
|         matches = re.findall(match_filter, status_raw, re.MULTILINE)
 | |
|         if matches:
 | |
|             # There may be multiple lines with timestamp/localtime info.
 | |
|             # The last one found is selected because it will reflect the end
 | |
|             # time.
 | |
|             for i in xrange(len(matches)):
 | |
|                 timestamp_, localtime_ = matches[-(i+1)]
 | |
|                 if not timestamp or timestamp_ > timestamp:
 | |
|                     timestamp = timestamp_
 | |
|                     localtime = localtime_
 | |
|         return timestamp, localtime
 | |
| 
 | |
|     def _CheckExperimental(self, testdir):
 | |
|         """Parses keyval file and return the value of `experimental`.
 | |
| 
 | |
|         @param testdir: The result directory that has the keyval file.
 | |
| 
 | |
|         @return The value of 'experimental', which is a boolean value indicating
 | |
|                 whether it is an experimental test or not.
 | |
| 
 | |
|         """
 | |
|         keyval_file = os.path.join(testdir, 'keyval')
 | |
|         if not os.path.isfile(keyval_file):
 | |
|             return False
 | |
| 
 | |
|         with open(keyval_file) as f:
 | |
|             for line in f:
 | |
|                 match = re.match(r'experimental=(.+)', line)
 | |
|                 if match:
 | |
|                     return match.group(1) == 'True'
 | |
|             else:
 | |
|                 return False
 | |
| 
 | |
| 
 | |
|     def _CollectResult(self, testdir, results, is_experimental=False):
 | |
|         """Collects results stored under testdir into a dictionary.
 | |
| 
 | |
|         The presence/location of status files (status.log, status and
 | |
|         job_report.html) varies depending on whether the job is a simple
 | |
|         client test, simple server test, old-style suite or new-style
 | |
|         suite.  For example:
 | |
|         -In some cases a single job_report.html may exist but many times
 | |
|          multiple instances are produced in a result tree.
 | |
|         -Most tests will produce a status.log but client tests invoked
 | |
|          by a server test will only emit a status file.
 | |
| 
 | |
|         The two common criteria that seem to define the presence of a
 | |
|         valid test result are:
 | |
|         1. Existence of a 'status.log' or 'status' file. Note that if both a
 | |
|              'status.log' and 'status' file exist for a test, the 'status' file
 | |
|              is always a subset of the 'status.log' fle contents.
 | |
|         2. Presence of a 'debug' directory.
 | |
| 
 | |
|         In some cases multiple 'status.log' files will exist where the parent
 | |
|         'status.log' contains the contents of multiple subdirectory 'status.log'
 | |
|         files.  Parent and subdirectory 'status.log' files are always expected
 | |
|         to agree on the outcome of a given test.
 | |
| 
 | |
|         The test results discovered from the 'status*' files are included
 | |
|         in the result dictionary.  The test directory name and a test directory
 | |
|         timestamp/localtime are saved to be used as sort keys for the results.
 | |
| 
 | |
|         The value of 'is_experimental' is included in the result dictionary.
 | |
| 
 | |
|         @param testdir: The autoserv test result directory.
 | |
|         @param results: A list to which a populated test-result-dictionary will
 | |
|                 be appended if a status file is found.
 | |
|         @param is_experimental: A boolean value indicating whether the result
 | |
|                 directory is for an experimental test.
 | |
| 
 | |
|         """
 | |
|         status_file = os.path.join(testdir, 'status.log')
 | |
|         if not os.path.isfile(status_file):
 | |
|             status_file = os.path.join(testdir, 'status')
 | |
|             if not os.path.isfile(status_file):
 | |
|                 return
 | |
| 
 | |
|         # Status is True if GOOD, else False for all others.
 | |
|         status = False
 | |
|         error_msg = ''
 | |
|         status_raw = open(status_file, 'r').read()
 | |
|         failure_tags = 'ABORT|ERROR|FAIL'
 | |
|         warning_tag = 'WARN|TEST_NA'
 | |
|         failure = re.search(r'%s' % failure_tags, status_raw)
 | |
|         warning = re.search(r'%s' % warning_tag, status_raw) and not failure
 | |
|         good = (re.search(r'GOOD.+completed successfully', status_raw) and
 | |
|                              not (failure or warning))
 | |
| 
 | |
|         # We'd like warnings to allow the tests to pass, but still gather info.
 | |
|         if good or warning:
 | |
|             status = True
 | |
| 
 | |
|         if not good:
 | |
|             match = re.search(r'^\t+(%s|%s)\t(.+)' % (failure_tags,
 | |
|                                                       warning_tag),
 | |
|                               status_raw, re.MULTILINE)
 | |
|             if match:
 | |
|                 failure_type = match.group(1)
 | |
|                 reason = match.group(2).split('\t')[4]
 | |
|                 if self._escape_error:
 | |
|                     reason = re.escape(reason)
 | |
|                 error_msg = ': '.join([failure_type, reason])
 | |
| 
 | |
|         # Grab the timestamp - can be used for sorting the test runs.
 | |
|         # Grab the localtime - may be printed to enable line filtering by date.
 | |
|         # Designed to match a line like this:
 | |
|         #   END GOOD testname ... timestamp=1347324321 localtime=Sep 10 17:45:21
 | |
|         status_re = r'GOOD|%s|%s' % (failure_tags, warning_tag)
 | |
|         timestamp, localtime = self._CollectEndTimes(status_raw, status_re)
 | |
|         # Hung tests will occasionally skip printing the END line so grab
 | |
|         # a default timestamp from the START line in those cases.
 | |
|         if not timestamp:
 | |
|             timestamp, localtime = self._CollectEndTimes(status_raw,
 | |
|                                                          is_end=False)
 | |
| 
 | |
|         results.append({
 | |
|                 'testdir': testdir,
 | |
|                 'crashes': self._CollectCrashes(status_raw),
 | |
|                 'status': status,
 | |
|                 'error_msg': error_msg,
 | |
|                 'localtime': localtime,
 | |
|                 'timestamp': timestamp,
 | |
|                 'perf': self._CollectPerf(testdir),
 | |
|                 'attr': self._CollectAttr(testdir),
 | |
|                 'info': self._CollectInfo(testdir, {'localtime': localtime,
 | |
|                                                     'timestamp': timestamp}),
 | |
|                 'experimental': is_experimental})
 | |
| 
 | |
|     def RecursivelyCollectResults(self, resdir, parent_experimental_tag=False):
 | |
|         """Recursively collect results into a list of dictionaries.
 | |
| 
 | |
|         Only recurses into directories that possess a 'debug' subdirectory
 | |
|         because anything else is not considered a 'test' directory.
 | |
| 
 | |
|         The value of 'experimental' in keyval file is used to determine whether
 | |
|         the result is for an experimental test. If it is, all its sub
 | |
|         directories are considered to be experimental tests too.
 | |
| 
 | |
|         @param resdir: results/test directory to parse results from and recurse
 | |
|                 into.
 | |
|         @param parent_experimental_tag: A boolean value, used to keep track of
 | |
|                 whether its parent directory is for an experimental test.
 | |
| 
 | |
|         @return List of dictionaries of results.
 | |
| 
 | |
|         """
 | |
|         results = []
 | |
|         is_experimental = (parent_experimental_tag or
 | |
|                            self._CheckExperimental(resdir))
 | |
|         self._CollectResult(resdir, results, is_experimental)
 | |
|         for testdir in glob.glob(os.path.join(resdir, '*')):
 | |
|             # Remove false positives that are missing a debug dir.
 | |
|             if not os.path.exists(os.path.join(testdir, 'debug')):
 | |
|                 continue
 | |
| 
 | |
|             results.extend(self.RecursivelyCollectResults(
 | |
|                     testdir, is_experimental))
 | |
|         return results
 | |
| 
 | |
| 
 | |
| class ReportGenerator(object):
 | |
|     """Collects and displays data from autoserv results directories.
 | |
| 
 | |
|     This class collects status and performance data from one or more autoserv
 | |
|     result directories and generates test reports.
 | |
|     """
 | |
| 
 | |
|     _KEYVAL_INDENT = 2
 | |
|     _STATUS_STRINGS = {'hr': {'pass': '[  PASSED  ]', 'fail': '[  FAILED  ]'},
 | |
|                        'csv': {'pass': 'PASS', 'fail': 'FAIL'}}
 | |
| 
 | |
|     def __init__(self, options, args):
 | |
|         self._options = options
 | |
|         self._args = args
 | |
|         self._color = terminal.Color(options.color)
 | |
|         self._results = []
 | |
| 
 | |
|     def _CollectAllResults(self):
 | |
|         """Parses results into the self._results list.
 | |
| 
 | |
|         Builds a list (self._results) where each entry is a dictionary of
 | |
|         result data from one test (which may contain other tests). Each
 | |
|         dictionary will contain values such as: test folder, status, localtime,
 | |
|         crashes, error_msg, perf keyvals [optional], info [optional].
 | |
| 
 | |
|         """
 | |
|         collector = ResultCollector(
 | |
|                 collect_perf=self._options.perf,
 | |
|                 collect_attr=self._options.attr,
 | |
|                 collect_info=self._options.info,
 | |
|                 escape_error=self._options.escape_error,
 | |
|                 allow_chrome_crashes=self._options.allow_chrome_crashes)
 | |
| 
 | |
|         for resdir in self._args:
 | |
|             if not os.path.isdir(resdir):
 | |
|                 Die('%r does not exist', resdir)
 | |
|             self._results.extend(collector.RecursivelyCollectResults(resdir))
 | |
| 
 | |
|         if not self._results:
 | |
|             Die('no test directories found')
 | |
| 
 | |
|     def _GenStatusString(self, status):
 | |
|         """Given a bool indicating success or failure, return the right string.
 | |
| 
 | |
|         Also takes --csv into account, returns old-style strings if it is set.
 | |
| 
 | |
|         @param status: True or False, indicating success or failure.
 | |
| 
 | |
|         @return The appropriate string for printing..
 | |
| 
 | |
|         """
 | |
|         success = 'pass' if status else 'fail'
 | |
|         if self._options.csv:
 | |
|             return self._STATUS_STRINGS['csv'][success]
 | |
|         return self._STATUS_STRINGS['hr'][success]
 | |
| 
 | |
|     def _Indent(self, msg):
 | |
|         """Given a message, indents it appropriately.
 | |
| 
 | |
|         @param msg: string to indent.
 | |
|         @return indented version of msg.
 | |
| 
 | |
|         """
 | |
|         return ' ' * self._KEYVAL_INDENT + msg
 | |
| 
 | |
|     def _GetTestColumnWidth(self):
 | |
|         """Returns the test column width based on the test data.
 | |
| 
 | |
|         The test results are aligned by discovering the longest width test
 | |
|         directory name or perf key stored in the list of result dictionaries.
 | |
| 
 | |
|         @return The width for the test column.
 | |
| 
 | |
|         """
 | |
|         width = 0
 | |
|         for result in self._results:
 | |
|             width = max(width, len(result['testdir']))
 | |
|             perf = result.get('perf')
 | |
|             if perf:
 | |
|                 perf_key_width = len(max(perf, key=len))
 | |
|                 width = max(width, perf_key_width + self._KEYVAL_INDENT)
 | |
|         return width
 | |
| 
 | |
|     def _PrintDashLine(self, width):
 | |
|         """Prints a line of dashes as a separator in output.
 | |
| 
 | |
|         @param width: an integer.
 | |
|         """
 | |
|         if not self._options.csv:
 | |
|             print ''.ljust(width + len(self._STATUS_STRINGS['hr']['pass']), '-')
 | |
| 
 | |
|     def _PrintEntries(self, entries):
 | |
|         """Prints a list of strings, delimited based on --csv flag.
 | |
| 
 | |
|         @param entries: a list of strings, entities to output.
 | |
| 
 | |
|         """
 | |
|         delimiter = ',' if self._options.csv else ' '
 | |
|         print delimiter.join(entries)
 | |
| 
 | |
|     def _PrintErrors(self, test, error_msg):
 | |
|         """Prints an indented error message, unless the --csv flag is set.
 | |
| 
 | |
|         @param test: the name of a test with which to prefix the line.
 | |
|         @param error_msg: a message to print.  None is allowed, but ignored.
 | |
| 
 | |
|         """
 | |
|         if not self._options.csv and error_msg:
 | |
|             self._PrintEntries([test, self._Indent(error_msg)])
 | |
| 
 | |
|     def _PrintErrorLogs(self, test, test_string):
 | |
|         """Prints the error log for |test| if --debug is set.
 | |
| 
 | |
|         @param test: the name of a test suitable for embedding in a path
 | |
|         @param test_string: the name of a test with which to prefix the line.
 | |
| 
 | |
|         """
 | |
|         if self._options.print_debug:
 | |
|             debug_file_regex = os.path.join(
 | |
|                     'results.', test, 'debug',
 | |
|                     '%s*.ERROR' % os.path.basename(test))
 | |
|             for path in glob.glob(debug_file_regex):
 | |
|                 try:
 | |
|                     with open(path) as fh:
 | |
|                         for line in fh:
 | |
|                             # Ensure line is not just WS.
 | |
|                             if len(line.lstrip()) <=  0:
 | |
|                                 continue
 | |
|                             self._PrintEntries(
 | |
|                                     [test_string, self._Indent(line.rstrip())])
 | |
|                 except IOError:
 | |
|                     print 'Could not open %s' % path
 | |
| 
 | |
|     def _PrintResultDictKeyVals(self, test_entry, result_dict):
 | |
|         """Formatted print a dict of keyvals like 'perf' or 'info'.
 | |
| 
 | |
|         This function emits each keyval on a single line for uncompressed
 | |
|         review.  The 'perf' dictionary contains performance keyvals while the
 | |
|         'info' dictionary contains ec info, bios info and some test timestamps.
 | |
| 
 | |
|         @param test_entry: The unique name of the test (dir) - matches other
 | |
|                 test output.
 | |
|         @param result_dict: A dict of keyvals to be presented.
 | |
| 
 | |
|         """
 | |
|         if not result_dict:
 | |
|             return
 | |
|         dict_keys = result_dict.keys()
 | |
|         dict_keys.sort()
 | |
|         width = self._GetTestColumnWidth()
 | |
|         for dict_key in dict_keys:
 | |
|             if self._options.csv:
 | |
|                 key_entry = dict_key
 | |
|             else:
 | |
|                 key_entry = dict_key.ljust(width - self._KEYVAL_INDENT)
 | |
|                 key_entry = key_entry.rjust(width)
 | |
|             value_entry = self._color.Color(
 | |
|                     self._color.BOLD, result_dict[dict_key])
 | |
|             self._PrintEntries([test_entry, key_entry, value_entry])
 | |
| 
 | |
|     def _GetSortedTests(self):
 | |
|         """Sort the test result dicts in preparation for results printing.
 | |
| 
 | |
|         By default sorts the results directionaries by their test names.
 | |
|         However, when running long suites, it is useful to see if an early test
 | |
|         has wedged the system and caused the remaining tests to abort/fail. The
 | |
|         datetime-based chronological sorting allows this view.
 | |
| 
 | |
|         Uses the --sort-chron command line option to control.
 | |
| 
 | |
|         """
 | |
|         if self._options.sort_chron:
 | |
|             # Need to reverse sort the test dirs to ensure the suite folder
 | |
|             # shows at the bottom. Because the suite folder shares its datetime
 | |
|             # with the last test it shows second-to-last without the reverse
 | |
|             # sort first.
 | |
|             tests = sorted(self._results, key=operator.itemgetter('testdir'),
 | |
|                            reverse=True)
 | |
|             tests = sorted(tests, key=operator.itemgetter('timestamp'))
 | |
|         else:
 | |
|             tests = sorted(self._results, key=operator.itemgetter('testdir'))
 | |
|         return tests
 | |
| 
 | |
|     # TODO(zamorzaev): reuse this method in _GetResultsForHTMLReport to avoid
 | |
|     # code copying.
 | |
|     def _GetDedupedResults(self):
 | |
|         """Aggregate results from multiple retries of the same test."""
 | |
|         deduped_results = {}
 | |
|         for test in self._GetSortedTests():
 | |
|             test_details_matched = re.search(r'(.*)results-(\d[0-9]*)-(.*)',
 | |
|                                              test['testdir'])
 | |
|             if not test_details_matched:
 | |
|                 continue
 | |
| 
 | |
|             log_dir, test_number, test_name = test_details_matched.groups()
 | |
|             if (test_name in deduped_results and
 | |
|                 deduped_results[test_name].get('status')):
 | |
|                 # Already have a successfull (re)try.
 | |
|                 continue
 | |
| 
 | |
|             deduped_results[test_name] = test
 | |
|         return deduped_results.values()
 | |
| 
 | |
|     def _GetResultsForHTMLReport(self):
 | |
|         """Return cleaned results for HTML report.!"""
 | |
|         import copy
 | |
|         tests = copy.deepcopy(self._GetSortedTests())
 | |
|         pass_tag = "Pass"
 | |
|         fail_tag = "Fail"
 | |
|         na_tag = "NA"
 | |
|         count = 0
 | |
|         html_results = {}
 | |
|         for test_status in tests:
 | |
|             individual_tc_results = {}
 | |
|             test_details_matched = re.search(r'(.*)results-(\d[0-9]*)-(.*)',
 | |
|                                              test_status['testdir'])
 | |
|             if not test_details_matched:
 | |
|                 continue
 | |
|             log_dir = test_details_matched.group(1)
 | |
|             test_number = test_details_matched.group(2)
 | |
|             test_name = test_details_matched.group(3)
 | |
|             if '/' in test_name:
 | |
|                 test_name = test_name.split('/')[0]
 | |
|             if test_status['error_msg'] is None:
 | |
|                 test_status['error_msg'] = ''
 | |
|             if not html_results.has_key(test_name):
 | |
|                 count = count + 1
 | |
|                 # Arranging the results in an order
 | |
|                 individual_tc_results['status'] = test_status['status']
 | |
|                 individual_tc_results['error_msg'] = test_status['error_msg']
 | |
|                 individual_tc_results['s_no'] = count
 | |
|                 individual_tc_results['crashes'] = test_status['crashes']
 | |
| 
 | |
|                 # Add <b> and </b> tag for the good format in the report.
 | |
|                 individual_tc_results['attempts'] = \
 | |
|                     '<b>test_result_number: %s - %s</b> : %s' % (
 | |
|                         test_number, log_dir, test_status['error_msg'])
 | |
|                 html_results[test_name] = individual_tc_results
 | |
|             else:
 | |
| 
 | |
|                 # If test found already then we are using the previous data
 | |
|                 # instead of creating two different html rows. If existing
 | |
|                 # status is False then needs to be updated
 | |
|                 if html_results[test_name]['status'] is False:
 | |
|                     html_results[test_name]['status'] = test_status['status']
 | |
|                     html_results[test_name]['error_msg'] = test_status[
 | |
|                         'error_msg']
 | |
|                     html_results[test_name]['crashes'] = \
 | |
|                         html_results[test_name]['crashes'] + test_status[
 | |
|                             'crashes']
 | |
|                     html_results[test_name]['attempts'] = \
 | |
|                         html_results[test_name]['attempts'] + \
 | |
|                         '</br><b>test_result_number : %s - %s</b> : %s' % (
 | |
|                             test_number, log_dir, test_status['error_msg'])
 | |
| 
 | |
|         # Re-formating the dictionary as s_no as key. So that we can have
 | |
|         # ordered data at the end
 | |
|         sorted_html_results = {}
 | |
|         for key in html_results.keys():
 | |
|             sorted_html_results[str(html_results[key]['s_no'])] = \
 | |
|                     html_results[key]
 | |
|             sorted_html_results[str(html_results[key]['s_no'])]['test'] = key
 | |
| 
 | |
|         # Mapping the Test case status if True->Pass, False->Fail and if
 | |
|         # True and the error message then NA
 | |
|         for key in sorted_html_results.keys():
 | |
|             if sorted_html_results[key]['status']:
 | |
|                 if sorted_html_results[key]['error_msg'] != '':
 | |
|                     sorted_html_results[key]['status'] = na_tag
 | |
|                 else:
 | |
|                     sorted_html_results[key]['status'] = pass_tag
 | |
|             else:
 | |
|                 sorted_html_results[key]['status'] = fail_tag
 | |
| 
 | |
|         return sorted_html_results
 | |
| 
 | |
|     def GenerateReportHTML(self):
 | |
|         """Generate clean HTMl report for the results."""
 | |
| 
 | |
|         results = self._GetResultsForHTMLReport()
 | |
|         html_table_header = """ <th>S.No</th>
 | |
|                                 <th>Test</th>
 | |
|                                 <th>Status</th>
 | |
|                                 <th>Error Message</th>
 | |
|                                 <th>Crashes</th>
 | |
|                                 <th>Attempts</th>
 | |
|                             """
 | |
|         passed_tests = len([key for key in results.keys() if results[key][
 | |
|                 'status'].lower() == 'pass'])
 | |
|         failed_tests = len([key for key in results.keys() if results[key][
 | |
|             'status'].lower() == 'fail'])
 | |
|         na_tests = len([key for key in results.keys() if results[key][
 | |
|             'status'].lower() == 'na'])
 | |
|         total_tests = passed_tests + failed_tests + na_tests
 | |
| 
 | |
|         # Sort the keys
 | |
|         ordered_keys = sorted([int(key) for key in results.keys()])
 | |
|         html_table_body = ''
 | |
|         for key in ordered_keys:
 | |
|             key = str(key)
 | |
|             if results[key]['status'].lower() == 'pass':
 | |
|                 color = 'LimeGreen'
 | |
|             elif results[key]['status'].lower() == 'na':
 | |
|                 color = 'yellow'
 | |
|             else:
 | |
|                 color = 'red'
 | |
|             html_table_body = html_table_body + """<tr>
 | |
|                                                     <td>%s</td>
 | |
|                                                     <td>%s</td>
 | |
|                                                     <td
 | |
|                                                     style="background-color:%s;">
 | |
|                                                     %s</td>
 | |
|                                                     <td>%s</td>
 | |
|                                                     <td>%s</td>
 | |
|                                                     <td>%s</td></tr>""" % \
 | |
|                                                 (key, results[key]['test'],
 | |
|                                                  color,
 | |
|                                                  results[key]['status'],
 | |
|                                                  results[key]['error_msg'],
 | |
|                                                  results[key]['crashes'],
 | |
|                                                  results[key]['attempts'])
 | |
|         html_page = """
 | |
|                         <!DOCTYPE html>
 | |
|                         <html lang="en">
 | |
|                         <head>
 | |
|                             <title>Automation Results</title>
 | |
|                             <meta charset="utf-8">
 | |
|                             <meta name="viewport" content="width=device-width,initial-scale=1">
 | |
|                             <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
 | |
|                             <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
 | |
|                             <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
 | |
|                         </head>
 | |
|                         <body>
 | |
|                             <div class="container">
 | |
|                                 <h2>Automation Report</h2>
 | |
|                                 <table class="table table-bordered" border="1">
 | |
|                                     <thead>
 | |
|                                         <tr style="background-color:LightSkyBlue;">
 | |
|                                         \n%s
 | |
|                                         </tr>
 | |
|                                     </thead>
 | |
|                                     <tbody>
 | |
|                                     \n%s
 | |
|                                     </tbody>
 | |
|                                 </table>
 | |
|                                 <div class="row">
 | |
|                                     <div class="col-sm-4">Passed: <b>%d</b></div>
 | |
|                                     <div class="col-sm-4">Failed: <b>%d</b></div>
 | |
|                                     <div class="col-sm-4">NA: <b>%d</b></div>
 | |
|                                 </div>
 | |
|                                 <div class="row">
 | |
|                                     <div class="col-sm-4">Total: <b>%d</b></div>
 | |
|                                 </div>
 | |
|                             </div>
 | |
|                         </body>
 | |
|                         </html>
 | |
| 
 | |
|                 """ % (html_table_header, html_table_body, passed_tests,
 | |
|                        failed_tests, na_tests, total_tests)
 | |
|         with open(os.path.join(self._options.html_report_dir,
 | |
|                                "test_report.html"), 'w') as html_file:
 | |
|             html_file.write(html_page)
 | |
| 
 | |
|     def _GenerateReportText(self):
 | |
|         """Prints a result report to stdout.
 | |
| 
 | |
|         Prints a result table to stdout. Each row of the table contains the
 | |
|         test result directory and the test result (PASS, FAIL). If the perf
 | |
|         option is enabled, each test entry is followed by perf keyval entries
 | |
|         from the test results.
 | |
| 
 | |
|         """
 | |
|         tests = self._GetSortedTests()
 | |
|         width = self._GetTestColumnWidth()
 | |
| 
 | |
|         crashes = {}
 | |
|         tests_pass = 0
 | |
|         self._PrintDashLine(width)
 | |
| 
 | |
|         for result in tests:
 | |
|             testdir = result['testdir']
 | |
|             test_entry = testdir if self._options.csv else testdir.ljust(width)
 | |
| 
 | |
|             status_entry = self._GenStatusString(result['status'])
 | |
|             if result['status']:
 | |
|                 color = self._color.GREEN
 | |
|                 # Change the color of 'PASSED' if the test run wasn't completely
 | |
|                 # ok, so it's more obvious it isn't a pure pass.
 | |
|                 if 'WARN' in result['error_msg']:
 | |
|                     color = self._color.YELLOW
 | |
|                 elif 'TEST_NA' in result['error_msg']:
 | |
|                     color = self._color.MAGENTA
 | |
|                 tests_pass += 1
 | |
|             else:
 | |
|                 color = self._color.RED
 | |
| 
 | |
|             test_entries = [test_entry, self._color.Color(color, status_entry)]
 | |
| 
 | |
|             info = result.get('info', {})
 | |
|             info.update(result.get('attr', {}))
 | |
|             if self._options.csv and (self._options.info or self._options.attr):
 | |
|                 if info:
 | |
|                     test_entries.extend(['%s=%s' % (k, info[k])
 | |
|                                         for k in sorted(info.keys())])
 | |
|                 if not result['status'] and result['error_msg']:
 | |
|                     test_entries.append('reason="%s"' % result['error_msg'])
 | |
| 
 | |
|             self._PrintEntries(test_entries)
 | |
|             self._PrintErrors(test_entry, result['error_msg'])
 | |
| 
 | |
|             # Print out error log for failed tests.
 | |
|             if not result['status']:
 | |
|                 self._PrintErrorLogs(testdir, test_entry)
 | |
| 
 | |
|             # Emit the perf keyvals entries. There will be no entries if the
 | |
|             # --no-perf option is specified.
 | |
|             self._PrintResultDictKeyVals(test_entry, result['perf'])
 | |
| 
 | |
|             # Determine that there was a crash during this test.
 | |
|             if result['crashes']:
 | |
|                 for crash in result['crashes']:
 | |
|                     if not crash in crashes:
 | |
|                         crashes[crash] = set([])
 | |
|                     crashes[crash].add(testdir)
 | |
| 
 | |
|             # Emit extra test metadata info on separate lines if not --csv.
 | |
|             if not self._options.csv:
 | |
|                 self._PrintResultDictKeyVals(test_entry, info)
 | |
| 
 | |
|         self._PrintDashLine(width)
 | |
| 
 | |
|         if not self._options.csv:
 | |
|             total_tests = len(tests)
 | |
|             percent_pass = 100 * tests_pass / total_tests
 | |
|             pass_str = '%d/%d (%d%%)' % (tests_pass, total_tests, percent_pass)
 | |
|             print 'Total PASS: ' + self._color.Color(self._color.BOLD, pass_str)
 | |
| 
 | |
|         if self._options.crash_detection:
 | |
|             print ''
 | |
|             if crashes:
 | |
|                 print self._color.Color(self._color.RED,
 | |
|                                         'Crashes detected during testing:')
 | |
|                 self._PrintDashLine(width)
 | |
| 
 | |
|                 for crash_name, crashed_tests in sorted(crashes.iteritems()):
 | |
|                     print self._color.Color(self._color.RED, crash_name)
 | |
|                     for crashed_test in crashed_tests:
 | |
|                         print self._Indent(crashed_test)
 | |
| 
 | |
|                 self._PrintDashLine(width)
 | |
|                 print ('Total unique crashes: ' +
 | |
|                        self._color.Color(self._color.BOLD, str(len(crashes))))
 | |
| 
 | |
|             # Sometimes the builders exit before these buffers are flushed.
 | |
|             sys.stderr.flush()
 | |
|             sys.stdout.flush()
 | |
| 
 | |
|     def Run(self):
 | |
|         """Runs report generation."""
 | |
|         self._CollectAllResults()
 | |
|         if not self._options.just_status_code:
 | |
|             self._GenerateReportText()
 | |
|             if self._options.html:
 | |
|                 print "\nLogging the data into test_report.html file."
 | |
|                 try:
 | |
|                     self.GenerateReportHTML()
 | |
|                 except Exception as e:
 | |
|                     print "Failed to generate HTML report %s" % str(e)
 | |
|         for d in self._GetDedupedResults():
 | |
|             if d['experimental'] and self._options.ignore_experimental_tests:
 | |
|                 continue
 | |
|             if not d['status'] or (
 | |
|                     self._options.crash_detection and d['crashes']):
 | |
|                 sys.exit(1)
 | |
| 
 | |
| 
 | |
| def main():
 | |
|     usage = 'Usage: %prog [options] result-directories...'
 | |
|     parser = optparse.OptionParser(usage=usage)
 | |
|     parser.add_option('--color', dest='color', action='store_true',
 | |
|                       default=_STDOUT_IS_TTY,
 | |
|                       help='Use color for text reports [default if TTY stdout]')
 | |
|     parser.add_option('--no-color', dest='color', action='store_false',
 | |
|                       help='Don\'t use color for text reports')
 | |
|     parser.add_option('--no-crash-detection', dest='crash_detection',
 | |
|                       action='store_false', default=True,
 | |
|                       help='Don\'t report crashes or error out when detected')
 | |
|     parser.add_option('--csv', dest='csv', action='store_true',
 | |
|                       help='Output test result in CSV format.  '
 | |
|                       'Implies --no-debug --no-crash-detection.')
 | |
|     parser.add_option('--html', dest='html', action='store_true',
 | |
|                       help='To generate HTML File.  '
 | |
|                            'Implies --no-debug --no-crash-detection.')
 | |
|     parser.add_option('--html-report-dir', dest='html_report_dir',
 | |
|                       action='store', default=None, help='Path to generate '
 | |
|                                                           'html report')
 | |
|     parser.add_option('--info', dest='info', action='store_true',
 | |
|                       default=False,
 | |
|                       help='Include info keyvals in the report')
 | |
|     parser.add_option('--escape-error', dest='escape_error',
 | |
|                       action='store_true', default=False,
 | |
|                       help='Escape error message text for tools.')
 | |
|     parser.add_option('--perf', dest='perf', action='store_true',
 | |
|                       default=True,
 | |
|                       help='Include perf keyvals in the report [default]')
 | |
|     parser.add_option('--attr', dest='attr', action='store_true',
 | |
|                       default=False,
 | |
|                       help='Include attr keyvals in the report')
 | |
|     parser.add_option('--no-perf', dest='perf', action='store_false',
 | |
|                       help='Don\'t include perf keyvals in the report')
 | |
|     parser.add_option('--sort-chron', dest='sort_chron', action='store_true',
 | |
|                       default=False,
 | |
|                       help='Sort results by datetime instead of by test name.')
 | |
|     parser.add_option('--no-debug', dest='print_debug', action='store_false',
 | |
|                       default=True,
 | |
|                       help='Don\'t print out logs when tests fail.')
 | |
|     parser.add_option('--allow_chrome_crashes',
 | |
|                       dest='allow_chrome_crashes',
 | |
|                       action='store_true', default=False,
 | |
|                       help='Treat Chrome crashes as non-fatal.')
 | |
|     parser.add_option('--ignore_experimental_tests',
 | |
|                       dest='ignore_experimental_tests',
 | |
|                       action='store_true', default=False,
 | |
|                       help='If set, experimental test results will not '
 | |
|                            'influence the exit code.')
 | |
|     parser.add_option('--just_status_code',
 | |
|                       dest='just_status_code',
 | |
|                       action='store_true', default=False,
 | |
|                       help='Skip generating a report, just return status code.')
 | |
| 
 | |
|     (options, args) = parser.parse_args()
 | |
| 
 | |
|     if not args:
 | |
|         parser.print_help()
 | |
|         Die('no result directories provided')
 | |
| 
 | |
|     if options.csv and (options.print_debug or options.crash_detection):
 | |
|         Warning('Forcing --no-debug --no-crash-detection')
 | |
|         options.print_debug = False
 | |
|         options.crash_detection = False
 | |
| 
 | |
|     report_options = ['color', 'csv', 'info', 'escape_error', 'perf', 'attr',
 | |
|                       'sort_chron', 'print_debug', 'html', 'html_report_dir']
 | |
|     if options.just_status_code and any(
 | |
|         getattr(options, opt) for opt in report_options):
 | |
|         Warning('Passed --just_status_code and incompatible options %s' %
 | |
|                 ' '.join(opt for opt in report_options if getattr(options,opt)))
 | |
| 
 | |
|     generator = ReportGenerator(options, args)
 | |
|     generator.Run()
 | |
| 
 | |
| 
 | |
| if __name__ == '__main__':
 | |
|     main()
 |