| Steve Block | a7e24c1 | 2009-10-30 11:49:00 +0000 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # |
| 3 | # Copyright 2008 the V8 project authors. All rights reserved. |
| 4 | # Redistribution and use in source and binary forms, with or without |
| 5 | # modification, are permitted provided that the following conditions are |
| 6 | # met: |
| 7 | # |
| 8 | # * Redistributions of source code must retain the above copyright |
| 9 | # notice, this list of conditions and the following disclaimer. |
| 10 | # * Redistributions in binary form must reproduce the above |
| 11 | # copyright notice, this list of conditions and the following |
| 12 | # disclaimer in the documentation and/or other materials provided |
| 13 | # with the distribution. |
| 14 | # * Neither the name of Google Inc. nor the names of its |
| 15 | # contributors may be used to endorse or promote products derived |
| 16 | # from this software without specific prior written permission. |
| 17 | # |
| 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 19 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| 20 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| 21 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| 22 | # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 23 | # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| 24 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 25 | # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 26 | # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 27 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 28 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 29 | |
| 30 | |
| 31 | import imp |
| 32 | import optparse |
| 33 | import os |
| 34 | from os.path import join, dirname, abspath, basename, isdir, exists |
| 35 | import platform |
| 36 | import re |
| 37 | import signal |
| 38 | import subprocess |
| 39 | import sys |
| 40 | import tempfile |
| 41 | import time |
| 42 | import threading |
| 43 | import utils |
| 44 | from Queue import Queue, Empty |
| 45 | |
| 46 | |
| 47 | VERBOSE = False |
| 48 | |
| 49 | |
| 50 | # --------------------------------------------- |
| 51 | # --- P r o g r e s s I n d i c a t o r s --- |
| 52 | # --------------------------------------------- |
| 53 | |
| 54 | |
| 55 | class ProgressIndicator(object): |
| 56 | |
| 57 | def __init__(self, cases): |
| 58 | self.cases = cases |
| 59 | self.queue = Queue(len(cases)) |
| 60 | for case in cases: |
| 61 | self.queue.put_nowait(case) |
| 62 | self.succeeded = 0 |
| 63 | self.remaining = len(cases) |
| 64 | self.total = len(cases) |
| 65 | self.failed = [ ] |
| 66 | self.crashed = 0 |
| 67 | self.terminate = False |
| 68 | self.lock = threading.Lock() |
| 69 | |
| 70 | def PrintFailureHeader(self, test): |
| 71 | if test.IsNegative(): |
| 72 | negative_marker = '[negative] ' |
| 73 | else: |
| 74 | negative_marker = '' |
| 75 | print "=== %(label)s %(negative)s===" % { |
| 76 | 'label': test.GetLabel(), |
| 77 | 'negative': negative_marker |
| 78 | } |
| 79 | print "Path: %s" % "/".join(test.path) |
| 80 | |
| 81 | def Run(self, tasks): |
| 82 | self.Starting() |
| 83 | threads = [] |
| 84 | # Spawn N-1 threads and then use this thread as the last one. |
| 85 | # That way -j1 avoids threading altogether which is a nice fallback |
| 86 | # in case of threading problems. |
| 87 | for i in xrange(tasks - 1): |
| 88 | thread = threading.Thread(target=self.RunSingle, args=[]) |
| 89 | threads.append(thread) |
| 90 | thread.start() |
| 91 | try: |
| 92 | self.RunSingle() |
| 93 | # Wait for the remaining threads |
| 94 | for thread in threads: |
| 95 | # Use a timeout so that signals (ctrl-c) will be processed. |
| 96 | thread.join(timeout=10000000) |
| 97 | except Exception, e: |
| 98 | # If there's an exception we schedule an interruption for any |
| 99 | # remaining threads. |
| 100 | self.terminate = True |
| 101 | # ...and then reraise the exception to bail out |
| 102 | raise |
| 103 | self.Done() |
| 104 | return not self.failed |
| 105 | |
| 106 | def RunSingle(self): |
| 107 | while not self.terminate: |
| 108 | try: |
| 109 | test = self.queue.get_nowait() |
| 110 | except Empty: |
| 111 | return |
| 112 | case = test.case |
| 113 | self.lock.acquire() |
| 114 | self.AboutToRun(case) |
| 115 | self.lock.release() |
| 116 | try: |
| 117 | start = time.time() |
| 118 | output = case.Run() |
| 119 | case.duration = (time.time() - start) |
| 120 | except IOError, e: |
| 121 | assert self.terminate |
| 122 | return |
| 123 | if self.terminate: |
| 124 | return |
| 125 | self.lock.acquire() |
| 126 | if output.UnexpectedOutput(): |
| 127 | self.failed.append(output) |
| 128 | if output.HasCrashed(): |
| 129 | self.crashed += 1 |
| 130 | else: |
| 131 | self.succeeded += 1 |
| 132 | self.remaining -= 1 |
| 133 | self.HasRun(output) |
| 134 | self.lock.release() |
| 135 | |
| 136 | |
| 137 | def EscapeCommand(command): |
| 138 | parts = [] |
| 139 | for part in command: |
| 140 | if ' ' in part: |
| 141 | # Escape spaces. We may need to escape more characters for this |
| 142 | # to work properly. |
| 143 | parts.append('"%s"' % part) |
| 144 | else: |
| 145 | parts.append(part) |
| 146 | return " ".join(parts) |
| 147 | |
| 148 | |
| 149 | class SimpleProgressIndicator(ProgressIndicator): |
| 150 | |
| 151 | def Starting(self): |
| 152 | print 'Running %i tests' % len(self.cases) |
| 153 | |
| 154 | def Done(self): |
| 155 | print |
| 156 | for failed in self.failed: |
| 157 | self.PrintFailureHeader(failed.test) |
| 158 | if failed.output.stderr: |
| 159 | print "--- stderr ---" |
| 160 | print failed.output.stderr.strip() |
| 161 | if failed.output.stdout: |
| 162 | print "--- stdout ---" |
| 163 | print failed.output.stdout.strip() |
| 164 | print "Command: %s" % EscapeCommand(failed.command) |
| 165 | if failed.HasCrashed(): |
| 166 | print "--- CRASHED ---" |
| 167 | if failed.HasTimedOut(): |
| 168 | print "--- TIMEOUT ---" |
| 169 | if len(self.failed) == 0: |
| 170 | print "===" |
| 171 | print "=== All tests succeeded" |
| 172 | print "===" |
| 173 | else: |
| 174 | print |
| 175 | print "===" |
| 176 | print "=== %i tests failed" % len(self.failed) |
| 177 | if self.crashed > 0: |
| 178 | print "=== %i tests CRASHED" % self.crashed |
| 179 | print "===" |
| 180 | |
| 181 | |
| 182 | class VerboseProgressIndicator(SimpleProgressIndicator): |
| 183 | |
| 184 | def AboutToRun(self, case): |
| 185 | print 'Starting %s...' % case.GetLabel() |
| 186 | sys.stdout.flush() |
| 187 | |
| 188 | def HasRun(self, output): |
| 189 | if output.UnexpectedOutput(): |
| 190 | if output.HasCrashed(): |
| 191 | outcome = 'CRASH' |
| 192 | else: |
| 193 | outcome = 'FAIL' |
| 194 | else: |
| 195 | outcome = 'pass' |
| 196 | print 'Done running %s: %s' % (output.test.GetLabel(), outcome) |
| 197 | |
| 198 | |
| 199 | class DotsProgressIndicator(SimpleProgressIndicator): |
| 200 | |
| 201 | def AboutToRun(self, case): |
| 202 | pass |
| 203 | |
| 204 | def HasRun(self, output): |
| 205 | total = self.succeeded + len(self.failed) |
| 206 | if (total > 1) and (total % 50 == 1): |
| 207 | sys.stdout.write('\n') |
| 208 | if output.UnexpectedOutput(): |
| 209 | if output.HasCrashed(): |
| 210 | sys.stdout.write('C') |
| 211 | sys.stdout.flush() |
| 212 | elif output.HasTimedOut(): |
| 213 | sys.stdout.write('T') |
| 214 | sys.stdout.flush() |
| 215 | else: |
| 216 | sys.stdout.write('F') |
| 217 | sys.stdout.flush() |
| 218 | else: |
| 219 | sys.stdout.write('.') |
| 220 | sys.stdout.flush() |
| 221 | |
| 222 | |
| 223 | class CompactProgressIndicator(ProgressIndicator): |
| 224 | |
| 225 | def __init__(self, cases, templates): |
| 226 | super(CompactProgressIndicator, self).__init__(cases) |
| 227 | self.templates = templates |
| 228 | self.last_status_length = 0 |
| 229 | self.start_time = time.time() |
| 230 | |
| 231 | def Starting(self): |
| 232 | pass |
| 233 | |
| 234 | def Done(self): |
| 235 | self.PrintProgress('Done') |
| 236 | |
| 237 | def AboutToRun(self, case): |
| 238 | self.PrintProgress(case.GetLabel()) |
| 239 | |
| 240 | def HasRun(self, output): |
| 241 | if output.UnexpectedOutput(): |
| 242 | self.ClearLine(self.last_status_length) |
| 243 | self.PrintFailureHeader(output.test) |
| 244 | stdout = output.output.stdout.strip() |
| 245 | if len(stdout): |
| 246 | print self.templates['stdout'] % stdout |
| 247 | stderr = output.output.stderr.strip() |
| 248 | if len(stderr): |
| 249 | print self.templates['stderr'] % stderr |
| 250 | print "Command: %s" % EscapeCommand(output.command) |
| 251 | if output.HasCrashed(): |
| 252 | print "--- CRASHED ---" |
| 253 | if output.HasTimedOut(): |
| 254 | print "--- TIMEOUT ---" |
| 255 | |
| 256 | def Truncate(self, str, length): |
| 257 | if length and (len(str) > (length - 3)): |
| 258 | return str[:(length-3)] + "..." |
| 259 | else: |
| 260 | return str |
| 261 | |
| 262 | def PrintProgress(self, name): |
| 263 | self.ClearLine(self.last_status_length) |
| 264 | elapsed = time.time() - self.start_time |
| 265 | status = self.templates['status_line'] % { |
| 266 | 'passed': self.succeeded, |
| 267 | 'remaining': (((self.total - self.remaining) * 100) // self.total), |
| 268 | 'failed': len(self.failed), |
| 269 | 'test': name, |
| 270 | 'mins': int(elapsed) / 60, |
| 271 | 'secs': int(elapsed) % 60 |
| 272 | } |
| 273 | status = self.Truncate(status, 78) |
| 274 | self.last_status_length = len(status) |
| 275 | print status, |
| 276 | sys.stdout.flush() |
| 277 | |
| 278 | |
| 279 | class ColorProgressIndicator(CompactProgressIndicator): |
| 280 | |
| 281 | def __init__(self, cases): |
| 282 | templates = { |
| 283 | 'status_line': "[%(mins)02i:%(secs)02i|\033[34m%%%(remaining) 4d\033[0m|\033[32m+%(passed) 4d\033[0m|\033[31m-%(failed) 4d\033[0m]: %(test)s", |
| 284 | 'stdout': "\033[1m%s\033[0m", |
| 285 | 'stderr': "\033[31m%s\033[0m", |
| 286 | } |
| 287 | super(ColorProgressIndicator, self).__init__(cases, templates) |
| 288 | |
| 289 | def ClearLine(self, last_line_length): |
| 290 | print "\033[1K\r", |
| 291 | |
| 292 | |
| 293 | class MonochromeProgressIndicator(CompactProgressIndicator): |
| 294 | |
| 295 | def __init__(self, cases): |
| 296 | templates = { |
| 297 | 'status_line': "[%(mins)02i:%(secs)02i|%%%(remaining) 4d|+%(passed) 4d|-%(failed) 4d]: %(test)s", |
| 298 | 'stdout': '%s', |
| 299 | 'stderr': '%s', |
| 300 | 'clear': lambda last_line_length: ("\r" + (" " * last_line_length) + "\r"), |
| 301 | 'max_length': 78 |
| 302 | } |
| 303 | super(MonochromeProgressIndicator, self).__init__(cases, templates) |
| 304 | |
| 305 | def ClearLine(self, last_line_length): |
| 306 | print ("\r" + (" " * last_line_length) + "\r"), |
| 307 | |
| 308 | |
| 309 | PROGRESS_INDICATORS = { |
| 310 | 'verbose': VerboseProgressIndicator, |
| 311 | 'dots': DotsProgressIndicator, |
| 312 | 'color': ColorProgressIndicator, |
| 313 | 'mono': MonochromeProgressIndicator |
| 314 | } |
| 315 | |
| 316 | |
| 317 | # ------------------------- |
| 318 | # --- F r a m e w o r k --- |
| 319 | # ------------------------- |
| 320 | |
| 321 | |
| 322 | class CommandOutput(object): |
| 323 | |
| 324 | def __init__(self, exit_code, timed_out, stdout, stderr): |
| 325 | self.exit_code = exit_code |
| 326 | self.timed_out = timed_out |
| 327 | self.stdout = stdout |
| 328 | self.stderr = stderr |
| Steve Block | 3ce2e20 | 2009-11-05 08:53:23 +0000 | [diff] [blame^] | 329 | self.failed = None |
| Steve Block | a7e24c1 | 2009-10-30 11:49:00 +0000 | [diff] [blame] | 330 | |
| 331 | |
| 332 | class TestCase(object): |
| 333 | |
| 334 | def __init__(self, context, path): |
| 335 | self.path = path |
| 336 | self.context = context |
| Steve Block | a7e24c1 | 2009-10-30 11:49:00 +0000 | [diff] [blame] | 337 | self.duration = None |
| 338 | |
| 339 | def IsNegative(self): |
| 340 | return False |
| 341 | |
| 342 | def CompareTime(self, other): |
| 343 | return cmp(other.duration, self.duration) |
| 344 | |
| 345 | def DidFail(self, output): |
| Steve Block | 3ce2e20 | 2009-11-05 08:53:23 +0000 | [diff] [blame^] | 346 | if output.failed is None: |
| 347 | output.failed = self.IsFailureOutput(output) |
| 348 | return output.failed |
| Steve Block | a7e24c1 | 2009-10-30 11:49:00 +0000 | [diff] [blame] | 349 | |
| 350 | def IsFailureOutput(self, output): |
| 351 | return output.exit_code != 0 |
| 352 | |
| 353 | def GetSource(self): |
| 354 | return "(no source available)" |
| 355 | |
| 356 | def RunCommand(self, command): |
| 357 | full_command = self.context.processor(command) |
| 358 | output = Execute(full_command, self.context, self.context.timeout) |
| 359 | self.Cleanup() |
| 360 | return TestOutput(self, full_command, output) |
| 361 | |
| 362 | def Run(self): |
| 363 | return self.RunCommand(self.GetCommand()) |
| 364 | |
| 365 | def Cleanup(self): |
| 366 | return |
| 367 | |
| 368 | |
| 369 | class TestOutput(object): |
| 370 | |
| 371 | def __init__(self, test, command, output): |
| 372 | self.test = test |
| 373 | self.command = command |
| 374 | self.output = output |
| 375 | |
| 376 | def UnexpectedOutput(self): |
| 377 | if self.HasCrashed(): |
| 378 | outcome = CRASH |
| 379 | elif self.HasTimedOut(): |
| 380 | outcome = TIMEOUT |
| 381 | elif self.HasFailed(): |
| 382 | outcome = FAIL |
| 383 | else: |
| 384 | outcome = PASS |
| 385 | return not outcome in self.test.outcomes |
| 386 | |
| 387 | def HasCrashed(self): |
| 388 | if utils.IsWindows(): |
| 389 | return 0x80000000 & self.output.exit_code and not (0x3FFFFF00 & self.output.exit_code) |
| 390 | else: |
| 391 | # Timed out tests will have exit_code -signal.SIGTERM. |
| 392 | if self.output.timed_out: |
| 393 | return False |
| 394 | return self.output.exit_code < 0 and \ |
| 395 | self.output.exit_code != -signal.SIGABRT |
| 396 | |
| 397 | def HasTimedOut(self): |
| 398 | return self.output.timed_out; |
| 399 | |
| 400 | def HasFailed(self): |
| 401 | execution_failed = self.test.DidFail(self.output) |
| 402 | if self.test.IsNegative(): |
| 403 | return not execution_failed |
| 404 | else: |
| 405 | return execution_failed |
| 406 | |
| 407 | |
| 408 | def KillProcessWithID(pid): |
| 409 | if utils.IsWindows(): |
| 410 | os.popen('taskkill /T /F /PID %d' % pid) |
| 411 | else: |
| 412 | os.kill(pid, signal.SIGTERM) |
| 413 | |
| 414 | |
| 415 | MAX_SLEEP_TIME = 0.1 |
| 416 | INITIAL_SLEEP_TIME = 0.0001 |
| 417 | SLEEP_TIME_FACTOR = 1.25 |
| 418 | |
| 419 | SEM_INVALID_VALUE = -1 |
| 420 | SEM_NOGPFAULTERRORBOX = 0x0002 # Microsoft Platform SDK WinBase.h |
| 421 | |
| 422 | def Win32SetErrorMode(mode): |
| 423 | prev_error_mode = SEM_INVALID_VALUE |
| 424 | try: |
| 425 | import ctypes |
| 426 | prev_error_mode = ctypes.windll.kernel32.SetErrorMode(mode); |
| 427 | except ImportError: |
| 428 | pass |
| 429 | return prev_error_mode |
| 430 | |
| 431 | def RunProcess(context, timeout, args, **rest): |
| 432 | if context.verbose: print "#", " ".join(args) |
| 433 | popen_args = args |
| 434 | prev_error_mode = SEM_INVALID_VALUE; |
| 435 | if utils.IsWindows(): |
| 436 | popen_args = '"' + subprocess.list2cmdline(args) + '"' |
| 437 | if context.suppress_dialogs: |
| 438 | # Try to change the error mode to avoid dialogs on fatal errors. Don't |
| 439 | # touch any existing error mode flags by merging the existing error mode. |
| 440 | # See http://blogs.msdn.com/oldnewthing/archive/2004/07/27/198410.aspx. |
| 441 | error_mode = SEM_NOGPFAULTERRORBOX; |
| 442 | prev_error_mode = Win32SetErrorMode(error_mode); |
| 443 | Win32SetErrorMode(error_mode | prev_error_mode); |
| 444 | process = subprocess.Popen( |
| 445 | shell = utils.IsWindows(), |
| 446 | args = popen_args, |
| 447 | **rest |
| 448 | ) |
| 449 | if utils.IsWindows() and context.suppress_dialogs and prev_error_mode != SEM_INVALID_VALUE: |
| 450 | Win32SetErrorMode(prev_error_mode) |
| 451 | # Compute the end time - if the process crosses this limit we |
| 452 | # consider it timed out. |
| 453 | if timeout is None: end_time = None |
| 454 | else: end_time = time.time() + timeout |
| 455 | timed_out = False |
| 456 | # Repeatedly check the exit code from the process in a |
| 457 | # loop and keep track of whether or not it times out. |
| 458 | exit_code = None |
| 459 | sleep_time = INITIAL_SLEEP_TIME |
| 460 | while exit_code is None: |
| 461 | if (not end_time is None) and (time.time() >= end_time): |
| 462 | # Kill the process and wait for it to exit. |
| 463 | KillProcessWithID(process.pid) |
| 464 | exit_code = process.wait() |
| 465 | timed_out = True |
| 466 | else: |
| 467 | exit_code = process.poll() |
| 468 | time.sleep(sleep_time) |
| 469 | sleep_time = sleep_time * SLEEP_TIME_FACTOR |
| 470 | if sleep_time > MAX_SLEEP_TIME: |
| 471 | sleep_time = MAX_SLEEP_TIME |
| 472 | return (process, exit_code, timed_out) |
| 473 | |
| 474 | |
| 475 | def PrintError(str): |
| 476 | sys.stderr.write(str) |
| 477 | sys.stderr.write('\n') |
| 478 | |
| 479 | |
| 480 | def CheckedUnlink(name): |
| 481 | try: |
| 482 | os.unlink(name) |
| 483 | except OSError, e: |
| 484 | PrintError("os.unlink() " + str(e)) |
| 485 | |
| 486 | |
| 487 | def Execute(args, context, timeout=None): |
| 488 | (fd_out, outname) = tempfile.mkstemp() |
| 489 | (fd_err, errname) = tempfile.mkstemp() |
| 490 | (process, exit_code, timed_out) = RunProcess( |
| 491 | context, |
| 492 | timeout, |
| 493 | args = args, |
| 494 | stdout = fd_out, |
| 495 | stderr = fd_err, |
| 496 | ) |
| 497 | os.close(fd_out) |
| 498 | os.close(fd_err) |
| 499 | output = file(outname).read() |
| 500 | errors = file(errname).read() |
| 501 | CheckedUnlink(outname) |
| 502 | CheckedUnlink(errname) |
| 503 | return CommandOutput(exit_code, timed_out, output, errors) |
| 504 | |
| 505 | |
| 506 | def ExecuteNoCapture(args, context, timeout=None): |
| 507 | (process, exit_code, timed_out) = RunProcess( |
| 508 | context, |
| 509 | timeout, |
| 510 | args = args, |
| 511 | ) |
| 512 | return CommandOutput(exit_code, False, "", "") |
| 513 | |
| 514 | |
| 515 | def CarCdr(path): |
| 516 | if len(path) == 0: |
| 517 | return (None, [ ]) |
| 518 | else: |
| 519 | return (path[0], path[1:]) |
| 520 | |
| 521 | |
| 522 | class TestConfiguration(object): |
| 523 | |
| 524 | def __init__(self, context, root): |
| 525 | self.context = context |
| 526 | self.root = root |
| 527 | |
| 528 | def Contains(self, path, file): |
| 529 | if len(path) > len(file): |
| 530 | return False |
| 531 | for i in xrange(len(path)): |
| 532 | if not path[i].match(file[i]): |
| 533 | return False |
| 534 | return True |
| 535 | |
| 536 | def GetTestStatus(self, sections, defs): |
| 537 | pass |
| 538 | |
| 539 | |
| 540 | class TestSuite(object): |
| 541 | |
| 542 | def __init__(self, name): |
| 543 | self.name = name |
| 544 | |
| 545 | def GetName(self): |
| 546 | return self.name |
| 547 | |
| 548 | |
| 549 | class TestRepository(TestSuite): |
| 550 | |
| 551 | def __init__(self, path): |
| 552 | normalized_path = abspath(path) |
| 553 | super(TestRepository, self).__init__(basename(normalized_path)) |
| 554 | self.path = normalized_path |
| 555 | self.is_loaded = False |
| 556 | self.config = None |
| 557 | |
| 558 | def GetConfiguration(self, context): |
| 559 | if self.is_loaded: |
| 560 | return self.config |
| 561 | self.is_loaded = True |
| 562 | file = None |
| 563 | try: |
| 564 | (file, pathname, description) = imp.find_module('testcfg', [ self.path ]) |
| 565 | module = imp.load_module('testcfg', file, pathname, description) |
| 566 | self.config = module.GetConfiguration(context, self.path) |
| 567 | finally: |
| 568 | if file: |
| 569 | file.close() |
| 570 | return self.config |
| 571 | |
| 572 | def GetBuildRequirements(self, path, context): |
| 573 | return self.GetConfiguration(context).GetBuildRequirements() |
| 574 | |
| 575 | def ListTests(self, current_path, path, context, mode): |
| 576 | return self.GetConfiguration(context).ListTests(current_path, path, mode) |
| 577 | |
| 578 | def GetTestStatus(self, context, sections, defs): |
| 579 | self.GetConfiguration(context).GetTestStatus(sections, defs) |
| 580 | |
| 581 | |
| 582 | class LiteralTestSuite(TestSuite): |
| 583 | |
| 584 | def __init__(self, tests): |
| 585 | super(LiteralTestSuite, self).__init__('root') |
| 586 | self.tests = tests |
| 587 | |
| 588 | def GetBuildRequirements(self, path, context): |
| 589 | (name, rest) = CarCdr(path) |
| 590 | result = [ ] |
| 591 | for test in self.tests: |
| 592 | if not name or name.match(test.GetName()): |
| 593 | result += test.GetBuildRequirements(rest, context) |
| 594 | return result |
| 595 | |
| 596 | def ListTests(self, current_path, path, context, mode): |
| 597 | (name, rest) = CarCdr(path) |
| 598 | result = [ ] |
| 599 | for test in self.tests: |
| 600 | test_name = test.GetName() |
| 601 | if not name or name.match(test_name): |
| 602 | full_path = current_path + [test_name] |
| 603 | result += test.ListTests(full_path, path, context, mode) |
| 604 | return result |
| 605 | |
| 606 | def GetTestStatus(self, context, sections, defs): |
| 607 | for test in self.tests: |
| 608 | test.GetTestStatus(context, sections, defs) |
| 609 | |
| 610 | |
| 611 | SUFFIX = {'debug': '_g', 'release': ''} |
| 612 | |
| 613 | |
| 614 | class Context(object): |
| 615 | |
| 616 | def __init__(self, workspace, buildspace, verbose, vm, timeout, processor, suppress_dialogs): |
| 617 | self.workspace = workspace |
| 618 | self.buildspace = buildspace |
| 619 | self.verbose = verbose |
| 620 | self.vm_root = vm |
| 621 | self.timeout = timeout |
| 622 | self.processor = processor |
| 623 | self.suppress_dialogs = suppress_dialogs |
| 624 | |
| 625 | def GetVm(self, mode): |
| 626 | name = self.vm_root + SUFFIX[mode] |
| 627 | if utils.IsWindows() and not name.endswith('.exe'): |
| 628 | name = name + '.exe' |
| 629 | return name |
| 630 | |
| 631 | def RunTestCases(all_cases, progress, tasks): |
| 632 | def DoSkip(case): |
| 633 | return SKIP in c.outcomes or SLOW in c.outcomes |
| 634 | cases_to_run = [ c for c in all_cases if not DoSkip(c) ] |
| 635 | progress = PROGRESS_INDICATORS[progress](cases_to_run) |
| 636 | return progress.Run(tasks) |
| 637 | |
| 638 | |
| 639 | def BuildRequirements(context, requirements, mode, scons_flags): |
| 640 | command_line = (['scons', '-Y', context.workspace, 'mode=' + ",".join(mode)] |
| 641 | + requirements |
| 642 | + scons_flags) |
| 643 | output = ExecuteNoCapture(command_line, context) |
| 644 | return output.exit_code == 0 |
| 645 | |
| 646 | |
| 647 | # ------------------------------------------- |
| 648 | # --- T e s t C o n f i g u r a t i o n --- |
| 649 | # ------------------------------------------- |
| 650 | |
| 651 | |
| 652 | SKIP = 'skip' |
| 653 | FAIL = 'fail' |
| 654 | PASS = 'pass' |
| 655 | OKAY = 'okay' |
| 656 | TIMEOUT = 'timeout' |
| 657 | CRASH = 'crash' |
| 658 | SLOW = 'slow' |
| 659 | |
| 660 | |
| 661 | class Expression(object): |
| 662 | pass |
| 663 | |
| 664 | |
| 665 | class Constant(Expression): |
| 666 | |
| 667 | def __init__(self, value): |
| 668 | self.value = value |
| 669 | |
| 670 | def Evaluate(self, env, defs): |
| 671 | return self.value |
| 672 | |
| 673 | |
| 674 | class Variable(Expression): |
| 675 | |
| 676 | def __init__(self, name): |
| 677 | self.name = name |
| 678 | |
| 679 | def GetOutcomes(self, env, defs): |
| 680 | if self.name in env: return ListSet([env[self.name]]) |
| 681 | else: return Nothing() |
| 682 | |
| 683 | |
| 684 | class Outcome(Expression): |
| 685 | |
| 686 | def __init__(self, name): |
| 687 | self.name = name |
| 688 | |
| 689 | def GetOutcomes(self, env, defs): |
| 690 | if self.name in defs: |
| 691 | return defs[self.name].GetOutcomes(env, defs) |
| 692 | else: |
| 693 | return ListSet([self.name]) |
| 694 | |
| 695 | |
| 696 | class Set(object): |
| 697 | pass |
| 698 | |
| 699 | |
| 700 | class ListSet(Set): |
| 701 | |
| 702 | def __init__(self, elms): |
| 703 | self.elms = elms |
| 704 | |
| 705 | def __str__(self): |
| 706 | return "ListSet%s" % str(self.elms) |
| 707 | |
| 708 | def Intersect(self, that): |
| 709 | if not isinstance(that, ListSet): |
| 710 | return that.Intersect(self) |
| 711 | return ListSet([ x for x in self.elms if x in that.elms ]) |
| 712 | |
| 713 | def Union(self, that): |
| 714 | if not isinstance(that, ListSet): |
| 715 | return that.Union(self) |
| 716 | return ListSet(self.elms + [ x for x in that.elms if x not in self.elms ]) |
| 717 | |
| 718 | def IsEmpty(self): |
| 719 | return len(self.elms) == 0 |
| 720 | |
| 721 | |
| 722 | class Everything(Set): |
| 723 | |
| 724 | def Intersect(self, that): |
| 725 | return that |
| 726 | |
| 727 | def Union(self, that): |
| 728 | return self |
| 729 | |
| 730 | def IsEmpty(self): |
| 731 | return False |
| 732 | |
| 733 | |
| 734 | class Nothing(Set): |
| 735 | |
| 736 | def Intersect(self, that): |
| 737 | return self |
| 738 | |
| 739 | def Union(self, that): |
| 740 | return that |
| 741 | |
| 742 | def IsEmpty(self): |
| 743 | return True |
| 744 | |
| 745 | |
| 746 | class Operation(Expression): |
| 747 | |
| 748 | def __init__(self, left, op, right): |
| 749 | self.left = left |
| 750 | self.op = op |
| 751 | self.right = right |
| 752 | |
| 753 | def Evaluate(self, env, defs): |
| 754 | if self.op == '||' or self.op == ',': |
| 755 | return self.left.Evaluate(env, defs) or self.right.Evaluate(env, defs) |
| 756 | elif self.op == 'if': |
| 757 | return False |
| 758 | elif self.op == '==': |
| 759 | inter = self.left.GetOutcomes(env, defs).Intersect(self.right.GetOutcomes(env, defs)) |
| 760 | return not inter.IsEmpty() |
| 761 | else: |
| 762 | assert self.op == '&&' |
| 763 | return self.left.Evaluate(env, defs) and self.right.Evaluate(env, defs) |
| 764 | |
| 765 | def GetOutcomes(self, env, defs): |
| 766 | if self.op == '||' or self.op == ',': |
| 767 | return self.left.GetOutcomes(env, defs).Union(self.right.GetOutcomes(env, defs)) |
| 768 | elif self.op == 'if': |
| 769 | if self.right.Evaluate(env, defs): return self.left.GetOutcomes(env, defs) |
| 770 | else: return Nothing() |
| 771 | else: |
| 772 | assert self.op == '&&' |
| 773 | return self.left.GetOutcomes(env, defs).Intersect(self.right.GetOutcomes(env, defs)) |
| 774 | |
| 775 | |
| 776 | def IsAlpha(str): |
| 777 | for char in str: |
| 778 | if not (char.isalpha() or char.isdigit() or char == '_'): |
| 779 | return False |
| 780 | return True |
| 781 | |
| 782 | |
| 783 | class Tokenizer(object): |
| 784 | """A simple string tokenizer that chops expressions into variables, |
| 785 | parens and operators""" |
| 786 | |
| 787 | def __init__(self, expr): |
| 788 | self.index = 0 |
| 789 | self.expr = expr |
| 790 | self.length = len(expr) |
| 791 | self.tokens = None |
| 792 | |
| 793 | def Current(self, length = 1): |
| 794 | if not self.HasMore(length): return "" |
| 795 | return self.expr[self.index:self.index+length] |
| 796 | |
| 797 | def HasMore(self, length = 1): |
| 798 | return self.index < self.length + (length - 1) |
| 799 | |
| 800 | def Advance(self, count = 1): |
| 801 | self.index = self.index + count |
| 802 | |
| 803 | def AddToken(self, token): |
| 804 | self.tokens.append(token) |
| 805 | |
| 806 | def SkipSpaces(self): |
| 807 | while self.HasMore() and self.Current().isspace(): |
| 808 | self.Advance() |
| 809 | |
| 810 | def Tokenize(self): |
| 811 | self.tokens = [ ] |
| 812 | while self.HasMore(): |
| 813 | self.SkipSpaces() |
| 814 | if not self.HasMore(): |
| 815 | return None |
| 816 | if self.Current() == '(': |
| 817 | self.AddToken('(') |
| 818 | self.Advance() |
| 819 | elif self.Current() == ')': |
| 820 | self.AddToken(')') |
| 821 | self.Advance() |
| 822 | elif self.Current() == '$': |
| 823 | self.AddToken('$') |
| 824 | self.Advance() |
| 825 | elif self.Current() == ',': |
| 826 | self.AddToken(',') |
| 827 | self.Advance() |
| 828 | elif IsAlpha(self.Current()): |
| 829 | buf = "" |
| 830 | while self.HasMore() and IsAlpha(self.Current()): |
| 831 | buf += self.Current() |
| 832 | self.Advance() |
| 833 | self.AddToken(buf) |
| 834 | elif self.Current(2) == '&&': |
| 835 | self.AddToken('&&') |
| 836 | self.Advance(2) |
| 837 | elif self.Current(2) == '||': |
| 838 | self.AddToken('||') |
| 839 | self.Advance(2) |
| 840 | elif self.Current(2) == '==': |
| 841 | self.AddToken('==') |
| 842 | self.Advance(2) |
| 843 | else: |
| 844 | return None |
| 845 | return self.tokens |
| 846 | |
| 847 | |
| 848 | class Scanner(object): |
| 849 | """A simple scanner that can serve out tokens from a given list""" |
| 850 | |
| 851 | def __init__(self, tokens): |
| 852 | self.tokens = tokens |
| 853 | self.length = len(tokens) |
| 854 | self.index = 0 |
| 855 | |
| 856 | def HasMore(self): |
| 857 | return self.index < self.length |
| 858 | |
| 859 | def Current(self): |
| 860 | return self.tokens[self.index] |
| 861 | |
| 862 | def Advance(self): |
| 863 | self.index = self.index + 1 |
| 864 | |
| 865 | |
| 866 | def ParseAtomicExpression(scan): |
| 867 | if scan.Current() == "true": |
| 868 | scan.Advance() |
| 869 | return Constant(True) |
| 870 | elif scan.Current() == "false": |
| 871 | scan.Advance() |
| 872 | return Constant(False) |
| 873 | elif IsAlpha(scan.Current()): |
| 874 | name = scan.Current() |
| 875 | scan.Advance() |
| 876 | return Outcome(name.lower()) |
| 877 | elif scan.Current() == '$': |
| 878 | scan.Advance() |
| 879 | if not IsAlpha(scan.Current()): |
| 880 | return None |
| 881 | name = scan.Current() |
| 882 | scan.Advance() |
| 883 | return Variable(name.lower()) |
| 884 | elif scan.Current() == '(': |
| 885 | scan.Advance() |
| 886 | result = ParseLogicalExpression(scan) |
| 887 | if (not result) or (scan.Current() != ')'): |
| 888 | return None |
| 889 | scan.Advance() |
| 890 | return result |
| 891 | else: |
| 892 | return None |
| 893 | |
| 894 | |
| 895 | BINARIES = ['=='] |
| 896 | def ParseOperatorExpression(scan): |
| 897 | left = ParseAtomicExpression(scan) |
| 898 | if not left: return None |
| 899 | while scan.HasMore() and (scan.Current() in BINARIES): |
| 900 | op = scan.Current() |
| 901 | scan.Advance() |
| 902 | right = ParseOperatorExpression(scan) |
| 903 | if not right: |
| 904 | return None |
| 905 | left = Operation(left, op, right) |
| 906 | return left |
| 907 | |
| 908 | |
| 909 | def ParseConditionalExpression(scan): |
| 910 | left = ParseOperatorExpression(scan) |
| 911 | if not left: return None |
| 912 | while scan.HasMore() and (scan.Current() == 'if'): |
| 913 | scan.Advance() |
| 914 | right = ParseOperatorExpression(scan) |
| 915 | if not right: |
| 916 | return None |
| 917 | left= Operation(left, 'if', right) |
| 918 | return left |
| 919 | |
| 920 | |
| 921 | LOGICALS = ["&&", "||", ","] |
| 922 | def ParseLogicalExpression(scan): |
| 923 | left = ParseConditionalExpression(scan) |
| 924 | if not left: return None |
| 925 | while scan.HasMore() and (scan.Current() in LOGICALS): |
| 926 | op = scan.Current() |
| 927 | scan.Advance() |
| 928 | right = ParseConditionalExpression(scan) |
| 929 | if not right: |
| 930 | return None |
| 931 | left = Operation(left, op, right) |
| 932 | return left |
| 933 | |
| 934 | |
| 935 | def ParseCondition(expr): |
| 936 | """Parses a logical expression into an Expression object""" |
| 937 | tokens = Tokenizer(expr).Tokenize() |
| 938 | if not tokens: |
| 939 | print "Malformed expression: '%s'" % expr |
| 940 | return None |
| 941 | scan = Scanner(tokens) |
| 942 | ast = ParseLogicalExpression(scan) |
| 943 | if not ast: |
| 944 | print "Malformed expression: '%s'" % expr |
| 945 | return None |
| 946 | if scan.HasMore(): |
| 947 | print "Malformed expression: '%s'" % expr |
| 948 | return None |
| 949 | return ast |
| 950 | |
| 951 | |
| 952 | class ClassifiedTest(object): |
| 953 | |
| 954 | def __init__(self, case, outcomes): |
| 955 | self.case = case |
| 956 | self.outcomes = outcomes |
| 957 | |
| 958 | |
| 959 | class Configuration(object): |
| 960 | """The parsed contents of a configuration file""" |
| 961 | |
| 962 | def __init__(self, sections, defs): |
| 963 | self.sections = sections |
| 964 | self.defs = defs |
| 965 | |
| 966 | def ClassifyTests(self, cases, env): |
| 967 | sections = [s for s in self.sections if s.condition.Evaluate(env, self.defs)] |
| 968 | all_rules = reduce(list.__add__, [s.rules for s in sections], []) |
| 969 | unused_rules = set(all_rules) |
| 970 | result = [ ] |
| 971 | all_outcomes = set([]) |
| 972 | for case in cases: |
| 973 | matches = [ r for r in all_rules if r.Contains(case.path) ] |
| 974 | outcomes = set([]) |
| 975 | for rule in matches: |
| 976 | outcomes = outcomes.union(rule.GetOutcomes(env, self.defs)) |
| 977 | unused_rules.discard(rule) |
| 978 | if not outcomes: |
| 979 | outcomes = [PASS] |
| 980 | case.outcomes = outcomes |
| 981 | all_outcomes = all_outcomes.union(outcomes) |
| 982 | result.append(ClassifiedTest(case, outcomes)) |
| 983 | return (result, list(unused_rules), all_outcomes) |
| 984 | |
| 985 | |
| 986 | class Section(object): |
| 987 | """A section of the configuration file. Sections are enabled or |
| 988 | disabled prior to running the tests, based on their conditions""" |
| 989 | |
| 990 | def __init__(self, condition): |
| 991 | self.condition = condition |
| 992 | self.rules = [ ] |
| 993 | |
| 994 | def AddRule(self, rule): |
| 995 | self.rules.append(rule) |
| 996 | |
| 997 | |
| 998 | class Rule(object): |
| 999 | """A single rule that specifies the expected outcome for a single |
| 1000 | test.""" |
| 1001 | |
| 1002 | def __init__(self, raw_path, path, value): |
| 1003 | self.raw_path = raw_path |
| 1004 | self.path = path |
| 1005 | self.value = value |
| 1006 | |
| 1007 | def GetOutcomes(self, env, defs): |
| 1008 | set = self.value.GetOutcomes(env, defs) |
| 1009 | assert isinstance(set, ListSet) |
| 1010 | return set.elms |
| 1011 | |
| 1012 | def Contains(self, path): |
| 1013 | if len(self.path) > len(path): |
| 1014 | return False |
| 1015 | for i in xrange(len(self.path)): |
| 1016 | if not self.path[i].match(path[i]): |
| 1017 | return False |
| 1018 | return True |
| 1019 | |
| 1020 | |
| 1021 | HEADER_PATTERN = re.compile(r'\[([^]]+)\]') |
| 1022 | RULE_PATTERN = re.compile(r'\s*([^: ]*)\s*:(.*)') |
| 1023 | DEF_PATTERN = re.compile(r'^def\s*(\w+)\s*=(.*)$') |
| 1024 | PREFIX_PATTERN = re.compile(r'^\s*prefix\s+([\w\_\.\-\/]+)$') |
| 1025 | |
| 1026 | |
| 1027 | def ReadConfigurationInto(path, sections, defs): |
| 1028 | current_section = Section(Constant(True)) |
| 1029 | sections.append(current_section) |
| 1030 | prefix = [] |
| 1031 | for line in utils.ReadLinesFrom(path): |
| 1032 | header_match = HEADER_PATTERN.match(line) |
| 1033 | if header_match: |
| 1034 | condition_str = header_match.group(1).strip() |
| 1035 | condition = ParseCondition(condition_str) |
| 1036 | new_section = Section(condition) |
| 1037 | sections.append(new_section) |
| 1038 | current_section = new_section |
| 1039 | continue |
| 1040 | rule_match = RULE_PATTERN.match(line) |
| 1041 | if rule_match: |
| 1042 | path = prefix + SplitPath(rule_match.group(1).strip()) |
| 1043 | value_str = rule_match.group(2).strip() |
| 1044 | value = ParseCondition(value_str) |
| 1045 | if not value: |
| 1046 | return False |
| 1047 | current_section.AddRule(Rule(rule_match.group(1), path, value)) |
| 1048 | continue |
| 1049 | def_match = DEF_PATTERN.match(line) |
| 1050 | if def_match: |
| 1051 | name = def_match.group(1).lower() |
| 1052 | value = ParseCondition(def_match.group(2).strip()) |
| 1053 | if not value: |
| 1054 | return False |
| 1055 | defs[name] = value |
| 1056 | continue |
| 1057 | prefix_match = PREFIX_PATTERN.match(line) |
| 1058 | if prefix_match: |
| 1059 | prefix = SplitPath(prefix_match.group(1).strip()) |
| 1060 | continue |
| 1061 | print "Malformed line: '%s'." % line |
| 1062 | return False |
| 1063 | return True |
| 1064 | |
| 1065 | |
| 1066 | # --------------- |
| 1067 | # --- M a i n --- |
| 1068 | # --------------- |
| 1069 | |
| 1070 | |
| 1071 | ARCH_GUESS = utils.GuessArchitecture() |
| 1072 | |
| 1073 | |
| 1074 | def BuildOptions(): |
| 1075 | result = optparse.OptionParser() |
| 1076 | result.add_option("-m", "--mode", help="The test modes in which to run (comma-separated)", |
| 1077 | default='release') |
| 1078 | result.add_option("-v", "--verbose", help="Verbose output", |
| 1079 | default=False, action="store_true") |
| 1080 | result.add_option("-S", dest="scons_flags", help="Flag to pass through to scons", |
| 1081 | default=[], action="append") |
| 1082 | result.add_option("-p", "--progress", |
| 1083 | help="The style of progress indicator (verbose, dots, color, mono)", |
| 1084 | choices=PROGRESS_INDICATORS.keys(), default="mono") |
| 1085 | result.add_option("--no-build", help="Don't build requirements", |
| 1086 | default=False, action="store_true") |
| 1087 | result.add_option("--build-only", help="Only build requirements, don't run the tests", |
| 1088 | default=False, action="store_true") |
| 1089 | result.add_option("--report", help="Print a summary of the tests to be run", |
| 1090 | default=False, action="store_true") |
| 1091 | result.add_option("-s", "--suite", help="A test suite", |
| 1092 | default=[], action="append") |
| 1093 | result.add_option("-t", "--timeout", help="Timeout in seconds", |
| 1094 | default=60, type="int") |
| 1095 | result.add_option("--arch", help='The architecture to run tests for', |
| 1096 | default='none') |
| 1097 | result.add_option("--simulator", help="Run tests with architecture simulator", |
| 1098 | default='none') |
| 1099 | result.add_option("--special-command", default=None) |
| 1100 | result.add_option("--valgrind", help="Run tests through valgrind", |
| 1101 | default=False, action="store_true") |
| 1102 | result.add_option("--cat", help="Print the source of the tests", |
| 1103 | default=False, action="store_true") |
| 1104 | result.add_option("--warn-unused", help="Report unused rules", |
| 1105 | default=False, action="store_true") |
| 1106 | result.add_option("-j", help="The number of parallel tasks to run", |
| 1107 | default=1, type="int") |
| 1108 | result.add_option("--time", help="Print timing information after running", |
| 1109 | default=False, action="store_true") |
| 1110 | result.add_option("--suppress-dialogs", help="Suppress Windows dialogs for crashing tests", |
| 1111 | dest="suppress_dialogs", default=True, action="store_true") |
| 1112 | result.add_option("--no-suppress-dialogs", help="Display Windows dialogs for crashing tests", |
| 1113 | dest="suppress_dialogs", action="store_false") |
| 1114 | result.add_option("--shell", help="Path to V8 shell", default="shell"); |
| 1115 | return result |
| 1116 | |
| 1117 | |
| 1118 | def ProcessOptions(options): |
| 1119 | global VERBOSE |
| 1120 | VERBOSE = options.verbose |
| 1121 | options.mode = options.mode.split(',') |
| 1122 | for mode in options.mode: |
| 1123 | if not mode in ['debug', 'release']: |
| 1124 | print "Unknown mode %s" % mode |
| 1125 | return False |
| 1126 | if options.simulator != 'none': |
| 1127 | # Simulator argument was set. Make sure arch and simulator agree. |
| 1128 | if options.simulator != options.arch: |
| 1129 | if options.arch == 'none': |
| 1130 | options.arch = options.simulator |
| 1131 | else: |
| 1132 | print "Architecture %s does not match sim %s" %(options.arch, options.simulator) |
| 1133 | return False |
| 1134 | # Ensure that the simulator argument is handed down to scons. |
| 1135 | options.scons_flags.append("simulator=" + options.simulator) |
| 1136 | else: |
| 1137 | # If options.arch is not set by the command line and no simulator setting |
| 1138 | # was found, set the arch to the guess. |
| 1139 | if options.arch == 'none': |
| 1140 | options.arch = ARCH_GUESS |
| 1141 | options.scons_flags.append("arch=" + options.arch) |
| 1142 | return True |
| 1143 | |
| 1144 | |
| 1145 | REPORT_TEMPLATE = """\ |
| 1146 | Total: %(total)i tests |
| 1147 | * %(skipped)4d tests will be skipped |
| 1148 | * %(nocrash)4d tests are expected to be flaky but not crash |
| 1149 | * %(pass)4d tests are expected to pass |
| 1150 | * %(fail_ok)4d tests are expected to fail that we won't fix |
| 1151 | * %(fail)4d tests are expected to fail that we should fix\ |
| 1152 | """ |
| 1153 | |
| 1154 | def PrintReport(cases): |
| 1155 | def IsFlaky(o): |
| 1156 | return (PASS in o) and (FAIL in o) and (not CRASH in o) and (not OKAY in o) |
| 1157 | def IsFailOk(o): |
| 1158 | return (len(o) == 2) and (FAIL in o) and (OKAY in o) |
| 1159 | unskipped = [c for c in cases if not SKIP in c.outcomes] |
| 1160 | print REPORT_TEMPLATE % { |
| 1161 | 'total': len(cases), |
| 1162 | 'skipped': len(cases) - len(unskipped), |
| 1163 | 'nocrash': len([t for t in unskipped if IsFlaky(t.outcomes)]), |
| 1164 | 'pass': len([t for t in unskipped if list(t.outcomes) == [PASS]]), |
| 1165 | 'fail_ok': len([t for t in unskipped if IsFailOk(t.outcomes)]), |
| 1166 | 'fail': len([t for t in unskipped if list(t.outcomes) == [FAIL]]) |
| 1167 | } |
| 1168 | |
| 1169 | |
| 1170 | class Pattern(object): |
| 1171 | |
| 1172 | def __init__(self, pattern): |
| 1173 | self.pattern = pattern |
| 1174 | self.compiled = None |
| 1175 | |
| 1176 | def match(self, str): |
| 1177 | if not self.compiled: |
| 1178 | pattern = "^" + self.pattern.replace('*', '.*') + "$" |
| 1179 | self.compiled = re.compile(pattern) |
| 1180 | return self.compiled.match(str) |
| 1181 | |
| 1182 | def __str__(self): |
| 1183 | return self.pattern |
| 1184 | |
| 1185 | |
| 1186 | def SplitPath(s): |
| 1187 | stripped = [ c.strip() for c in s.split('/') ] |
| 1188 | return [ Pattern(s) for s in stripped if len(s) > 0 ] |
| 1189 | |
| 1190 | |
| 1191 | def GetSpecialCommandProcessor(value): |
| 1192 | if (not value) or (value.find('@') == -1): |
| 1193 | def ExpandCommand(args): |
| 1194 | return args |
| 1195 | return ExpandCommand |
| 1196 | else: |
| 1197 | pos = value.find('@') |
| 1198 | import urllib |
| 1199 | prefix = urllib.unquote(value[:pos]).split() |
| 1200 | suffix = urllib.unquote(value[pos+1:]).split() |
| 1201 | def ExpandCommand(args): |
| 1202 | return prefix + args + suffix |
| 1203 | return ExpandCommand |
| 1204 | |
| 1205 | |
| 1206 | BUILT_IN_TESTS = ['mjsunit', 'cctest', 'message'] |
| 1207 | |
| 1208 | |
| 1209 | def GetSuites(test_root): |
| 1210 | def IsSuite(path): |
| 1211 | return isdir(path) and exists(join(path, 'testcfg.py')) |
| 1212 | return [ f for f in os.listdir(test_root) if IsSuite(join(test_root, f)) ] |
| 1213 | |
| 1214 | |
| 1215 | def FormatTime(d): |
| 1216 | millis = round(d * 1000) % 1000 |
| 1217 | return time.strftime("%M:%S.", time.gmtime(d)) + ("%03i" % millis) |
| 1218 | |
| 1219 | |
| 1220 | def Main(): |
| 1221 | parser = BuildOptions() |
| 1222 | (options, args) = parser.parse_args() |
| 1223 | if not ProcessOptions(options): |
| 1224 | parser.print_help() |
| 1225 | return 1 |
| 1226 | |
| 1227 | workspace = abspath(join(dirname(sys.argv[0]), '..')) |
| 1228 | suites = GetSuites(join(workspace, 'test')) |
| 1229 | repositories = [TestRepository(join(workspace, 'test', name)) for name in suites] |
| 1230 | repositories += [TestRepository(a) for a in options.suite] |
| 1231 | |
| 1232 | root = LiteralTestSuite(repositories) |
| 1233 | if len(args) == 0: |
| 1234 | paths = [SplitPath(t) for t in BUILT_IN_TESTS] |
| 1235 | else: |
| 1236 | paths = [ ] |
| 1237 | for arg in args: |
| 1238 | path = SplitPath(arg) |
| 1239 | paths.append(path) |
| 1240 | |
| 1241 | # Check for --valgrind option. If enabled, we overwrite the special |
| 1242 | # command flag with a command that uses the run-valgrind.py script. |
| 1243 | if options.valgrind: |
| 1244 | run_valgrind = join(workspace, "tools", "run-valgrind.py") |
| 1245 | options.special_command = "python -u " + run_valgrind + " @" |
| 1246 | |
| 1247 | shell = abspath(options.shell) |
| 1248 | buildspace = dirname(shell) |
| 1249 | context = Context(workspace, buildspace, VERBOSE, |
| 1250 | shell, |
| 1251 | options.timeout, |
| 1252 | GetSpecialCommandProcessor(options.special_command), |
| 1253 | options.suppress_dialogs) |
| 1254 | # First build the required targets |
| 1255 | if not options.no_build: |
| 1256 | reqs = [ ] |
| 1257 | for path in paths: |
| 1258 | reqs += root.GetBuildRequirements(path, context) |
| 1259 | reqs = list(set(reqs)) |
| 1260 | if len(reqs) > 0: |
| 1261 | if options.j != 1: |
| 1262 | options.scons_flags += ['-j', str(options.j)] |
| 1263 | if not BuildRequirements(context, reqs, options.mode, options.scons_flags): |
| 1264 | return 1 |
| 1265 | |
| 1266 | # Just return if we are only building the targets for running the tests. |
| 1267 | if options.build_only: |
| 1268 | return 0 |
| 1269 | |
| 1270 | # Get status for tests |
| 1271 | sections = [ ] |
| 1272 | defs = { } |
| 1273 | root.GetTestStatus(context, sections, defs) |
| 1274 | config = Configuration(sections, defs) |
| 1275 | |
| 1276 | # List the tests |
| 1277 | all_cases = [ ] |
| 1278 | all_unused = [ ] |
| 1279 | unclassified_tests = [ ] |
| 1280 | globally_unused_rules = None |
| 1281 | for path in paths: |
| 1282 | for mode in options.mode: |
| 1283 | if not exists(context.GetVm(mode)): |
| 1284 | print "Can't find shell executable: '%s'" % context.GetVm(mode) |
| 1285 | continue |
| 1286 | env = { |
| 1287 | 'mode': mode, |
| 1288 | 'system': utils.GuessOS(), |
| 1289 | 'arch': options.arch, |
| 1290 | 'simulator': options.simulator |
| 1291 | } |
| 1292 | test_list = root.ListTests([], path, context, mode) |
| 1293 | unclassified_tests += test_list |
| 1294 | (cases, unused_rules, all_outcomes) = config.ClassifyTests(test_list, env) |
| 1295 | if globally_unused_rules is None: |
| 1296 | globally_unused_rules = set(unused_rules) |
| 1297 | else: |
| 1298 | globally_unused_rules = globally_unused_rules.intersection(unused_rules) |
| 1299 | all_cases += cases |
| 1300 | all_unused.append(unused_rules) |
| 1301 | |
| 1302 | if options.cat: |
| 1303 | visited = set() |
| 1304 | for test in unclassified_tests: |
| 1305 | key = tuple(test.path) |
| 1306 | if key in visited: |
| 1307 | continue |
| 1308 | visited.add(key) |
| 1309 | print "--- begin source: %s ---" % test.GetLabel() |
| 1310 | source = test.GetSource().strip() |
| 1311 | print source |
| 1312 | print "--- end source: %s ---" % test.GetLabel() |
| 1313 | return 0 |
| 1314 | |
| 1315 | if options.warn_unused: |
| 1316 | for rule in globally_unused_rules: |
| 1317 | print "Rule for '%s' was not used." % '/'.join([str(s) for s in rule.path]) |
| 1318 | |
| 1319 | if options.report: |
| 1320 | PrintReport(all_cases) |
| 1321 | |
| 1322 | result = None |
| 1323 | if len(all_cases) == 0: |
| 1324 | print "No tests to run." |
| 1325 | return 0 |
| 1326 | else: |
| 1327 | try: |
| 1328 | start = time.time() |
| 1329 | if RunTestCases(all_cases, options.progress, options.j): |
| 1330 | result = 0 |
| 1331 | else: |
| 1332 | result = 1 |
| 1333 | duration = time.time() - start |
| 1334 | except KeyboardInterrupt: |
| 1335 | print "Interrupted" |
| 1336 | return 1 |
| 1337 | |
| 1338 | if options.time: |
| 1339 | # Write the times to stderr to make it easy to separate from the |
| 1340 | # test output. |
| 1341 | print |
| 1342 | sys.stderr.write("--- Total time: %s ---\n" % FormatTime(duration)) |
| 1343 | timed_tests = [ t.case for t in all_cases if not t.case.duration is None ] |
| 1344 | timed_tests.sort(lambda a, b: a.CompareTime(b)) |
| 1345 | index = 1 |
| 1346 | for entry in timed_tests[:20]: |
| 1347 | t = FormatTime(entry.duration) |
| 1348 | sys.stderr.write("%4i (%s) %s\n" % (index, t, entry.GetLabel())) |
| 1349 | index += 1 |
| 1350 | |
| 1351 | return result |
| 1352 | |
| 1353 | |
| 1354 | if __name__ == '__main__': |
| 1355 | sys.exit(Main()) |