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