blob: 36ce7be83f64adbd31d42927a821e063c647f3b9 [file] [log] [blame]
Ben Murdochb8a8cc12014-11-26 15:28:44 +00001# Copyright 2012 the V8 project authors. All rights reserved.
2# Redistribution and use in source and binary forms, with or without
3# modification, are permitted provided that the following conditions are
4# met:
5#
6# * Redistributions of source code must retain the above copyright
7# notice, this list of conditions and the following disclaimer.
8# * Redistributions in binary form must reproduce the above
9# copyright notice, this list of conditions and the following
10# disclaimer in the documentation and/or other materials provided
11# with the distribution.
12# * Neither the name of Google Inc. nor the names of its
13# contributors may be used to endorse or promote products derived
14# from this software without specific prior written permission.
15#
16# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
28
29import os
30import shutil
31import time
32
33from pool import Pool
34from . import commands
35from . import perfdata
36from . import utils
37
38
39class Job(object):
40 def __init__(self, command, dep_command, test_id, timeout, verbose):
41 self.command = command
42 self.dep_command = dep_command
43 self.id = test_id
44 self.timeout = timeout
45 self.verbose = verbose
46
47
48def RunTest(job):
49 start_time = time.time()
50 if job.dep_command is not None:
51 dep_output = commands.Execute(job.dep_command, job.verbose, job.timeout)
52 # TODO(jkummerow): We approximate the test suite specific function
53 # IsFailureOutput() by just checking the exit code here. Currently
54 # only cctests define dependencies, for which this simplification is
55 # correct.
56 if dep_output.exit_code != 0:
57 return (job.id, dep_output, time.time() - start_time)
58 output = commands.Execute(job.command, job.verbose, job.timeout)
59 return (job.id, output, time.time() - start_time)
60
61class Runner(object):
62
63 def __init__(self, suites, progress_indicator, context):
64 self.datapath = os.path.join("out", "testrunner_data")
65 self.perf_data_manager = perfdata.PerfDataManager(self.datapath)
66 self.perfdata = self.perf_data_manager.GetStore(context.arch, context.mode)
67 self.perf_failures = False
68 self.printed_allocations = False
69 self.tests = [ t for s in suites for t in s.tests ]
70 if not context.no_sorting:
71 for t in self.tests:
72 t.duration = self.perfdata.FetchPerfData(t) or 1.0
73 self.tests.sort(key=lambda t: t.duration, reverse=True)
74 self._CommonInit(len(self.tests), progress_indicator, context)
75
76 def _CommonInit(self, num_tests, progress_indicator, context):
77 self.indicator = progress_indicator
78 progress_indicator.runner = self
79 self.context = context
80 self.succeeded = 0
81 self.total = num_tests
82 self.remaining = num_tests
83 self.failed = []
84 self.crashed = 0
85 self.reran_tests = 0
86
87 def _RunPerfSafe(self, fun):
88 try:
89 fun()
90 except Exception, e:
91 print("PerfData exception: %s" % e)
92 self.perf_failures = True
93
94 def _GetJob(self, test):
95 command = self.GetCommand(test)
96 timeout = self.context.timeout
97 if ("--stress-opt" in test.flags or
98 "--stress-opt" in self.context.mode_flags or
99 "--stress-opt" in self.context.extra_flags):
100 timeout *= 4
101 if test.dependency is not None:
102 dep_command = [ c.replace(test.path, test.dependency) for c in command ]
103 else:
104 dep_command = None
105 return Job(command, dep_command, test.id, timeout, self.context.verbose)
106
107 def _MaybeRerun(self, pool, test):
108 if test.run <= self.context.rerun_failures_count:
109 # Possibly rerun this test if its run count is below the maximum per
110 # test. <= as the flag controls reruns not including the first run.
111 if test.run == 1:
112 # Count the overall number of reran tests on the first rerun.
113 if self.reran_tests < self.context.rerun_failures_max:
114 self.reran_tests += 1
115 else:
116 # Don't rerun this if the overall number of rerun tests has been
117 # reached.
118 return
119 if test.run >= 2 and test.duration > self.context.timeout / 20.0:
120 # Rerun slow tests at most once.
121 return
122
123 # Rerun this test.
124 test.duration = None
125 test.output = None
126 test.run += 1
127 pool.add([self._GetJob(test)])
128 self.remaining += 1
129
130 def _ProcessTestNormal(self, test, result, pool):
131 self.indicator.AboutToRun(test)
132 test.output = result[1]
133 test.duration = result[2]
134 has_unexpected_output = test.suite.HasUnexpectedOutput(test)
135 if has_unexpected_output:
136 self.failed.append(test)
137 if test.output.HasCrashed():
138 self.crashed += 1
139 else:
140 self.succeeded += 1
141 self.remaining -= 1
142 # For the indicator, everything that happens after the first run is treated
143 # as unexpected even if it flakily passes in order to include it in the
144 # output.
145 self.indicator.HasRun(test, has_unexpected_output or test.run > 1)
146 if has_unexpected_output:
147 # Rerun test failures after the indicator has processed the results.
148 self._MaybeRerun(pool, test)
149 # Update the perf database if the test succeeded.
150 return not has_unexpected_output
151
152 def _ProcessTestPredictable(self, test, result, pool):
153 def HasDifferentAllocations(output1, output2):
154 def AllocationStr(stdout):
155 for line in reversed((stdout or "").splitlines()):
156 if line.startswith("### Allocations = "):
157 self.printed_allocations = True
158 return line
159 return ""
160 return (AllocationStr(output1.stdout) != AllocationStr(output2.stdout))
161
162 # Always pass the test duration for the database update.
163 test.duration = result[2]
164 if test.run == 1 and result[1].HasTimedOut():
165 # If we get a timeout in the first run, we are already in an
166 # unpredictable state. Just report it as a failure and don't rerun.
167 self.indicator.AboutToRun(test)
168 test.output = result[1]
169 self.remaining -= 1
170 self.failed.append(test)
171 self.indicator.HasRun(test, True)
172 if test.run > 1 and HasDifferentAllocations(test.output, result[1]):
173 # From the second run on, check for different allocations. If a
174 # difference is found, call the indicator twice to report both tests.
175 # All runs of each test are counted as one for the statistic.
176 self.indicator.AboutToRun(test)
177 self.remaining -= 1
178 self.failed.append(test)
179 self.indicator.HasRun(test, True)
180 self.indicator.AboutToRun(test)
181 test.output = result[1]
182 self.indicator.HasRun(test, True)
183 elif test.run >= 3:
184 # No difference on the third run -> report a success.
185 self.indicator.AboutToRun(test)
186 self.remaining -= 1
187 self.succeeded += 1
188 test.output = result[1]
189 self.indicator.HasRun(test, False)
190 else:
191 # No difference yet and less than three runs -> add another run and
192 # remember the output for comparison.
193 test.run += 1
194 test.output = result[1]
195 pool.add([self._GetJob(test)])
196 # Always update the perf database.
197 return True
198
199 def Run(self, jobs):
200 self.indicator.Starting()
201 self._RunInternal(jobs)
202 self.indicator.Done()
203 if self.failed or self.remaining:
204 return 1
205 return 0
206
207 def _RunInternal(self, jobs):
208 pool = Pool(jobs)
209 test_map = {}
210 # TODO(machenbach): Instead of filling the queue completely before
211 # pool.imap_unordered, make this a generator that already starts testing
212 # while the queue is filled.
213 queue = []
214 queued_exception = None
215 for test in self.tests:
216 assert test.id >= 0
217 test_map[test.id] = test
218 try:
219 queue.append([self._GetJob(test)])
220 except Exception, e:
221 # If this failed, save the exception and re-raise it later (after
222 # all other tests have had a chance to run).
223 queued_exception = e
224 continue
225 try:
226 it = pool.imap_unordered(RunTest, queue)
227 for result in it:
228 test = test_map[result[0]]
229 if self.context.predictable:
230 update_perf = self._ProcessTestPredictable(test, result, pool)
231 else:
232 update_perf = self._ProcessTestNormal(test, result, pool)
233 if update_perf:
234 self._RunPerfSafe(lambda: self.perfdata.UpdatePerfData(test))
235 finally:
236 pool.terminate()
237 self._RunPerfSafe(lambda: self.perf_data_manager.close())
238 if self.perf_failures:
239 # Nuke perf data in case of failures. This might not work on windows as
240 # some files might still be open.
241 print "Deleting perf test data due to db corruption."
242 shutil.rmtree(self.datapath)
243 if queued_exception:
244 raise queued_exception
245
246 # Make sure that any allocations were printed in predictable mode.
247 assert not self.context.predictable or self.printed_allocations
248
249 def GetCommand(self, test):
250 d8testflag = []
251 shell = test.suite.shell()
252 if shell == "d8":
253 d8testflag = ["--test"]
254 if utils.IsWindows():
255 shell += ".exe"
256 cmd = (self.context.command_prefix +
257 [os.path.abspath(os.path.join(self.context.shell_dir, shell))] +
258 d8testflag +
259 ["--random-seed=%s" % self.context.random_seed] +
260 test.suite.GetFlagsForTestCase(test, self.context) +
261 self.context.extra_flags)
262 return cmd
263
264
265class BreakNowException(Exception):
266 def __init__(self, value):
267 self.value = value
268 def __str__(self):
269 return repr(self.value)