blob: 748c06dfbab14461a76bef4c32274d54622e1f41 [file] [log] [blame]
murgatroid993466c4b2016-01-12 10:26:04 -08001# Copyright 2015-2016, Google Inc.
Craig Tillerc2c79212015-02-16 12:00:01 -08002# 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 Nobleddef2462015-01-06 18:08:25 -080037import subprocess
38import sys
ctiller3040cb72015-01-07 12:13:17 -080039import tempfile
40import time
Nicolas Nobleddef2462015-01-06 18:08:25 -080041
ctiller3040cb72015-01-07 12:13:17 -080042
ctiller94e5dde2015-01-09 10:41:59 -080043_DEFAULT_MAX_JOBS = 16 * multiprocessing.cpu_count()
Adele Zhoud01cbe32015-11-02 14:20:43 -080044_MAX_RESULT_SIZE = 8192
Nicolas Nobleddef2462015-01-06 18:08:25 -080045
Nicolas "Pixel" Noblef72d7b52015-12-03 03:07:43 +010046def platform_string():
47 if platform.system() == 'Windows':
48 return 'windows'
49 elif platform.system()[:7] == 'MSYS_NT':
50 return 'windows'
51 elif platform.system() == 'Darwin':
52 return 'mac'
53 elif platform.system() == 'Linux':
54 return 'linux'
55 else:
56 return 'posix'
57
Nicolas Nobleddef2462015-01-06 18:08:25 -080058
Craig Tiller336ad502015-02-24 14:46:02 -080059# setup a signal handler so that signal.pause registers 'something'
60# when a child finishes
61# not using futures and threading to avoid a dependency on subprocess32
Nicolas "Pixel" Noblef72d7b52015-12-03 03:07:43 +010062if platform_string() == 'windows':
Craig Tiller5058c692015-04-08 09:42:04 -070063 pass
64else:
65 have_alarm = False
66 def alarm_handler(unused_signum, unused_frame):
67 global have_alarm
68 have_alarm = False
69
70 signal.signal(signal.SIGCHLD, lambda unused_signum, unused_frame: None)
71 signal.signal(signal.SIGALRM, alarm_handler)
Craig Tiller336ad502015-02-24 14:46:02 -080072
73
ctiller3040cb72015-01-07 12:13:17 -080074_SUCCESS = object()
75_FAILURE = object()
76_RUNNING = object()
77_KILLED = object()
78
79
Craig Tiller3b083062015-01-12 13:51:28 -080080_COLORS = {
Nicolas Noble044db742015-01-14 16:57:24 -080081 'red': [ 31, 0 ],
82 'green': [ 32, 0 ],
83 'yellow': [ 33, 0 ],
84 'lightgray': [ 37, 0],
85 'gray': [ 30, 1 ],
Craig Tillerd7e09c32015-09-25 11:33:39 -070086 'purple': [ 35, 0 ],
Craig Tiller3b083062015-01-12 13:51:28 -080087 }
88
89
90_BEGINNING_OF_LINE = '\x1b[0G'
91_CLEAR_LINE = '\x1b[2K'
92
93
94_TAG_COLOR = {
95 'FAILED': 'red',
Craig Tillerd7e09c32015-09-25 11:33:39 -070096 'FLAKE': 'purple',
Craig Tiller3dc1e4f2015-09-25 11:46:56 -070097 'TIMEOUT_FLAKE': 'purple',
Masood Malekghassemie5f70022015-06-29 09:20:26 -070098 'WARNING': 'yellow',
Craig Tillere1d0d1c2015-02-27 08:54:23 -080099 'TIMEOUT': 'red',
Craig Tiller3b083062015-01-12 13:51:28 -0800100 'PASSED': 'green',
Nicolas Noble044db742015-01-14 16:57:24 -0800101 'START': 'gray',
Craig Tiller3b083062015-01-12 13:51:28 -0800102 'WAITING': 'yellow',
Nicolas Noble044db742015-01-14 16:57:24 -0800103 'SUCCESS': 'green',
104 'IDLE': 'gray',
Craig Tiller3b083062015-01-12 13:51:28 -0800105 }
106
107
Nicolas "Pixel" Noble99768ac2015-05-13 02:34:06 +0200108def message(tag, msg, explanatory_text=None, do_newline=False):
109 if message.old_tag == tag and message.old_msg == msg and not explanatory_text:
110 return
111 message.old_tag = tag
112 message.old_msg = msg
Craig Tiller23d2f3f2015-02-24 15:23:32 -0800113 try:
Nicolas "Pixel" Noblef72d7b52015-12-03 03:07:43 +0100114 if platform_string() == 'windows' or not sys.stdout.isatty():
Craig Tiller9f3b2d72015-08-25 11:50:57 -0700115 if explanatory_text:
116 print explanatory_text
117 print '%s: %s' % (tag, msg)
118 return
vjpaia29d2d72015-07-08 10:31:15 -0700119 sys.stdout.write('%s%s%s\x1b[%d;%dm%s\x1b[0m: %s%s' % (
120 _BEGINNING_OF_LINE,
121 _CLEAR_LINE,
122 '\n%s' % explanatory_text if explanatory_text is not None else '',
123 _COLORS[_TAG_COLOR[tag]][1],
124 _COLORS[_TAG_COLOR[tag]][0],
125 tag,
126 msg,
127 '\n' if do_newline or explanatory_text is not None else ''))
Craig Tiller23d2f3f2015-02-24 15:23:32 -0800128 sys.stdout.flush()
129 except:
130 pass
Craig Tiller3b083062015-01-12 13:51:28 -0800131
Adele Zhoue4c35612015-10-16 15:34:23 -0700132message.old_tag = ''
133message.old_msg = ''
Craig Tiller3b083062015-01-12 13:51:28 -0800134
Craig Tiller71735182015-01-15 17:07:13 -0800135def which(filename):
136 if '/' in filename:
137 return filename
138 for path in os.environ['PATH'].split(os.pathsep):
139 if os.path.exists(os.path.join(path, filename)):
140 return os.path.join(path, filename)
141 raise Exception('%s not found' % filename)
142
143
Craig Tiller547db2b2015-01-30 14:08:39 -0800144class JobSpec(object):
145 """Specifies what to run for a job."""
146
Jan Tattermusch725835a2015-08-01 21:02:35 -0700147 def __init__(self, cmdline, shortname=None, environ=None, hash_targets=None,
Craig Tiller95cc07b2015-09-28 13:41:30 -0700148 cwd=None, shell=False, timeout_seconds=5*60, flake_retries=0,
Jan Tattermusche2686282015-10-08 16:27:07 -0700149 timeout_retries=0, kill_handler=None):
Craig Tiller547db2b2015-01-30 14:08:39 -0800150 """
151 Arguments:
152 cmdline: a list of arguments to pass as the command line
153 environ: a dictionary of environment variables to set in the child process
154 hash_targets: which files to include in the hash representing the jobs version
155 (or empty, indicating the job should not be hashed)
Jan Tattermusche2686282015-10-08 16:27:07 -0700156 kill_handler: a handler that will be called whenever job.kill() is invoked
Craig Tiller547db2b2015-01-30 14:08:39 -0800157 """
murgatroid99132ce6a2015-03-04 17:29:14 -0800158 if environ is None:
159 environ = {}
160 if hash_targets is None:
161 hash_targets = []
Craig Tiller547db2b2015-01-30 14:08:39 -0800162 self.cmdline = cmdline
163 self.environ = environ
164 self.shortname = cmdline[0] if shortname is None else shortname
165 self.hash_targets = hash_targets or []
Craig Tiller5058c692015-04-08 09:42:04 -0700166 self.cwd = cwd
Jan Tattermusche8243592015-04-17 14:14:01 -0700167 self.shell = shell
Jan Tattermusch725835a2015-08-01 21:02:35 -0700168 self.timeout_seconds = timeout_seconds
Craig Tiller91318bc2015-09-24 08:58:39 -0700169 self.flake_retries = flake_retries
Craig Tillerbfc8a062015-09-28 14:40:21 -0700170 self.timeout_retries = timeout_retries
Jan Tattermusche2686282015-10-08 16:27:07 -0700171 self.kill_handler = kill_handler
Craig Tiller547db2b2015-01-30 14:08:39 -0800172
173 def identity(self):
174 return '%r %r %r' % (self.cmdline, self.environ, self.hash_targets)
175
176 def __hash__(self):
177 return hash(self.identity())
178
179 def __cmp__(self, other):
180 return self.identity() == other.identity()
Craig Tillerdb218992016-01-05 09:28:46 -0800181
Jan Tattermusch2dd156e2015-12-04 18:26:17 -0800182 def __repr__(self):
183 return 'JobSpec(shortname=%s, cmdline=%s)' % (self.shortname, self.cmdline)
Craig Tiller547db2b2015-01-30 14:08:39 -0800184
185
Adele Zhoue4c35612015-10-16 15:34:23 -0700186class JobResult(object):
187 def __init__(self):
188 self.state = 'UNKNOWN'
189 self.returncode = -1
190 self.elapsed_time = 0
Adele Zhoud5fffa52015-10-23 15:51:42 -0700191 self.num_failures = 0
Adele Zhoue4c35612015-10-16 15:34:23 -0700192 self.retries = 0
193 self.message = ''
Craig Tillerdb218992016-01-05 09:28:46 -0800194
Adele Zhoue4c35612015-10-16 15:34:23 -0700195
ctiller3040cb72015-01-07 12:13:17 -0800196class Job(object):
197 """Manages one job."""
198
Adele Zhou2271ab52015-10-28 13:59:14 -0700199 def __init__(self, spec, bin_hash, newline_on_success, travis, add_env):
Craig Tiller547db2b2015-01-30 14:08:39 -0800200 self._spec = spec
Craig Tiller71735182015-01-15 17:07:13 -0800201 self._bin_hash = bin_hash
Nicolas Noble044db742015-01-14 16:57:24 -0800202 self._newline_on_success = newline_on_success
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100203 self._travis = travis
Craig Tiller91318bc2015-09-24 08:58:39 -0700204 self._add_env = add_env.copy()
Craig Tiller91318bc2015-09-24 08:58:39 -0700205 self._retries = 0
Craig Tiller95cc07b2015-09-28 13:41:30 -0700206 self._timeout_retries = 0
Jan Tattermusch91ad0182015-10-01 09:22:03 -0700207 self._suppress_failure_message = False
Craig Tillerb84728d2015-02-26 15:40:39 -0800208 message('START', spec.shortname, do_newline=self._travis)
Adele Zhoue4c35612015-10-16 15:34:23 -0700209 self.result = JobResult()
Craig Tiller91318bc2015-09-24 08:58:39 -0700210 self.start()
211
Adele Zhoue4c35612015-10-16 15:34:23 -0700212 def GetSpec(self):
213 return self._spec
214
Craig Tiller91318bc2015-09-24 08:58:39 -0700215 def start(self):
216 self._tempfile = tempfile.TemporaryFile()
217 env = dict(os.environ)
218 env.update(self._spec.environ)
219 env.update(self._add_env)
220 self._start = time.time()
Craig Tiller60078bb2015-11-04 16:39:54 -0800221 try_start = lambda: subprocess.Popen(args=self._spec.cmdline,
222 stderr=subprocess.STDOUT,
223 stdout=self._tempfile,
224 cwd=self._spec.cwd,
225 shell=self._spec.shell,
226 env=env)
227 delay = 0.3
228 for i in range(0, 4):
229 try:
230 self._process = try_start()
231 break
232 except OSError:
233 message('WARNING', 'Failed to start %s, retrying in %f seconds' % (self._spec.shortname, delay))
234 time.sleep(delay)
235 delay *= 2
236 else:
237 self._process = try_start()
Craig Tiller91318bc2015-09-24 08:58:39 -0700238 self._state = _RUNNING
ctiller3040cb72015-01-07 12:13:17 -0800239
Craig Tiller71735182015-01-15 17:07:13 -0800240 def state(self, update_cache):
ctiller3040cb72015-01-07 12:13:17 -0800241 """Poll current state of the job. Prints messages at completion."""
Craig Tillerdb218992016-01-05 09:28:46 -0800242 def stdout(self=self):
243 self._tempfile.seek(0)
244 stdout = self._tempfile.read()
245 self.result.message = stdout[-_MAX_RESULT_SIZE:]
246 return stdout
ctiller3040cb72015-01-07 12:13:17 -0800247 if self._state == _RUNNING and self._process.poll() is not None:
Craig Tiller9d6139a2015-02-26 15:24:43 -0800248 elapsed = time.time() - self._start
Adele Zhoue4c35612015-10-16 15:34:23 -0700249 self.result.elapsed_time = elapsed
ctiller3040cb72015-01-07 12:13:17 -0800250 if self._process.returncode != 0:
Craig Tiller91318bc2015-09-24 08:58:39 -0700251 if self._retries < self._spec.flake_retries:
252 message('FLAKE', '%s [ret=%d, pid=%d]' % (
Craig Tillerd0ffe142015-05-19 21:51:13 -0700253 self._spec.shortname, self._process.returncode, self._process.pid),
Craig Tillerdb218992016-01-05 09:28:46 -0800254 stdout(), do_newline=True)
Craig Tiller91318bc2015-09-24 08:58:39 -0700255 self._retries += 1
Adele Zhoud5fffa52015-10-23 15:51:42 -0700256 self.result.num_failures += 1
Adele Zhoue4c35612015-10-16 15:34:23 -0700257 self.result.retries = self._timeout_retries + self._retries
Craig Tiller91318bc2015-09-24 08:58:39 -0700258 self.start()
259 else:
260 self._state = _FAILURE
Jan Tattermusch91ad0182015-10-01 09:22:03 -0700261 if not self._suppress_failure_message:
262 message('FAILED', '%s [ret=%d, pid=%d]' % (
263 self._spec.shortname, self._process.returncode, self._process.pid),
Craig Tillerdb218992016-01-05 09:28:46 -0800264 stdout(), do_newline=True)
Adele Zhoue4c35612015-10-16 15:34:23 -0700265 self.result.state = 'FAILED'
Adele Zhoud5fffa52015-10-23 15:51:42 -0700266 self.result.num_failures += 1
Adele Zhoue4c35612015-10-16 15:34:23 -0700267 self.result.returncode = self._process.returncode
ctiller3040cb72015-01-07 12:13:17 -0800268 else:
269 self._state = _SUCCESS
Craig Tiller95cc07b2015-09-28 13:41:30 -0700270 message('PASSED', '%s [time=%.1fsec; retries=%d;%d]' % (
271 self._spec.shortname, elapsed, self._retries, self._timeout_retries),
272 do_newline=self._newline_on_success or self._travis)
Adele Zhoue4c35612015-10-16 15:34:23 -0700273 self.result.state = 'PASSED'
Craig Tiller547db2b2015-01-30 14:08:39 -0800274 if self._bin_hash:
275 update_cache.finished(self._spec.identity(), self._bin_hash)
Craig Tiller590105a2016-01-19 13:03:46 -0800276 elif (self._state == _RUNNING and
277 self._spec.timeout_seconds is not None and
278 time.time() - self._start > self._spec.timeout_seconds):
Craig Tiller95cc07b2015-09-28 13:41:30 -0700279 if self._timeout_retries < self._spec.timeout_retries:
Craig Tillerdb218992016-01-05 09:28:46 -0800280 message('TIMEOUT_FLAKE', '%s [pid=%d]' % (self._spec.shortname, self._process.pid), stdout(), do_newline=True)
Craig Tiller95cc07b2015-09-28 13:41:30 -0700281 self._timeout_retries += 1
Adele Zhoud5fffa52015-10-23 15:51:42 -0700282 self.result.num_failures += 1
Adele Zhoue4c35612015-10-16 15:34:23 -0700283 self.result.retries = self._timeout_retries + self._retries
Jan Tattermusch39e3cb32015-10-22 18:21:08 -0700284 if self._spec.kill_handler:
285 self._spec.kill_handler(self)
Craig Tiller3dc1e4f2015-09-25 11:46:56 -0700286 self._process.terminate()
287 self.start()
288 else:
Craig Tillerdb218992016-01-05 09:28:46 -0800289 message('TIMEOUT', '%s [pid=%d]' % (self._spec.shortname, self._process.pid), stdout(), do_newline=True)
Craig Tiller3dc1e4f2015-09-25 11:46:56 -0700290 self.kill()
Adele Zhoue4c35612015-10-16 15:34:23 -0700291 self.result.state = 'TIMEOUT'
Adele Zhoud5fffa52015-10-23 15:51:42 -0700292 self.result.num_failures += 1
ctiller3040cb72015-01-07 12:13:17 -0800293 return self._state
294
295 def kill(self):
296 if self._state == _RUNNING:
297 self._state = _KILLED
Jan Tattermusche2686282015-10-08 16:27:07 -0700298 if self._spec.kill_handler:
299 self._spec.kill_handler(self)
ctiller3040cb72015-01-07 12:13:17 -0800300 self._process.terminate()
301
Jan Tattermusch91ad0182015-10-01 09:22:03 -0700302 def suppress_failure_message(self):
303 self._suppress_failure_message = True
Craig Tillerdb218992016-01-05 09:28:46 -0800304
ctiller3040cb72015-01-07 12:13:17 -0800305
Nicolas Nobleddef2462015-01-06 18:08:25 -0800306class Jobset(object):
307 """Manages one run of jobs."""
308
Craig Tiller533b1a22015-05-29 08:41:29 -0700309 def __init__(self, check_cancelled, maxjobs, newline_on_success, travis,
Adele Zhou2271ab52015-10-28 13:59:14 -0700310 stop_on_failure, add_env, cache):
ctiller3040cb72015-01-07 12:13:17 -0800311 self._running = set()
312 self._check_cancelled = check_cancelled
313 self._cancelled = False
Nicolas Nobleddef2462015-01-06 18:08:25 -0800314 self._failures = 0
Craig Tiller738c3342015-01-12 14:28:33 -0800315 self._completed = 0
ctiller94e5dde2015-01-09 10:41:59 -0800316 self._maxjobs = maxjobs
Nicolas Noble044db742015-01-14 16:57:24 -0800317 self._newline_on_success = newline_on_success
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100318 self._travis = travis
Craig Tiller71735182015-01-15 17:07:13 -0800319 self._cache = cache
Craig Tiller533b1a22015-05-29 08:41:29 -0700320 self._stop_on_failure = stop_on_failure
Craig Tiller74e770d2015-06-11 09:38:09 -0700321 self._hashes = {}
Craig Tillerf53d9c82015-08-04 14:19:43 -0700322 self._add_env = add_env
Adele Zhoue4c35612015-10-16 15:34:23 -0700323 self.resultset = {}
Craig Tiller6364dcb2015-11-24 16:29:06 -0800324 self._remaining = None
325
326 def set_remaining(self, remaining):
327 self._remaining = remaining
328
Adele Zhoue4c35612015-10-16 15:34:23 -0700329 def get_num_failures(self):
Craig Tiller6364dcb2015-11-24 16:29:06 -0800330 return self._failures
Nicolas Nobleddef2462015-01-06 18:08:25 -0800331
Craig Tiller547db2b2015-01-30 14:08:39 -0800332 def start(self, spec):
ctiller3040cb72015-01-07 12:13:17 -0800333 """Start a job. Return True on success, False on failure."""
ctiller94e5dde2015-01-09 10:41:59 -0800334 while len(self._running) >= self._maxjobs:
ctiller3040cb72015-01-07 12:13:17 -0800335 if self.cancelled(): return False
336 self.reap()
337 if self.cancelled(): return False
Craig Tiller547db2b2015-01-30 14:08:39 -0800338 if spec.hash_targets:
Craig Tiller74e770d2015-06-11 09:38:09 -0700339 if spec.identity() in self._hashes:
340 bin_hash = self._hashes[spec.identity()]
341 else:
342 bin_hash = hashlib.sha1()
343 for fn in spec.hash_targets:
344 with open(which(fn)) as f:
345 bin_hash.update(f.read())
346 bin_hash = bin_hash.hexdigest()
347 self._hashes[spec.identity()] = bin_hash
Craig Tiller547db2b2015-01-30 14:08:39 -0800348 should_run = self._cache.should_run(spec.identity(), bin_hash)
349 else:
350 bin_hash = None
351 should_run = True
352 if should_run:
Adele Zhoue4c35612015-10-16 15:34:23 -0700353 job = Job(spec,
354 bin_hash,
355 self._newline_on_success,
356 self._travis,
Adele Zhou2271ab52015-10-28 13:59:14 -0700357 self._add_env)
Adele Zhoue4c35612015-10-16 15:34:23 -0700358 self._running.add(job)
Adele Zhoud5fffa52015-10-23 15:51:42 -0700359 self.resultset[job.GetSpec().shortname] = []
ctiller3040cb72015-01-07 12:13:17 -0800360 return True
Nicolas Nobleddef2462015-01-06 18:08:25 -0800361
ctiller3040cb72015-01-07 12:13:17 -0800362 def reap(self):
363 """Collect the dead jobs."""
364 while self._running:
365 dead = set()
366 for job in self._running:
Craig Tiller71735182015-01-15 17:07:13 -0800367 st = job.state(self._cache)
ctiller3040cb72015-01-07 12:13:17 -0800368 if st == _RUNNING: continue
Craig Tiller533b1a22015-05-29 08:41:29 -0700369 if st == _FAILURE or st == _KILLED:
370 self._failures += 1
371 if self._stop_on_failure:
372 self._cancelled = True
373 for job in self._running:
374 job.kill()
ctiller3040cb72015-01-07 12:13:17 -0800375 dead.add(job)
Craig Tiller74e770d2015-06-11 09:38:09 -0700376 break
ctiller3040cb72015-01-07 12:13:17 -0800377 for job in dead:
Craig Tiller738c3342015-01-12 14:28:33 -0800378 self._completed += 1
Adele Zhoud5fffa52015-10-23 15:51:42 -0700379 self.resultset[job.GetSpec().shortname].append(job.result)
ctiller3040cb72015-01-07 12:13:17 -0800380 self._running.remove(job)
Craig Tiller3b083062015-01-12 13:51:28 -0800381 if dead: return
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100382 if (not self._travis):
Craig Tiller2c23ad52015-12-04 06:50:38 -0800383 rstr = '' if self._remaining is None else '%d queued, ' % self._remaining
384 message('WAITING', '%s%d jobs running, %d complete, %d failed' % (
385 rstr, len(self._running), self._completed, self._failures))
Nicolas "Pixel" Noblef72d7b52015-12-03 03:07:43 +0100386 if platform_string() == 'windows':
Craig Tiller5058c692015-04-08 09:42:04 -0700387 time.sleep(0.1)
388 else:
389 global have_alarm
390 if not have_alarm:
391 have_alarm = True
392 signal.alarm(10)
393 signal.pause()
ctiller3040cb72015-01-07 12:13:17 -0800394
395 def cancelled(self):
396 """Poll for cancellation."""
397 if self._cancelled: return True
398 if not self._check_cancelled(): return False
399 for job in self._running:
400 job.kill()
401 self._cancelled = True
402 return True
403
404 def finish(self):
405 while self._running:
406 if self.cancelled(): pass # poll cancellation
407 self.reap()
408 return not self.cancelled() and self._failures == 0
Nicolas Nobleddef2462015-01-06 18:08:25 -0800409
410
ctiller3040cb72015-01-07 12:13:17 -0800411def _never_cancelled():
412 return False
413
414
Craig Tiller71735182015-01-15 17:07:13 -0800415# cache class that caches nothing
416class NoCache(object):
417 def should_run(self, cmdline, bin_hash):
418 return True
419
420 def finished(self, cmdline, bin_hash):
421 pass
422
423
Craig Tiller6364dcb2015-11-24 16:29:06 -0800424def tag_remaining(xs):
425 staging = []
426 for x in xs:
427 staging.append(x)
428 if len(staging) > 1000:
429 yield (staging.pop(0), None)
430 n = len(staging)
431 for i, x in enumerate(staging):
432 yield (x, n - i - 1)
433
434
Nicolas Nobleb09078f2015-01-14 18:06:05 -0800435def run(cmdlines,
436 check_cancelled=_never_cancelled,
437 maxjobs=None,
Craig Tiller71735182015-01-15 17:07:13 -0800438 newline_on_success=False,
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100439 travis=False,
David Garcia Quintase90cd372015-05-31 18:15:26 -0700440 infinite_runs=False,
Craig Tiller533b1a22015-05-29 08:41:29 -0700441 stop_on_failure=False,
Nicolas "Pixel" Noble5937b5b2015-06-26 02:04:12 +0200442 cache=None,
Craig Tillerf53d9c82015-08-04 14:19:43 -0700443 add_env={}):
ctiller94e5dde2015-01-09 10:41:59 -0800444 js = Jobset(check_cancelled,
Nicolas Noble044db742015-01-14 16:57:24 -0800445 maxjobs if maxjobs is not None else _DEFAULT_MAX_JOBS,
Craig Tillerf53d9c82015-08-04 14:19:43 -0700446 newline_on_success, travis, stop_on_failure, add_env,
Adele Zhou2271ab52015-10-28 13:59:14 -0700447 cache if cache is not None else NoCache())
Craig Tiller6364dcb2015-11-24 16:29:06 -0800448 for cmdline, remaining in tag_remaining(cmdlines):
ctiller3040cb72015-01-07 12:13:17 -0800449 if not js.start(cmdline):
450 break
Craig Tiller6364dcb2015-11-24 16:29:06 -0800451 if remaining is not None:
452 js.set_remaining(remaining)
453 js.finish()
Adele Zhoue4c35612015-10-16 15:34:23 -0700454 return js.get_num_failures(), js.resultset