| Index: generate_test_report.py
|
| diff --git a/generate_test_report.py b/generate_test_report.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..9235482cb9818f2493afbb97d30430933ee884a3
|
| --- /dev/null
|
| +++ b/generate_test_report.py
|
| @@ -0,0 +1,282 @@
|
| +#!/usr/bin/python
|
| +# 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 glob
|
| +import optparse
|
| +import os
|
| +import re
|
| +import sys
|
| +
|
| +
|
| +_STDOUT_IS_TTY = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
|
| +
|
| +
|
| +class Color(object):
|
| + """Conditionally wraps text in ANSI color escape sequences."""
|
| + BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
|
| + BOLD = -1
|
| + COLOR_START = '\033[1;%dm'
|
| + BOLD_START = '\033[1m'
|
| + RESET = '\033[0m'
|
| +
|
| + def __init__(self, enabled=True):
|
| + self._enabled = enabled
|
| +
|
| + def Color(self, color, text):
|
| + """Returns text with conditionally added color escape sequences.
|
| +
|
| + Args:
|
| + color: Text color -- one of the color constants defined in this class.
|
| + text: The text to color.
|
| +
|
| + Returns:
|
| + If self._enabled is False, returns the original text. If it's True,
|
| + returns text with color escape sequences based on the value of color.
|
| + """
|
| + if not self._enabled:
|
| + return text
|
| + if color == self.BOLD:
|
| + start = self.BOLD_START
|
| + else:
|
| + start = self.COLOR_START % (color + 30)
|
| + return start + text + self.RESET
|
| +
|
| +
|
| +def Die(message):
|
| + """Emits a red error message and halts execution.
|
| +
|
| + Args:
|
| + message: The message to be emitted before exiting.
|
| + """
|
| + print Color(_STDOUT_IS_TTY).Color(Color.RED, '\nERROR: ' + message)
|
| + sys.exit(1)
|
| +
|
| +
|
| +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
|
| +
|
| + def __init__(self, options, args):
|
| + self._options = options
|
| + self._args = args
|
| + self._color = Color(options.color)
|
| +
|
| + def _CollectPerf(self, testdir):
|
| + """Parses keyval file under testdir.
|
| +
|
| + If testdir contains a result folder, process the keyval file and return
|
| + a dictionary of perf keyval pairs.
|
| +
|
| + Args:
|
| + testdir: The autoserv test result directory.
|
| +
|
| + Returns:
|
| + 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.
|
| + """
|
| +
|
| + perf = {}
|
| + if not self._options.perf:
|
| + return perf
|
| +
|
| + keyval_file = os.path.join(testdir, 'results', 'keyval')
|
| + if not os.path.isfile(keyval_file):
|
| + return perf
|
| +
|
| + instances = {}
|
| +
|
| + for line in open(keyval_file):
|
| + match = re.search(r'^(.+){perf}=(.+)$', 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
|
| +
|
| + perf[key_inst] = val
|
| +
|
| + return perf
|
| +
|
| + def _CollectResult(self, testdir):
|
| + """Adds results stored under testdir to the self._results dictionary.
|
| +
|
| + If testdir contains 'status.log' or 'status' files, assume it's a test
|
| + result directory and add the results data to the self._results dictionary.
|
| + The test directory name is used as a key into the results dictionary.
|
| +
|
| + Args:
|
| + testdir: The autoserv test result directory.
|
| + """
|
| +
|
| + 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_raw = open(status_file, 'r').read()
|
| + status = 'FAIL'
|
| + if (re.search(r'GOOD.+completed successfully', status_raw) and
|
| + not re.search(r'ABORT|ERROR|FAIL|TEST_NA', status_raw)):
|
| + status = 'PASS'
|
| +
|
| + perf = self._CollectPerf(testdir)
|
| +
|
| + if testdir.startswith(self._options.strip):
|
| + testdir = testdir.replace(self._options.strip, '', 1)
|
| +
|
| + self._results[testdir] = {'status': status,
|
| + 'perf': perf}
|
| +
|
| + def _CollectResultsRec(self, resdir):
|
| + """Recursively collect results into the self._results dictionary.
|
| +
|
| + Args:
|
| + resdir: results/test directory to parse results from and recurse into.
|
| + """
|
| +
|
| + self._CollectResult(resdir)
|
| + for testdir in glob.glob(os.path.join(resdir, '*')):
|
| + self._CollectResultsRec(testdir)
|
| +
|
| + def _CollectResults(self):
|
| + """Parses results into the self._results dictionary.
|
| +
|
| + Initializes a dictionary (self._results) with test folders as keys and
|
| + result data (status, perf keyvals) as values.
|
| + """
|
| + self._results = {}
|
| + for resdir in self._args:
|
| + if not os.path.isdir(resdir):
|
| + Die('\'%s\' does not exist' % resdir)
|
| + self._CollectResultsRec(resdir)
|
| +
|
| + if not self._results:
|
| + Die('no test directories found')
|
| +
|
| + def GetTestColumnWidth(self):
|
| + """Returns the test column width based on the test data.
|
| +
|
| + Aligns the test results by formatting the test directory entry based on
|
| + the longest test directory or perf key string stored in the self._results
|
| + dictionary.
|
| +
|
| + Returns:
|
| + The width for the test columnt.
|
| + """
|
| + width = len(max(self._results, key=len))
|
| + for result in self._results.values():
|
| + perf = result['perf']
|
| + if perf:
|
| + perf_key_width = len(max(perf, key=len))
|
| + width = max(width, perf_key_width + self._KEYVAL_INDENT)
|
| + return width + 1
|
| +
|
| + 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._results.keys()
|
| + tests.sort()
|
| +
|
| + width = self.GetTestColumnWidth()
|
| + line = ''.ljust(width + 5, '-')
|
| +
|
| + tests_pass = 0
|
| + print line
|
| + for test in tests:
|
| + # Emit the test/status entry first
|
| + test_entry = test.ljust(width)
|
| + result = self._results[test]
|
| + status_entry = result['status']
|
| + if status_entry == 'PASS':
|
| + color = Color.GREEN
|
| + tests_pass += 1
|
| + else:
|
| + color = Color.RED
|
| + status_entry = self._color.Color(color, status_entry)
|
| + print test_entry + status_entry
|
| +
|
| + # Emit the perf keyvals entries. There will be no entries if the
|
| + # --no-perf option is specified.
|
| + perf = result['perf']
|
| + perf_keys = perf.keys()
|
| + perf_keys.sort()
|
| +
|
| + for perf_key in perf_keys:
|
| + perf_key_entry = perf_key.ljust(width - self._KEYVAL_INDENT)
|
| + perf_key_entry = perf_key_entry.rjust(width)
|
| + perf_value_entry = self._color.Color(Color.BOLD, perf[perf_key])
|
| + print perf_key_entry + perf_value_entry
|
| +
|
| + print line
|
| +
|
| + 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(Color.BOLD, pass_str)
|
| +
|
| + def Run(self):
|
| + """Runs report generation."""
|
| + self._CollectResults()
|
| + self._GenerateReportText()
|
| +
|
| +
|
| +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('--perf', dest='perf', action='store_true',
|
| + default=True,
|
| + help='Include perf keyvals in the report [default]')
|
| + parser.add_option('--no-perf', dest='perf', action='store_false',
|
| + help='Don\'t include perf keyvals in the report')
|
| + parser.add_option('--strip', dest='strip', type='string', action='store',
|
| + default='results.',
|
| + help='Strip a prefix from test directory names'
|
| + ' [default: \'%default\']')
|
| + parser.add_option('--no-strip', dest='strip', const='', action='store_const',
|
| + help='Don\'t strip a prefix from test directory names')
|
| + (options, args) = parser.parse_args()
|
| +
|
| + if not args:
|
| + parser.print_help()
|
| + Die('no result directories provided')
|
| +
|
| + generator = ReportGenerator(options, args)
|
| + generator.Run()
|
| +
|
| +
|
| +if __name__ == '__main__':
|
| + main()
|
|
|