blob: a8ff9f613fbf55daf32e8914ae8128677262b4c3 [file] [log] [blame]
Craig Tillerc2c79212015-02-16 12:00:01 -08001# Copyright 2015, Google Inc.
2# All rights reserved.
3#
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 disclaimer
12# in the documentation and/or other materials provided with the
13# distribution.
14# * Neither the name of Google Inc. nor the names of its
15# contributors may be used to endorse or promote products derived from
16# 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
Nicolas Nobleddef2462015-01-06 18:08:25 -080030"""Run a group of subprocesses and then finish."""
31
Craig Tiller71735182015-01-15 17:07:13 -080032import hashlib
Nicolas Nobleddef2462015-01-06 18:08:25 -080033import multiprocessing
Craig Tiller71735182015-01-15 17:07:13 -080034import os
Craig Tiller5058c692015-04-08 09:42:04 -070035import platform
Craig Tiller336ad502015-02-24 14:46:02 -080036import signal
Nicolas "Pixel" Noble5937b5b2015-06-26 02:04:12 +020037import string
Nicolas Nobleddef2462015-01-06 18:08:25 -080038import subprocess
39import sys
ctiller3040cb72015-01-07 12:13:17 -080040import tempfile
41import time
Nicolas "Pixel" Noble5937b5b2015-06-26 02:04:12 +020042import xml.etree.cElementTree as ET
Nicolas Nobleddef2462015-01-06 18:08:25 -080043
ctiller3040cb72015-01-07 12:13:17 -080044
ctiller94e5dde2015-01-09 10:41:59 -080045_DEFAULT_MAX_JOBS = 16 * multiprocessing.cpu_count()
Nicolas Nobleddef2462015-01-06 18:08:25 -080046
47
Craig Tiller336ad502015-02-24 14:46:02 -080048# setup a signal handler so that signal.pause registers 'something'
49# when a child finishes
50# not using futures and threading to avoid a dependency on subprocess32
Adele Zhoue4c35612015-10-16 15:34:23 -070051if platform.system() == 'Windows':
Craig Tiller5058c692015-04-08 09:42:04 -070052 pass
53else:
54 have_alarm = False
55 def alarm_handler(unused_signum, unused_frame):
56 global have_alarm
57 have_alarm = False
58
59 signal.signal(signal.SIGCHLD, lambda unused_signum, unused_frame: None)
60 signal.signal(signal.SIGALRM, alarm_handler)
Craig Tiller336ad502015-02-24 14:46:02 -080061
62
ctiller3040cb72015-01-07 12:13:17 -080063_SUCCESS = object()
64_FAILURE = object()
65_RUNNING = object()
66_KILLED = object()
67
68
Craig Tiller3b083062015-01-12 13:51:28 -080069_COLORS = {
Nicolas Noble044db742015-01-14 16:57:24 -080070 'red': [ 31, 0 ],
71 'green': [ 32, 0 ],
72 'yellow': [ 33, 0 ],
73 'lightgray': [ 37, 0],
74 'gray': [ 30, 1 ],
Craig Tillerd7e09c32015-09-25 11:33:39 -070075 'purple': [ 35, 0 ],
Craig Tiller3b083062015-01-12 13:51:28 -080076 }
77
78
79_BEGINNING_OF_LINE = '\x1b[0G'
80_CLEAR_LINE = '\x1b[2K'
81
82
83_TAG_COLOR = {
84 'FAILED': 'red',
Craig Tillerd7e09c32015-09-25 11:33:39 -070085 'FLAKE': 'purple',
Craig Tiller3dc1e4f2015-09-25 11:46:56 -070086 'TIMEOUT_FLAKE': 'purple',
Masood Malekghassemie5f70022015-06-29 09:20:26 -070087 'WARNING': 'yellow',
Craig Tillere1d0d1c2015-02-27 08:54:23 -080088 'TIMEOUT': 'red',
Craig Tiller3b083062015-01-12 13:51:28 -080089 'PASSED': 'green',
Nicolas Noble044db742015-01-14 16:57:24 -080090 'START': 'gray',
Craig Tiller3b083062015-01-12 13:51:28 -080091 'WAITING': 'yellow',
Nicolas Noble044db742015-01-14 16:57:24 -080092 'SUCCESS': 'green',
93 'IDLE': 'gray',
Craig Tiller3b083062015-01-12 13:51:28 -080094 }
95
96
Nicolas "Pixel" Noble99768ac2015-05-13 02:34:06 +020097def message(tag, msg, explanatory_text=None, do_newline=False):
98 if message.old_tag == tag and message.old_msg == msg and not explanatory_text:
99 return
100 message.old_tag = tag
101 message.old_msg = msg
Craig Tiller23d2f3f2015-02-24 15:23:32 -0800102 try:
Craig Tiller9f3b2d72015-08-25 11:50:57 -0700103 if platform.system() == 'Windows' or not sys.stdout.isatty():
104 if explanatory_text:
105 print explanatory_text
106 print '%s: %s' % (tag, msg)
107 return
vjpaia29d2d72015-07-08 10:31:15 -0700108 sys.stdout.write('%s%s%s\x1b[%d;%dm%s\x1b[0m: %s%s' % (
109 _BEGINNING_OF_LINE,
110 _CLEAR_LINE,
111 '\n%s' % explanatory_text if explanatory_text is not None else '',
112 _COLORS[_TAG_COLOR[tag]][1],
113 _COLORS[_TAG_COLOR[tag]][0],
114 tag,
115 msg,
116 '\n' if do_newline or explanatory_text is not None else ''))
Craig Tiller23d2f3f2015-02-24 15:23:32 -0800117 sys.stdout.flush()
118 except:
119 pass
Craig Tiller3b083062015-01-12 13:51:28 -0800120
Adele Zhoue4c35612015-10-16 15:34:23 -0700121message.old_tag = ''
122message.old_msg = ''
Craig Tiller3b083062015-01-12 13:51:28 -0800123
Craig Tiller71735182015-01-15 17:07:13 -0800124def which(filename):
125 if '/' in filename:
126 return filename
127 for path in os.environ['PATH'].split(os.pathsep):
128 if os.path.exists(os.path.join(path, filename)):
129 return os.path.join(path, filename)
130 raise Exception('%s not found' % filename)
131
132
Jan Tattermusch3ea43622015-10-14 16:35:15 -0700133def _filter_stdout(stdout):
134 """Filters out nonprintable and XML-illegal characters from stdout."""
135 # keep whitespaces but remove formfeed and vertical tab characters
136 # that make XML report unparseable.
137 return filter(lambda x: x in string.printable and x != '\f' and x != '\v',
138 stdout.decode(errors='ignore'))
139
140
Craig Tiller547db2b2015-01-30 14:08:39 -0800141class JobSpec(object):
142 """Specifies what to run for a job."""
143
Jan Tattermusch725835a2015-08-01 21:02:35 -0700144 def __init__(self, cmdline, shortname=None, environ=None, hash_targets=None,
Craig Tiller95cc07b2015-09-28 13:41:30 -0700145 cwd=None, shell=False, timeout_seconds=5*60, flake_retries=0,
Jan Tattermusche2686282015-10-08 16:27:07 -0700146 timeout_retries=0, kill_handler=None):
Craig Tiller547db2b2015-01-30 14:08:39 -0800147 """
148 Arguments:
149 cmdline: a list of arguments to pass as the command line
150 environ: a dictionary of environment variables to set in the child process
151 hash_targets: which files to include in the hash representing the jobs version
152 (or empty, indicating the job should not be hashed)
Jan Tattermusche2686282015-10-08 16:27:07 -0700153 kill_handler: a handler that will be called whenever job.kill() is invoked
Craig Tiller547db2b2015-01-30 14:08:39 -0800154 """
murgatroid99132ce6a2015-03-04 17:29:14 -0800155 if environ is None:
156 environ = {}
157 if hash_targets is None:
158 hash_targets = []
Craig Tiller547db2b2015-01-30 14:08:39 -0800159 self.cmdline = cmdline
160 self.environ = environ
161 self.shortname = cmdline[0] if shortname is None else shortname
162 self.hash_targets = hash_targets or []
Craig Tiller5058c692015-04-08 09:42:04 -0700163 self.cwd = cwd
Jan Tattermusche8243592015-04-17 14:14:01 -0700164 self.shell = shell
Jan Tattermusch725835a2015-08-01 21:02:35 -0700165 self.timeout_seconds = timeout_seconds
Craig Tiller91318bc2015-09-24 08:58:39 -0700166 self.flake_retries = flake_retries
Craig Tillerbfc8a062015-09-28 14:40:21 -0700167 self.timeout_retries = timeout_retries
Jan Tattermusche2686282015-10-08 16:27:07 -0700168 self.kill_handler = kill_handler
Craig Tiller547db2b2015-01-30 14:08:39 -0800169
170 def identity(self):
171 return '%r %r %r' % (self.cmdline, self.environ, self.hash_targets)
172
173 def __hash__(self):
174 return hash(self.identity())
175
176 def __cmp__(self, other):
177 return self.identity() == other.identity()
178
179
Adele Zhoue4c35612015-10-16 15:34:23 -0700180class JobResult(object):
181 def __init__(self):
182 self.state = 'UNKNOWN'
183 self.returncode = -1
184 self.elapsed_time = 0
Adele Zhoud5fffa52015-10-23 15:51:42 -0700185 self.num_failures = 0
Adele Zhoue4c35612015-10-16 15:34:23 -0700186 self.retries = 0
187 self.message = ''
188
189
ctiller3040cb72015-01-07 12:13:17 -0800190class Job(object):
191 """Manages one job."""
192
Craig Tillerf53d9c82015-08-04 14:19:43 -0700193 def __init__(self, spec, bin_hash, newline_on_success, travis, add_env, xml_report):
Craig Tiller547db2b2015-01-30 14:08:39 -0800194 self._spec = spec
Craig Tiller71735182015-01-15 17:07:13 -0800195 self._bin_hash = bin_hash
Nicolas Noble044db742015-01-14 16:57:24 -0800196 self._newline_on_success = newline_on_success
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100197 self._travis = travis
Craig Tiller91318bc2015-09-24 08:58:39 -0700198 self._add_env = add_env.copy()
Nicolas "Pixel" Noble5937b5b2015-06-26 02:04:12 +0200199 self._xml_test = ET.SubElement(xml_report, 'testcase',
200 name=self._spec.shortname) if xml_report is not None else None
Craig Tiller91318bc2015-09-24 08:58:39 -0700201 self._retries = 0
Craig Tiller95cc07b2015-09-28 13:41:30 -0700202 self._timeout_retries = 0
Jan Tattermusch91ad0182015-10-01 09:22:03 -0700203 self._suppress_failure_message = False
Craig Tillerb84728d2015-02-26 15:40:39 -0800204 message('START', spec.shortname, do_newline=self._travis)
Adele Zhoue4c35612015-10-16 15:34:23 -0700205 self.result = JobResult()
Craig Tiller91318bc2015-09-24 08:58:39 -0700206 self.start()
207
Adele Zhoue4c35612015-10-16 15:34:23 -0700208 def GetSpec(self):
209 return self._spec
210
Craig Tiller91318bc2015-09-24 08:58:39 -0700211 def start(self):
212 self._tempfile = tempfile.TemporaryFile()
213 env = dict(os.environ)
214 env.update(self._spec.environ)
215 env.update(self._add_env)
216 self._start = time.time()
217 self._process = subprocess.Popen(args=self._spec.cmdline,
218 stderr=subprocess.STDOUT,
219 stdout=self._tempfile,
220 cwd=self._spec.cwd,
221 shell=self._spec.shell,
222 env=env)
223 self._state = _RUNNING
ctiller3040cb72015-01-07 12:13:17 -0800224
Craig Tiller71735182015-01-15 17:07:13 -0800225 def state(self, update_cache):
ctiller3040cb72015-01-07 12:13:17 -0800226 """Poll current state of the job. Prints messages at completion."""
227 if self._state == _RUNNING and self._process.poll() is not None:
Craig Tiller9d6139a2015-02-26 15:24:43 -0800228 elapsed = time.time() - self._start
Nicolas "Pixel" Noble5937b5b2015-06-26 02:04:12 +0200229 self._tempfile.seek(0)
230 stdout = self._tempfile.read()
Jan Tattermusch3ea43622015-10-14 16:35:15 -0700231 filtered_stdout = _filter_stdout(stdout)
Nicolas "Pixel" Noble4a5a8f32015-08-13 19:43:00 +0200232 # TODO: looks like jenkins master is slow because parsing the junit results XMLs is not
233 # implemented efficiently. This is an experiment to workaround the issue by making sure
234 # results.xml file is small enough.
235 filtered_stdout = filtered_stdout[-128:]
Adele Zhoue4c35612015-10-16 15:34:23 -0700236 self.result.message = filtered_stdout
237 self.result.elapsed_time = elapsed
Nicolas "Pixel" Noble5937b5b2015-06-26 02:04:12 +0200238 if self._xml_test is not None:
239 self._xml_test.set('time', str(elapsed))
240 ET.SubElement(self._xml_test, 'system-out').text = filtered_stdout
ctiller3040cb72015-01-07 12:13:17 -0800241 if self._process.returncode != 0:
Craig Tiller91318bc2015-09-24 08:58:39 -0700242 if self._retries < self._spec.flake_retries:
243 message('FLAKE', '%s [ret=%d, pid=%d]' % (
Craig Tillerd0ffe142015-05-19 21:51:13 -0700244 self._spec.shortname, self._process.returncode, self._process.pid),
245 stdout, do_newline=True)
Craig Tiller91318bc2015-09-24 08:58:39 -0700246 self._retries += 1
Adele Zhoud5fffa52015-10-23 15:51:42 -0700247 self.result.num_failures += 1
Adele Zhoue4c35612015-10-16 15:34:23 -0700248 self.result.retries = self._timeout_retries + self._retries
Craig Tiller91318bc2015-09-24 08:58:39 -0700249 self.start()
250 else:
251 self._state = _FAILURE
Jan Tattermusch91ad0182015-10-01 09:22:03 -0700252 if not self._suppress_failure_message:
253 message('FAILED', '%s [ret=%d, pid=%d]' % (
254 self._spec.shortname, self._process.returncode, self._process.pid),
255 stdout, do_newline=True)
Adele Zhoue4c35612015-10-16 15:34:23 -0700256 self.result.state = 'FAILED'
Adele Zhoud5fffa52015-10-23 15:51:42 -0700257 self.result.num_failures += 1
Adele Zhoue4c35612015-10-16 15:34:23 -0700258 self.result.returncode = self._process.returncode
Craig Tiller91318bc2015-09-24 08:58:39 -0700259 if self._xml_test is not None:
Jan Tattermusch3ea43622015-10-14 16:35:15 -0700260 ET.SubElement(self._xml_test, 'failure', message='Failure')
ctiller3040cb72015-01-07 12:13:17 -0800261 else:
262 self._state = _SUCCESS
Craig Tiller95cc07b2015-09-28 13:41:30 -0700263 message('PASSED', '%s [time=%.1fsec; retries=%d;%d]' % (
264 self._spec.shortname, elapsed, self._retries, self._timeout_retries),
265 do_newline=self._newline_on_success or self._travis)
Adele Zhoue4c35612015-10-16 15:34:23 -0700266 self.result.state = 'PASSED'
Craig Tiller547db2b2015-01-30 14:08:39 -0800267 if self._bin_hash:
268 update_cache.finished(self._spec.identity(), self._bin_hash)
Jan Tattermusch725835a2015-08-01 21:02:35 -0700269 elif self._state == _RUNNING and time.time() - self._start > self._spec.timeout_seconds:
Craig Tiller84216782015-05-12 09:43:54 -0700270 self._tempfile.seek(0)
271 stdout = self._tempfile.read()
Jan Tattermusch3ea43622015-10-14 16:35:15 -0700272 filtered_stdout = _filter_stdout(stdout)
Adele Zhoue4c35612015-10-16 15:34:23 -0700273 self.result.message = filtered_stdout
Craig Tiller95cc07b2015-09-28 13:41:30 -0700274 if self._timeout_retries < self._spec.timeout_retries:
Craig Tiller3dc1e4f2015-09-25 11:46:56 -0700275 message('TIMEOUT_FLAKE', self._spec.shortname, stdout, do_newline=True)
Craig Tiller95cc07b2015-09-28 13:41:30 -0700276 self._timeout_retries += 1
Adele Zhoud5fffa52015-10-23 15:51:42 -0700277 self.result.num_failures += 1
Adele Zhoue4c35612015-10-16 15:34:23 -0700278 self.result.retries = self._timeout_retries + self._retries
Jan Tattermusch39e3cb32015-10-22 18:21:08 -0700279 if self._spec.kill_handler:
280 self._spec.kill_handler(self)
Craig Tiller3dc1e4f2015-09-25 11:46:56 -0700281 self._process.terminate()
282 self.start()
283 else:
284 message('TIMEOUT', self._spec.shortname, stdout, do_newline=True)
285 self.kill()
Adele Zhoue4c35612015-10-16 15:34:23 -0700286 self.result.state = 'TIMEOUT'
Adele Zhoud5fffa52015-10-23 15:51:42 -0700287 self.result.num_failures += 1
Craig Tiller3dc1e4f2015-09-25 11:46:56 -0700288 if self._xml_test is not None:
289 ET.SubElement(self._xml_test, 'system-out').text = filtered_stdout
290 ET.SubElement(self._xml_test, 'error', message='Timeout')
ctiller3040cb72015-01-07 12:13:17 -0800291 return self._state
292
293 def kill(self):
294 if self._state == _RUNNING:
295 self._state = _KILLED
Jan Tattermusche2686282015-10-08 16:27:07 -0700296 if self._spec.kill_handler:
297 self._spec.kill_handler(self)
ctiller3040cb72015-01-07 12:13:17 -0800298 self._process.terminate()
299
Jan Tattermusch91ad0182015-10-01 09:22:03 -0700300 def suppress_failure_message(self):
301 self._suppress_failure_message = True
Adele Zhoud5fffa52015-10-23 15:51:42 -0700302
ctiller3040cb72015-01-07 12:13:17 -0800303
Nicolas Nobleddef2462015-01-06 18:08:25 -0800304class Jobset(object):
305 """Manages one run of jobs."""
306
Craig Tiller533b1a22015-05-29 08:41:29 -0700307 def __init__(self, check_cancelled, maxjobs, newline_on_success, travis,
Craig Tillerf53d9c82015-08-04 14:19:43 -0700308 stop_on_failure, add_env, cache, xml_report):
ctiller3040cb72015-01-07 12:13:17 -0800309 self._running = set()
310 self._check_cancelled = check_cancelled
311 self._cancelled = False
Nicolas Nobleddef2462015-01-06 18:08:25 -0800312 self._failures = 0
Craig Tiller738c3342015-01-12 14:28:33 -0800313 self._completed = 0
ctiller94e5dde2015-01-09 10:41:59 -0800314 self._maxjobs = maxjobs
Nicolas Noble044db742015-01-14 16:57:24 -0800315 self._newline_on_success = newline_on_success
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100316 self._travis = travis
Craig Tiller71735182015-01-15 17:07:13 -0800317 self._cache = cache
Craig Tiller533b1a22015-05-29 08:41:29 -0700318 self._stop_on_failure = stop_on_failure
Craig Tiller74e770d2015-06-11 09:38:09 -0700319 self._hashes = {}
Nicolas "Pixel" Noble5937b5b2015-06-26 02:04:12 +0200320 self._xml_report = xml_report
Craig Tillerf53d9c82015-08-04 14:19:43 -0700321 self._add_env = add_env
Adele Zhoue4c35612015-10-16 15:34:23 -0700322 self.resultset = {}
323
324 def get_num_failures(self):
325 return self._failures
Nicolas Nobleddef2462015-01-06 18:08:25 -0800326
Craig Tiller547db2b2015-01-30 14:08:39 -0800327 def start(self, spec):
ctiller3040cb72015-01-07 12:13:17 -0800328 """Start a job. Return True on success, False on failure."""
ctiller94e5dde2015-01-09 10:41:59 -0800329 while len(self._running) >= self._maxjobs:
ctiller3040cb72015-01-07 12:13:17 -0800330 if self.cancelled(): return False
331 self.reap()
332 if self.cancelled(): return False
Craig Tiller547db2b2015-01-30 14:08:39 -0800333 if spec.hash_targets:
Craig Tiller74e770d2015-06-11 09:38:09 -0700334 if spec.identity() in self._hashes:
335 bin_hash = self._hashes[spec.identity()]
336 else:
337 bin_hash = hashlib.sha1()
338 for fn in spec.hash_targets:
339 with open(which(fn)) as f:
340 bin_hash.update(f.read())
341 bin_hash = bin_hash.hexdigest()
342 self._hashes[spec.identity()] = bin_hash
Craig Tiller547db2b2015-01-30 14:08:39 -0800343 should_run = self._cache.should_run(spec.identity(), bin_hash)
344 else:
345 bin_hash = None
346 should_run = True
347 if should_run:
Adele Zhoue4c35612015-10-16 15:34:23 -0700348 job = Job(spec,
349 bin_hash,
350 self._newline_on_success,
351 self._travis,
352 self._add_env,
353 self._xml_report)
354 self._running.add(job)
Adele Zhoud5fffa52015-10-23 15:51:42 -0700355 self.resultset[job.GetSpec().shortname] = []
ctiller3040cb72015-01-07 12:13:17 -0800356 return True
Nicolas Nobleddef2462015-01-06 18:08:25 -0800357
ctiller3040cb72015-01-07 12:13:17 -0800358 def reap(self):
359 """Collect the dead jobs."""
360 while self._running:
361 dead = set()
362 for job in self._running:
Craig Tiller71735182015-01-15 17:07:13 -0800363 st = job.state(self._cache)
ctiller3040cb72015-01-07 12:13:17 -0800364 if st == _RUNNING: continue
Craig Tiller533b1a22015-05-29 08:41:29 -0700365 if st == _FAILURE or st == _KILLED:
366 self._failures += 1
367 if self._stop_on_failure:
368 self._cancelled = True
369 for job in self._running:
370 job.kill()
ctiller3040cb72015-01-07 12:13:17 -0800371 dead.add(job)
Craig Tiller74e770d2015-06-11 09:38:09 -0700372 break
ctiller3040cb72015-01-07 12:13:17 -0800373 for job in dead:
Craig Tiller738c3342015-01-12 14:28:33 -0800374 self._completed += 1
Adele Zhoud5fffa52015-10-23 15:51:42 -0700375 self.resultset[job.GetSpec().shortname].append(job.result)
ctiller3040cb72015-01-07 12:13:17 -0800376 self._running.remove(job)
Craig Tiller3b083062015-01-12 13:51:28 -0800377 if dead: return
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100378 if (not self._travis):
379 message('WAITING', '%d jobs running, %d complete, %d failed' % (
380 len(self._running), self._completed, self._failures))
Craig Tiller5058c692015-04-08 09:42:04 -0700381 if platform.system() == 'Windows':
382 time.sleep(0.1)
383 else:
384 global have_alarm
385 if not have_alarm:
386 have_alarm = True
387 signal.alarm(10)
388 signal.pause()
ctiller3040cb72015-01-07 12:13:17 -0800389
390 def cancelled(self):
391 """Poll for cancellation."""
392 if self._cancelled: return True
393 if not self._check_cancelled(): return False
394 for job in self._running:
395 job.kill()
396 self._cancelled = True
397 return True
398
399 def finish(self):
400 while self._running:
401 if self.cancelled(): pass # poll cancellation
402 self.reap()
403 return not self.cancelled() and self._failures == 0
Nicolas Nobleddef2462015-01-06 18:08:25 -0800404
405
ctiller3040cb72015-01-07 12:13:17 -0800406def _never_cancelled():
407 return False
408
409
Craig Tiller71735182015-01-15 17:07:13 -0800410# cache class that caches nothing
411class NoCache(object):
412 def should_run(self, cmdline, bin_hash):
413 return True
414
415 def finished(self, cmdline, bin_hash):
416 pass
417
418
Nicolas Nobleb09078f2015-01-14 18:06:05 -0800419def run(cmdlines,
420 check_cancelled=_never_cancelled,
421 maxjobs=None,
Craig Tiller71735182015-01-15 17:07:13 -0800422 newline_on_success=False,
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100423 travis=False,
David Garcia Quintase90cd372015-05-31 18:15:26 -0700424 infinite_runs=False,
Craig Tiller533b1a22015-05-29 08:41:29 -0700425 stop_on_failure=False,
Nicolas "Pixel" Noble5937b5b2015-06-26 02:04:12 +0200426 cache=None,
Craig Tillerf53d9c82015-08-04 14:19:43 -0700427 xml_report=None,
428 add_env={}):
ctiller94e5dde2015-01-09 10:41:59 -0800429 js = Jobset(check_cancelled,
Nicolas Noble044db742015-01-14 16:57:24 -0800430 maxjobs if maxjobs is not None else _DEFAULT_MAX_JOBS,
Craig Tillerf53d9c82015-08-04 14:19:43 -0700431 newline_on_success, travis, stop_on_failure, add_env,
Nicolas "Pixel" Noble5937b5b2015-06-26 02:04:12 +0200432 cache if cache is not None else NoCache(),
433 xml_report)
Craig Tillerb84728d2015-02-26 15:40:39 -0800434 for cmdline in cmdlines:
ctiller3040cb72015-01-07 12:13:17 -0800435 if not js.start(cmdline):
436 break
Adele Zhoue4c35612015-10-16 15:34:23 -0700437 js.finish()
438 return js.get_num_failures(), js.resultset