blob: 586925ae8319ebb92d6abaaaf2019ed75e169a00 [file] [log] [blame]
Steve Blocka7e24c12009-10-30 11:49:00 +00001#!/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
31import imp
32import optparse
33import os
34from os.path import join, dirname, abspath, basename, isdir, exists
35import platform
36import re
37import signal
38import subprocess
39import sys
40import tempfile
41import time
42import threading
43import utils
44from Queue import Queue, Empty
45
46
47VERBOSE = 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
55class 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
137def 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
149class 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
182class 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
199class 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
223class 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
279class 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
293class 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
309PROGRESS_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
322class 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 Block3ce2e202009-11-05 08:53:23 +0000329 self.failed = None
Steve Blocka7e24c12009-10-30 11:49:00 +0000330
331
332class TestCase(object):
333
334 def __init__(self, context, path):
335 self.path = path
336 self.context = context
Steve Blocka7e24c12009-10-30 11:49:00 +0000337 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 Block3ce2e202009-11-05 08:53:23 +0000346 if output.failed is None:
347 output.failed = self.IsFailureOutput(output)
348 return output.failed
Steve Blocka7e24c12009-10-30 11:49:00 +0000349
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
369class 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
408def 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
415MAX_SLEEP_TIME = 0.1
416INITIAL_SLEEP_TIME = 0.0001
417SLEEP_TIME_FACTOR = 1.25
418
419SEM_INVALID_VALUE = -1
420SEM_NOGPFAULTERRORBOX = 0x0002 # Microsoft Platform SDK WinBase.h
421
422def 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
431def 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
475def PrintError(str):
476 sys.stderr.write(str)
477 sys.stderr.write('\n')
478
479
480def CheckedUnlink(name):
481 try:
482 os.unlink(name)
483 except OSError, e:
484 PrintError("os.unlink() " + str(e))
485
486
487def 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
506def 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
515def CarCdr(path):
516 if len(path) == 0:
517 return (None, [ ])
518 else:
519 return (path[0], path[1:])
520
521
522class 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
540class TestSuite(object):
541
542 def __init__(self, name):
543 self.name = name
544
545 def GetName(self):
546 return self.name
547
548
549class 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
582class 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
611SUFFIX = {'debug': '_g', 'release': ''}
612
613
614class 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
631def 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
639def 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
652SKIP = 'skip'
653FAIL = 'fail'
654PASS = 'pass'
655OKAY = 'okay'
656TIMEOUT = 'timeout'
657CRASH = 'crash'
658SLOW = 'slow'
659
660
661class Expression(object):
662 pass
663
664
665class 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
674class 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
684class 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
696class Set(object):
697 pass
698
699
700class 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
722class 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
734class 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
746class 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
776def 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
783class 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
848class 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
866def 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
895BINARIES = ['==']
896def 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
909def 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
921LOGICALS = ["&&", "||", ","]
922def 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
935def 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
952class ClassifiedTest(object):
953
954 def __init__(self, case, outcomes):
955 self.case = case
956 self.outcomes = outcomes
957
958
959class 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
986class 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
998class 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
1021HEADER_PATTERN = re.compile(r'\[([^]]+)\]')
1022RULE_PATTERN = re.compile(r'\s*([^: ]*)\s*:(.*)')
1023DEF_PATTERN = re.compile(r'^def\s*(\w+)\s*=(.*)$')
1024PREFIX_PATTERN = re.compile(r'^\s*prefix\s+([\w\_\.\-\/]+)$')
1025
1026
1027def 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
1071ARCH_GUESS = utils.GuessArchitecture()
1072
1073
1074def 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
1118def 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
1145REPORT_TEMPLATE = """\
1146Total: %(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
1154def 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
1170class 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
1186def 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
1191def 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
1206BUILT_IN_TESTS = ['mjsunit', 'cctest', 'message']
1207
1208
1209def 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
1215def FormatTime(d):
1216 millis = round(d * 1000) % 1000
1217 return time.strftime("%M:%S.", time.gmtime(d)) + ("%03i" % millis)
1218
1219
1220def 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
1354if __name__ == '__main__':
1355 sys.exit(Main())