OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python |
| 2 # Copyright (c) 2010 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 |
| 6 |
| 7 """Parses and displays the contents of one or more autoserv result directories. |
| 8 |
| 9 This script parses the contents of one or more autoserv results folders and |
| 10 generates test reports. |
| 11 """ |
| 12 |
| 13 |
| 14 import glob |
| 15 import optparse |
| 16 import os |
| 17 import re |
| 18 import sys |
| 19 |
| 20 |
| 21 _STDOUT_IS_TTY = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() |
| 22 |
| 23 |
| 24 class Color(object): |
| 25 """Conditionally wraps text in ANSI color escape sequences.""" |
| 26 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) |
| 27 BOLD = -1 |
| 28 COLOR_START = '\033[1;%dm' |
| 29 BOLD_START = '\033[1m' |
| 30 RESET = '\033[0m' |
| 31 |
| 32 def __init__(self, enabled=True): |
| 33 self._enabled = enabled |
| 34 |
| 35 def Color(self, color, text): |
| 36 """Returns text with conditionally added color escape sequences. |
| 37 |
| 38 Args: |
| 39 color: Text color -- one of the color constants defined in this class. |
| 40 text: The text to color. |
| 41 |
| 42 Returns: |
| 43 If self._enabled is False, returns the original text. If it's True, |
| 44 returns text with color escape sequences based on the value of color. |
| 45 """ |
| 46 if not self._enabled: |
| 47 return text |
| 48 if color == self.BOLD: |
| 49 start = self.BOLD_START |
| 50 else: |
| 51 start = self.COLOR_START % (color + 30) |
| 52 return start + text + self.RESET |
| 53 |
| 54 |
| 55 def Die(message): |
| 56 """Emits a red error message and halts execution. |
| 57 |
| 58 Args: |
| 59 message: The message to be emitted before exiting. |
| 60 """ |
| 61 print Color(_STDOUT_IS_TTY).Color(Color.RED, '\nERROR: ' + message) |
| 62 sys.exit(1) |
| 63 |
| 64 |
| 65 class ReportGenerator(object): |
| 66 """Collects and displays data from autoserv results directories. |
| 67 |
| 68 This class collects status and performance data from one or more autoserv |
| 69 result directories and generates test reports. |
| 70 """ |
| 71 |
| 72 _KEYVAL_INDENT = 2 |
| 73 |
| 74 def __init__(self, options, args): |
| 75 self._options = options |
| 76 self._args = args |
| 77 self._color = Color(options.color) |
| 78 |
| 79 def _CollectPerf(self, testdir): |
| 80 """Parses keyval file under testdir. |
| 81 |
| 82 If testdir contains a result folder, process the keyval file and return |
| 83 a dictionary of perf keyval pairs. |
| 84 |
| 85 Args: |
| 86 testdir: The autoserv test result directory. |
| 87 |
| 88 Returns: |
| 89 If the perf option is disabled or the there's no keyval file under |
| 90 testdir, returns an empty dictionary. Otherwise, returns a dictionary of |
| 91 parsed keyvals. Duplicate keys are uniquified by their instance number. |
| 92 """ |
| 93 |
| 94 perf = {} |
| 95 if not self._options.perf: |
| 96 return perf |
| 97 |
| 98 keyval_file = os.path.join(testdir, 'results', 'keyval') |
| 99 if not os.path.isfile(keyval_file): |
| 100 return perf |
| 101 |
| 102 instances = {} |
| 103 |
| 104 for line in open(keyval_file): |
| 105 match = re.search(r'^(.+){perf}=(.+)$', line) |
| 106 if match: |
| 107 key = match.group(1) |
| 108 val = match.group(2) |
| 109 |
| 110 # If the same key name was generated multiple times, uniquify all |
| 111 # instances other than the first one by adding the instance count |
| 112 # to the key name. |
| 113 key_inst = key |
| 114 instance = instances.get(key, 0) |
| 115 if instance: |
| 116 key_inst = '%s{%d}' % (key, instance) |
| 117 instances[key] = instance + 1 |
| 118 |
| 119 perf[key_inst] = val |
| 120 |
| 121 return perf |
| 122 |
| 123 def _CollectResult(self, testdir): |
| 124 """Adds results stored under testdir to the self._results dictionary. |
| 125 |
| 126 If testdir contains 'status.log' or 'status' files, assume it's a test |
| 127 result directory and add the results data to the self._results dictionary. |
| 128 The test directory name is used as a key into the results dictionary. |
| 129 |
| 130 Args: |
| 131 testdir: The autoserv test result directory. |
| 132 """ |
| 133 |
| 134 status_file = os.path.join(testdir, 'status.log') |
| 135 if not os.path.isfile(status_file): |
| 136 status_file = os.path.join(testdir, 'status') |
| 137 if not os.path.isfile(status_file): |
| 138 return |
| 139 |
| 140 status_raw = open(status_file, 'r').read() |
| 141 status = 'FAIL' |
| 142 if (re.search(r'GOOD.+completed successfully', status_raw) and |
| 143 not re.search(r'ABORT|ERROR|FAIL|TEST_NA', status_raw)): |
| 144 status = 'PASS' |
| 145 |
| 146 perf = self._CollectPerf(testdir) |
| 147 |
| 148 if testdir.startswith(self._options.strip): |
| 149 testdir = testdir.replace(self._options.strip, '', 1) |
| 150 |
| 151 self._results[testdir] = {'status': status, |
| 152 'perf': perf} |
| 153 |
| 154 def _CollectResultsRec(self, resdir): |
| 155 """Recursively collect results into the self._results dictionary. |
| 156 |
| 157 Args: |
| 158 resdir: results/test directory to parse results from and recurse into. |
| 159 """ |
| 160 |
| 161 self._CollectResult(resdir) |
| 162 for testdir in glob.glob(os.path.join(resdir, '*')): |
| 163 self._CollectResultsRec(testdir) |
| 164 |
| 165 def _CollectResults(self): |
| 166 """Parses results into the self._results dictionary. |
| 167 |
| 168 Initializes a dictionary (self._results) with test folders as keys and |
| 169 result data (status, perf keyvals) as values. |
| 170 """ |
| 171 self._results = {} |
| 172 for resdir in self._args: |
| 173 if not os.path.isdir(resdir): |
| 174 Die('\'%s\' does not exist' % resdir) |
| 175 self._CollectResultsRec(resdir) |
| 176 |
| 177 if not self._results: |
| 178 Die('no test directories found') |
| 179 |
| 180 def GetTestColumnWidth(self): |
| 181 """Returns the test column width based on the test data. |
| 182 |
| 183 Aligns the test results by formatting the test directory entry based on |
| 184 the longest test directory or perf key string stored in the self._results |
| 185 dictionary. |
| 186 |
| 187 Returns: |
| 188 The width for the test columnt. |
| 189 """ |
| 190 width = len(max(self._results, key=len)) |
| 191 for result in self._results.values(): |
| 192 perf = result['perf'] |
| 193 if perf: |
| 194 perf_key_width = len(max(perf, key=len)) |
| 195 width = max(width, perf_key_width + self._KEYVAL_INDENT) |
| 196 return width + 1 |
| 197 |
| 198 def _GenerateReportText(self): |
| 199 """Prints a result report to stdout. |
| 200 |
| 201 Prints a result table to stdout. Each row of the table contains the test |
| 202 result directory and the test result (PASS, FAIL). If the perf option is |
| 203 enabled, each test entry is followed by perf keyval entries from the test |
| 204 results. |
| 205 """ |
| 206 tests = self._results.keys() |
| 207 tests.sort() |
| 208 |
| 209 width = self.GetTestColumnWidth() |
| 210 line = ''.ljust(width + 5, '-') |
| 211 |
| 212 tests_pass = 0 |
| 213 print line |
| 214 for test in tests: |
| 215 # Emit the test/status entry first |
| 216 test_entry = test.ljust(width) |
| 217 result = self._results[test] |
| 218 status_entry = result['status'] |
| 219 if status_entry == 'PASS': |
| 220 color = Color.GREEN |
| 221 tests_pass += 1 |
| 222 else: |
| 223 color = Color.RED |
| 224 status_entry = self._color.Color(color, status_entry) |
| 225 print test_entry + status_entry |
| 226 |
| 227 # Emit the perf keyvals entries. There will be no entries if the |
| 228 # --no-perf option is specified. |
| 229 perf = result['perf'] |
| 230 perf_keys = perf.keys() |
| 231 perf_keys.sort() |
| 232 |
| 233 for perf_key in perf_keys: |
| 234 perf_key_entry = perf_key.ljust(width - self._KEYVAL_INDENT) |
| 235 perf_key_entry = perf_key_entry.rjust(width) |
| 236 perf_value_entry = self._color.Color(Color.BOLD, perf[perf_key]) |
| 237 print perf_key_entry + perf_value_entry |
| 238 |
| 239 print line |
| 240 |
| 241 total_tests = len(tests) |
| 242 percent_pass = 100 * tests_pass / total_tests |
| 243 pass_str = '%d/%d (%d%%)' % (tests_pass, total_tests, percent_pass) |
| 244 print 'Total PASS: ' + self._color.Color(Color.BOLD, pass_str) |
| 245 |
| 246 def Run(self): |
| 247 """Runs report generation.""" |
| 248 self._CollectResults() |
| 249 self._GenerateReportText() |
| 250 |
| 251 |
| 252 def main(): |
| 253 usage = 'Usage: %prog [options] result-directories...' |
| 254 parser = optparse.OptionParser(usage=usage) |
| 255 parser.add_option('--color', dest='color', action='store_true', |
| 256 default=_STDOUT_IS_TTY, |
| 257 help='Use color for text reports [default if TTY stdout]') |
| 258 parser.add_option('--no-color', dest='color', action='store_false', |
| 259 help='Don\'t use color for text reports') |
| 260 parser.add_option('--perf', dest='perf', action='store_true', |
| 261 default=True, |
| 262 help='Include perf keyvals in the report [default]') |
| 263 parser.add_option('--no-perf', dest='perf', action='store_false', |
| 264 help='Don\'t include perf keyvals in the report') |
| 265 parser.add_option('--strip', dest='strip', type='string', action='store', |
| 266 default='results.', |
| 267 help='Strip a prefix from test directory names' |
| 268 ' [default: \'%default\']') |
| 269 parser.add_option('--no-strip', dest='strip', const='', action='store_const', |
| 270 help='Don\'t strip a prefix from test directory names') |
| 271 (options, args) = parser.parse_args() |
| 272 |
| 273 if not args: |
| 274 parser.print_help() |
| 275 Die('no result directories provided') |
| 276 |
| 277 generator = ReportGenerator(options, args) |
| 278 generator.Run() |
| 279 |
| 280 |
| 281 if __name__ == '__main__': |
| 282 main() |
OLD | NEW |