| #!/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() |