1# Copyright 2012 the V8 project authors. All rights reserved. 2# Redistribution and use in source and binary forms, with or without 3# modification, are permitted provided that the following conditions are 4# met: 5# 6# * Redistributions of source code must retain the above copyright 7# notice, this list of conditions and the following disclaimer. 8# * Redistributions in binary form must reproduce the above 9# copyright notice, this list of conditions and the following 10# disclaimer in the documentation and/or other materials provided 11# with the distribution. 12# * Neither the name of Google Inc. nor the names of its 13# contributors may be used to endorse or promote products derived 14# from this software without specific prior written permission. 15# 16# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 28 29from functools import wraps 30import json 31import os 32import sys 33import time 34 35from . import execution 36from . import junit_output 37 38 39ABS_PATH_PREFIX = os.getcwd() + os.sep 40 41 42class ProgressIndicator(object): 43 44 def __init__(self): 45 self.runner = None 46 47 def SetRunner(self, runner): 48 self.runner = runner 49 50 def Starting(self): 51 pass 52 53 def Done(self): 54 pass 55 56 def AboutToRun(self, test): 57 pass 58 59 def HasRun(self, test, has_unexpected_output): 60 pass 61 62 def Heartbeat(self): 63 pass 64 65 def PrintFailureHeader(self, test): 66 if test.suite.IsNegativeTest(test): 67 negative_marker = '[negative] ' 68 else: 69 negative_marker = '' 70 print "=== %(label)s %(negative)s===" % { 71 'label': test.GetLabel(), 72 'negative': negative_marker 73 } 74 75 def _EscapeCommand(self, test): 76 command = execution.GetCommand(test, self.runner.context) 77 parts = [] 78 for part in command: 79 if ' ' in part: 80 # Escape spaces. We may need to escape more characters for this 81 # to work properly. 82 parts.append('"%s"' % part) 83 else: 84 parts.append(part) 85 return " ".join(parts) 86 87 88class IndicatorNotifier(object): 89 """Holds a list of progress indicators and notifies them all on events.""" 90 def __init__(self): 91 self.indicators = [] 92 93 def Register(self, indicator): 94 self.indicators.append(indicator) 95 96 97# Forge all generic event-dispatching methods in IndicatorNotifier, which are 98# part of the ProgressIndicator interface. 99for func_name in ProgressIndicator.__dict__: 100 func = getattr(ProgressIndicator, func_name) 101 if callable(func) and not func.__name__.startswith('_'): 102 def wrap_functor(f): 103 @wraps(f) 104 def functor(self, *args, **kwargs): 105 """Generic event dispatcher.""" 106 for indicator in self.indicators: 107 getattr(indicator, f.__name__)(*args, **kwargs) 108 return functor 109 setattr(IndicatorNotifier, func_name, wrap_functor(func)) 110 111 112class SimpleProgressIndicator(ProgressIndicator): 113 """Abstract base class for {Verbose,Dots}ProgressIndicator""" 114 115 def Starting(self): 116 print 'Running %i tests' % self.runner.total 117 118 def Done(self): 119 print 120 for failed in self.runner.failed: 121 self.PrintFailureHeader(failed) 122 if failed.output.stderr: 123 print "--- stderr ---" 124 print failed.output.stderr.strip() 125 if failed.output.stdout: 126 print "--- stdout ---" 127 print failed.output.stdout.strip() 128 print "Command: %s" % self._EscapeCommand(failed) 129 if failed.output.HasCrashed(): 130 print "exit code: %d" % failed.output.exit_code 131 print "--- CRASHED ---" 132 if failed.output.HasTimedOut(): 133 print "--- TIMEOUT ---" 134 if len(self.runner.failed) == 0: 135 print "===" 136 print "=== All tests succeeded" 137 print "===" 138 else: 139 print 140 print "===" 141 print "=== %i tests failed" % len(self.runner.failed) 142 if self.runner.crashed > 0: 143 print "=== %i tests CRASHED" % self.runner.crashed 144 print "===" 145 146 147class VerboseProgressIndicator(SimpleProgressIndicator): 148 149 def AboutToRun(self, test): 150 print 'Starting %s...' % test.GetLabel() 151 sys.stdout.flush() 152 153 def HasRun(self, test, has_unexpected_output): 154 if has_unexpected_output: 155 if test.output.HasCrashed(): 156 outcome = 'CRASH' 157 else: 158 outcome = 'FAIL' 159 else: 160 outcome = 'pass' 161 print 'Done running %s: %s' % (test.GetLabel(), outcome) 162 sys.stdout.flush() 163 164 def Heartbeat(self): 165 print 'Still working...' 166 sys.stdout.flush() 167 168 169class DotsProgressIndicator(SimpleProgressIndicator): 170 171 def HasRun(self, test, has_unexpected_output): 172 total = self.runner.succeeded + len(self.runner.failed) 173 if (total > 1) and (total % 50 == 1): 174 sys.stdout.write('\n') 175 if has_unexpected_output: 176 if test.output.HasCrashed(): 177 sys.stdout.write('C') 178 sys.stdout.flush() 179 elif test.output.HasTimedOut(): 180 sys.stdout.write('T') 181 sys.stdout.flush() 182 else: 183 sys.stdout.write('F') 184 sys.stdout.flush() 185 else: 186 sys.stdout.write('.') 187 sys.stdout.flush() 188 189 190class CompactProgressIndicator(ProgressIndicator): 191 """Abstract base class for {Color,Monochrome}ProgressIndicator""" 192 193 def __init__(self, templates): 194 super(CompactProgressIndicator, self).__init__() 195 self.templates = templates 196 self.last_status_length = 0 197 self.start_time = time.time() 198 199 def Done(self): 200 self.PrintProgress('Done') 201 print "" # Line break. 202 203 def AboutToRun(self, test): 204 self.PrintProgress(test.GetLabel()) 205 206 def HasRun(self, test, has_unexpected_output): 207 if has_unexpected_output: 208 self.ClearLine(self.last_status_length) 209 self.PrintFailureHeader(test) 210 stdout = test.output.stdout.strip() 211 if len(stdout): 212 print self.templates['stdout'] % stdout 213 stderr = test.output.stderr.strip() 214 if len(stderr): 215 print self.templates['stderr'] % stderr 216 print "Command: %s" % self._EscapeCommand(test) 217 if test.output.HasCrashed(): 218 print "exit code: %d" % test.output.exit_code 219 print "--- CRASHED ---" 220 if test.output.HasTimedOut(): 221 print "--- TIMEOUT ---" 222 223 def Truncate(self, string, length): 224 if length and (len(string) > (length - 3)): 225 return string[:(length - 3)] + "..." 226 else: 227 return string 228 229 def PrintProgress(self, name): 230 self.ClearLine(self.last_status_length) 231 elapsed = time.time() - self.start_time 232 progress = 0 if not self.runner.total else ( 233 ((self.runner.total - self.runner.remaining) * 100) // 234 self.runner.total) 235 status = self.templates['status_line'] % { 236 'passed': self.runner.succeeded, 237 'progress': progress, 238 'failed': len(self.runner.failed), 239 'test': name, 240 'mins': int(elapsed) / 60, 241 'secs': int(elapsed) % 60 242 } 243 status = self.Truncate(status, 78) 244 self.last_status_length = len(status) 245 print status, 246 sys.stdout.flush() 247 248 249class ColorProgressIndicator(CompactProgressIndicator): 250 251 def __init__(self): 252 templates = { 253 'status_line': ("[%(mins)02i:%(secs)02i|" 254 "\033[34m%%%(progress) 4d\033[0m|" 255 "\033[32m+%(passed) 4d\033[0m|" 256 "\033[31m-%(failed) 4d\033[0m]: %(test)s"), 257 'stdout': "\033[1m%s\033[0m", 258 'stderr': "\033[31m%s\033[0m", 259 } 260 super(ColorProgressIndicator, self).__init__(templates) 261 262 def ClearLine(self, last_line_length): 263 print "\033[1K\r", 264 265 266class MonochromeProgressIndicator(CompactProgressIndicator): 267 268 def __init__(self): 269 templates = { 270 'status_line': ("[%(mins)02i:%(secs)02i|%%%(progress) 4d|" 271 "+%(passed) 4d|-%(failed) 4d]: %(test)s"), 272 'stdout': '%s', 273 'stderr': '%s', 274 } 275 super(MonochromeProgressIndicator, self).__init__(templates) 276 277 def ClearLine(self, last_line_length): 278 print ("\r" + (" " * last_line_length) + "\r"), 279 280 281class JUnitTestProgressIndicator(ProgressIndicator): 282 283 def __init__(self, junitout, junittestsuite): 284 self.outputter = junit_output.JUnitTestOutput(junittestsuite) 285 if junitout: 286 self.outfile = open(junitout, "w") 287 else: 288 self.outfile = sys.stdout 289 290 def Done(self): 291 self.outputter.FinishAndWrite(self.outfile) 292 if self.outfile != sys.stdout: 293 self.outfile.close() 294 295 def HasRun(self, test, has_unexpected_output): 296 fail_text = "" 297 if has_unexpected_output: 298 stdout = test.output.stdout.strip() 299 if len(stdout): 300 fail_text += "stdout:\n%s\n" % stdout 301 stderr = test.output.stderr.strip() 302 if len(stderr): 303 fail_text += "stderr:\n%s\n" % stderr 304 fail_text += "Command: %s" % self._EscapeCommand(test) 305 if test.output.HasCrashed(): 306 fail_text += "exit code: %d\n--- CRASHED ---" % test.output.exit_code 307 if test.output.HasTimedOut(): 308 fail_text += "--- TIMEOUT ---" 309 self.outputter.HasRunTest( 310 [test.GetLabel()] + self.runner.context.mode_flags + test.flags, 311 test.duration, 312 fail_text) 313 314 315class JsonTestProgressIndicator(ProgressIndicator): 316 317 def __init__(self, json_test_results, arch, mode, random_seed): 318 self.json_test_results = json_test_results 319 self.arch = arch 320 self.mode = mode 321 self.random_seed = random_seed 322 self.results = [] 323 self.tests = [] 324 325 def Done(self): 326 complete_results = [] 327 if os.path.exists(self.json_test_results): 328 with open(self.json_test_results, "r") as f: 329 # Buildbot might start out with an empty file. 330 complete_results = json.loads(f.read() or "[]") 331 332 # Sort tests by duration. 333 timed_tests = [t for t in self.tests if t.duration is not None] 334 timed_tests.sort(lambda a, b: cmp(b.duration, a.duration)) 335 slowest_tests = [ 336 { 337 "name": test.GetLabel(), 338 "flags": test.flags, 339 "command": self._EscapeCommand(test).replace(ABS_PATH_PREFIX, ""), 340 "duration": test.duration, 341 } for test in timed_tests[:20] 342 ] 343 344 complete_results.append({ 345 "arch": self.arch, 346 "mode": self.mode, 347 "results": self.results, 348 "slowest_tests": slowest_tests, 349 }) 350 351 with open(self.json_test_results, "w") as f: 352 f.write(json.dumps(complete_results)) 353 354 def HasRun(self, test, has_unexpected_output): 355 # Buffer all tests for sorting the durations in the end. 356 self.tests.append(test) 357 if not has_unexpected_output: 358 # Omit tests that run as expected. Passing tests of reruns after failures 359 # will have unexpected_output to be reported here has well. 360 return 361 362 self.results.append({ 363 "name": test.GetLabel(), 364 "flags": test.flags, 365 "command": self._EscapeCommand(test).replace(ABS_PATH_PREFIX, ""), 366 "run": test.run, 367 "stdout": test.output.stdout, 368 "stderr": test.output.stderr, 369 "exit_code": test.output.exit_code, 370 "result": test.suite.GetOutcome(test), 371 "expected": list(test.outcomes or ["PASS"]), 372 "duration": test.duration, 373 374 # TODO(machenbach): This stores only the global random seed from the 375 # context and not possible overrides when using random-seed stress. 376 "random_seed": self.random_seed, 377 "target_name": test.suite.shell(), 378 "variant": test.variant, 379 }) 380 381 382PROGRESS_INDICATORS = { 383 'verbose': VerboseProgressIndicator, 384 'dots': DotsProgressIndicator, 385 'color': ColorProgressIndicator, 386 'mono': MonochromeProgressIndicator 387} 388