blob: 7b2c62d1a2b12233a9c7f8352b2b0346ee34e515 [file] [log] [blame]
Craig Tiller6169d5f2016-03-31 07:46:18 -07001# Copyright 2015, 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
siddharthshukla0589e532016-07-07 16:08:01 +020032from __future__ import print_function
33
Nicolas Nobleddef2462015-01-06 18:08:25 -080034import multiprocessing
Craig Tiller71735182015-01-15 17:07:13 -080035import os
Craig Tiller5058c692015-04-08 09:42:04 -070036import platform
Craig Tiller5f735a62016-01-20 09:31:15 -080037import re
Craig Tiller336ad502015-02-24 14:46:02 -080038import signal
Nicolas Nobleddef2462015-01-06 18:08:25 -080039import subprocess
40import sys
ctiller3040cb72015-01-07 12:13:17 -080041import tempfile
42import time
Nicolas Nobleddef2462015-01-06 18:08:25 -080043
ctiller3040cb72015-01-07 12:13:17 -080044
Craig Tiller5f735a62016-01-20 09:31:15 -080045# cpu cost measurement
46measure_cpu_costs = False
47
48
ctiller94e5dde2015-01-09 10:41:59 -080049_DEFAULT_MAX_JOBS = 16 * multiprocessing.cpu_count()
Adele Zhoud01cbe32015-11-02 14:20:43 -080050_MAX_RESULT_SIZE = 8192
Nicolas Nobleddef2462015-01-06 18:08:25 -080051
Mark D. Roth62885422016-12-02 15:44:04 +000052
Mark D. Roth158a4a42016-12-02 10:53:26 -080053# NOTE: If you change this, please make sure to test reviewing the
54# github PR with http://reviewable.io, which is known to add UTF-8
55# characters to the PR description, which leak into the environment here
56# and cause failures.
Mark D. Roth62885422016-12-02 15:44:04 +000057def strip_non_ascii_chars(s):
58 return ''.join(c for c in s if ord(c) < 128)
59
60
Masood Malekghassemi768b1db2016-06-06 16:45:19 -070061def sanitized_environment(env):
62 sanitized = {}
63 for key, value in env.items():
Mark D. Roth62885422016-12-02 15:44:04 +000064 sanitized[strip_non_ascii_chars(key)] = strip_non_ascii_chars(value)
Masood Malekghassemi768b1db2016-06-06 16:45:19 -070065 return sanitized
66
Mark D. Roth62885422016-12-02 15:44:04 +000067
Nicolas "Pixel" Noblef72d7b52015-12-03 03:07:43 +010068def platform_string():
69 if platform.system() == 'Windows':
70 return 'windows'
71 elif platform.system()[:7] == 'MSYS_NT':
72 return 'windows'
73 elif platform.system() == 'Darwin':
74 return 'mac'
75 elif platform.system() == 'Linux':
76 return 'linux'
77 else:
78 return 'posix'
79
Nicolas Nobleddef2462015-01-06 18:08:25 -080080
Craig Tiller336ad502015-02-24 14:46:02 -080081# setup a signal handler so that signal.pause registers 'something'
82# when a child finishes
83# not using futures and threading to avoid a dependency on subprocess32
Nicolas "Pixel" Noblef72d7b52015-12-03 03:07:43 +010084if platform_string() == 'windows':
Craig Tiller5058c692015-04-08 09:42:04 -070085 pass
86else:
87 have_alarm = False
88 def alarm_handler(unused_signum, unused_frame):
89 global have_alarm
90 have_alarm = False
91
92 signal.signal(signal.SIGCHLD, lambda unused_signum, unused_frame: None)
93 signal.signal(signal.SIGALRM, alarm_handler)
Craig Tiller336ad502015-02-24 14:46:02 -080094
95
ctiller3040cb72015-01-07 12:13:17 -080096_SUCCESS = object()
97_FAILURE = object()
98_RUNNING = object()
99_KILLED = object()
100
101
Craig Tiller3b083062015-01-12 13:51:28 -0800102_COLORS = {
Nicolas Noble044db742015-01-14 16:57:24 -0800103 'red': [ 31, 0 ],
104 'green': [ 32, 0 ],
105 'yellow': [ 33, 0 ],
106 'lightgray': [ 37, 0],
107 'gray': [ 30, 1 ],
Craig Tillerd7e09c32015-09-25 11:33:39 -0700108 'purple': [ 35, 0 ],
Matt Kwong5c691c62016-10-20 17:11:18 -0700109 'cyan': [ 36, 0 ]
Craig Tiller3b083062015-01-12 13:51:28 -0800110 }
111
112
113_BEGINNING_OF_LINE = '\x1b[0G'
114_CLEAR_LINE = '\x1b[2K'
115
116
117_TAG_COLOR = {
118 'FAILED': 'red',
Craig Tillerd7e09c32015-09-25 11:33:39 -0700119 'FLAKE': 'purple',
Craig Tiller3dc1e4f2015-09-25 11:46:56 -0700120 'TIMEOUT_FLAKE': 'purple',
Masood Malekghassemie5f70022015-06-29 09:20:26 -0700121 'WARNING': 'yellow',
Craig Tillere1d0d1c2015-02-27 08:54:23 -0800122 'TIMEOUT': 'red',
Craig Tiller3b083062015-01-12 13:51:28 -0800123 'PASSED': 'green',
Nicolas Noble044db742015-01-14 16:57:24 -0800124 'START': 'gray',
Craig Tiller3b083062015-01-12 13:51:28 -0800125 'WAITING': 'yellow',
Nicolas Noble044db742015-01-14 16:57:24 -0800126 'SUCCESS': 'green',
127 'IDLE': 'gray',
Matt Kwong5c691c62016-10-20 17:11:18 -0700128 'SKIPPED': 'cyan'
Craig Tiller3b083062015-01-12 13:51:28 -0800129 }
130
131
Nicolas "Pixel" Noble99768ac2015-05-13 02:34:06 +0200132def message(tag, msg, explanatory_text=None, do_newline=False):
133 if message.old_tag == tag and message.old_msg == msg and not explanatory_text:
134 return
135 message.old_tag = tag
136 message.old_msg = msg
Craig Tiller23d2f3f2015-02-24 15:23:32 -0800137 try:
Nicolas "Pixel" Noblef72d7b52015-12-03 03:07:43 +0100138 if platform_string() == 'windows' or not sys.stdout.isatty():
Craig Tiller9f3b2d72015-08-25 11:50:57 -0700139 if explanatory_text:
siddharthshukla0589e532016-07-07 16:08:01 +0200140 print(explanatory_text)
141 print('%s: %s' % (tag, msg))
Jan Tattermusch9fe161c2016-12-09 12:04:34 +0100142 else:
143 sys.stdout.write('%s%s%s\x1b[%d;%dm%s\x1b[0m: %s%s' % (
144 _BEGINNING_OF_LINE,
145 _CLEAR_LINE,
146 '\n%s' % explanatory_text if explanatory_text is not None else '',
147 _COLORS[_TAG_COLOR[tag]][1],
148 _COLORS[_TAG_COLOR[tag]][0],
149 tag,
150 msg,
151 '\n' if do_newline or explanatory_text is not None else ''))
Craig Tiller23d2f3f2015-02-24 15:23:32 -0800152 sys.stdout.flush()
153 except:
154 pass
Craig Tiller3b083062015-01-12 13:51:28 -0800155
Adele Zhoue4c35612015-10-16 15:34:23 -0700156message.old_tag = ''
157message.old_msg = ''
Craig Tiller3b083062015-01-12 13:51:28 -0800158
Craig Tiller71735182015-01-15 17:07:13 -0800159def which(filename):
160 if '/' in filename:
161 return filename
162 for path in os.environ['PATH'].split(os.pathsep):
163 if os.path.exists(os.path.join(path, filename)):
164 return os.path.join(path, filename)
165 raise Exception('%s not found' % filename)
166
167
Craig Tiller547db2b2015-01-30 14:08:39 -0800168class JobSpec(object):
169 """Specifies what to run for a job."""
170
Craig Tiller74189cd2016-06-23 15:39:06 -0700171 def __init__(self, cmdline, shortname=None, environ=None,
Craig Tiller95cc07b2015-09-28 13:41:30 -0700172 cwd=None, shell=False, timeout_seconds=5*60, flake_retries=0,
Jan Tattermuschb2758442016-03-28 09:32:20 -0700173 timeout_retries=0, kill_handler=None, cpu_cost=1.0,
174 verbose_success=False):
Craig Tiller547db2b2015-01-30 14:08:39 -0800175 """
176 Arguments:
177 cmdline: a list of arguments to pass as the command line
178 environ: a dictionary of environment variables to set in the child process
Jan Tattermusche2686282015-10-08 16:27:07 -0700179 kill_handler: a handler that will be called whenever job.kill() is invoked
Craig Tiller56c6b6a2016-01-20 08:27:37 -0800180 cpu_cost: number of cores per second this job needs
Craig Tiller547db2b2015-01-30 14:08:39 -0800181 """
murgatroid99132ce6a2015-03-04 17:29:14 -0800182 if environ is None:
183 environ = {}
Craig Tiller547db2b2015-01-30 14:08:39 -0800184 self.cmdline = cmdline
185 self.environ = environ
186 self.shortname = cmdline[0] if shortname is None else shortname
Craig Tiller5058c692015-04-08 09:42:04 -0700187 self.cwd = cwd
Jan Tattermusche8243592015-04-17 14:14:01 -0700188 self.shell = shell
Jan Tattermusch725835a2015-08-01 21:02:35 -0700189 self.timeout_seconds = timeout_seconds
Craig Tiller91318bc2015-09-24 08:58:39 -0700190 self.flake_retries = flake_retries
Craig Tillerbfc8a062015-09-28 14:40:21 -0700191 self.timeout_retries = timeout_retries
Jan Tattermusche2686282015-10-08 16:27:07 -0700192 self.kill_handler = kill_handler
Craig Tiller56c6b6a2016-01-20 08:27:37 -0800193 self.cpu_cost = cpu_cost
Jan Tattermuschb2758442016-03-28 09:32:20 -0700194 self.verbose_success = verbose_success
Craig Tiller547db2b2015-01-30 14:08:39 -0800195
196 def identity(self):
Craig Tiller74189cd2016-06-23 15:39:06 -0700197 return '%r %r' % (self.cmdline, self.environ)
Craig Tiller547db2b2015-01-30 14:08:39 -0800198
199 def __hash__(self):
200 return hash(self.identity())
201
202 def __cmp__(self, other):
203 return self.identity() == other.identity()
Craig Tillerdb218992016-01-05 09:28:46 -0800204
Jan Tattermusch2dd156e2015-12-04 18:26:17 -0800205 def __repr__(self):
206 return 'JobSpec(shortname=%s, cmdline=%s)' % (self.shortname, self.cmdline)
Craig Tiller547db2b2015-01-30 14:08:39 -0800207
208
Adele Zhoue4c35612015-10-16 15:34:23 -0700209class JobResult(object):
210 def __init__(self):
211 self.state = 'UNKNOWN'
212 self.returncode = -1
213 self.elapsed_time = 0
Adele Zhoud5fffa52015-10-23 15:51:42 -0700214 self.num_failures = 0
Adele Zhoue4c35612015-10-16 15:34:23 -0700215 self.retries = 0
216 self.message = ''
Craig Tillerdb218992016-01-05 09:28:46 -0800217
Adele Zhoue4c35612015-10-16 15:34:23 -0700218
ctiller3040cb72015-01-07 12:13:17 -0800219class Job(object):
220 """Manages one job."""
221
Jan Tattermusch68e27bf2016-12-16 14:09:03 +0100222 def __init__(self, spec, newline_on_success, travis, add_env,
223 quiet_success=False):
Craig Tiller547db2b2015-01-30 14:08:39 -0800224 self._spec = spec
Nicolas Noble044db742015-01-14 16:57:24 -0800225 self._newline_on_success = newline_on_success
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100226 self._travis = travis
Craig Tiller91318bc2015-09-24 08:58:39 -0700227 self._add_env = add_env.copy()
Craig Tiller91318bc2015-09-24 08:58:39 -0700228 self._retries = 0
Craig Tiller95cc07b2015-09-28 13:41:30 -0700229 self._timeout_retries = 0
Jan Tattermusch91ad0182015-10-01 09:22:03 -0700230 self._suppress_failure_message = False
Jan Tattermusch68e27bf2016-12-16 14:09:03 +0100231 self._quiet_success = quiet_success
232 if not self._quiet_success:
233 message('START', spec.shortname, do_newline=self._travis)
Adele Zhoue4c35612015-10-16 15:34:23 -0700234 self.result = JobResult()
Craig Tiller91318bc2015-09-24 08:58:39 -0700235 self.start()
236
Adele Zhoue4c35612015-10-16 15:34:23 -0700237 def GetSpec(self):
238 return self._spec
239
Craig Tiller91318bc2015-09-24 08:58:39 -0700240 def start(self):
241 self._tempfile = tempfile.TemporaryFile()
242 env = dict(os.environ)
243 env.update(self._spec.environ)
244 env.update(self._add_env)
Masood Malekghassemi768b1db2016-06-06 16:45:19 -0700245 env = sanitized_environment(env)
Craig Tiller91318bc2015-09-24 08:58:39 -0700246 self._start = time.time()
Craig Tiller5f735a62016-01-20 09:31:15 -0800247 cmdline = self._spec.cmdline
248 if measure_cpu_costs:
249 cmdline = ['time', '--portability'] + cmdline
250 try_start = lambda: subprocess.Popen(args=cmdline,
Craig Tiller60078bb2015-11-04 16:39:54 -0800251 stderr=subprocess.STDOUT,
252 stdout=self._tempfile,
253 cwd=self._spec.cwd,
254 shell=self._spec.shell,
255 env=env)
256 delay = 0.3
257 for i in range(0, 4):
258 try:
259 self._process = try_start()
260 break
261 except OSError:
262 message('WARNING', 'Failed to start %s, retrying in %f seconds' % (self._spec.shortname, delay))
263 time.sleep(delay)
264 delay *= 2
265 else:
266 self._process = try_start()
Craig Tiller91318bc2015-09-24 08:58:39 -0700267 self._state = _RUNNING
ctiller3040cb72015-01-07 12:13:17 -0800268
Craig Tiller74189cd2016-06-23 15:39:06 -0700269 def state(self):
ctiller3040cb72015-01-07 12:13:17 -0800270 """Poll current state of the job. Prints messages at completion."""
Craig Tillerdb218992016-01-05 09:28:46 -0800271 def stdout(self=self):
272 self._tempfile.seek(0)
273 stdout = self._tempfile.read()
274 self.result.message = stdout[-_MAX_RESULT_SIZE:]
275 return stdout
ctiller3040cb72015-01-07 12:13:17 -0800276 if self._state == _RUNNING and self._process.poll() is not None:
Craig Tiller9d6139a2015-02-26 15:24:43 -0800277 elapsed = time.time() - self._start
Adele Zhoue4c35612015-10-16 15:34:23 -0700278 self.result.elapsed_time = elapsed
ctiller3040cb72015-01-07 12:13:17 -0800279 if self._process.returncode != 0:
Craig Tiller91318bc2015-09-24 08:58:39 -0700280 if self._retries < self._spec.flake_retries:
281 message('FLAKE', '%s [ret=%d, pid=%d]' % (
Craig Tillerd0ffe142015-05-19 21:51:13 -0700282 self._spec.shortname, self._process.returncode, self._process.pid),
Craig Tillerdb218992016-01-05 09:28:46 -0800283 stdout(), do_newline=True)
Craig Tiller91318bc2015-09-24 08:58:39 -0700284 self._retries += 1
Adele Zhoud5fffa52015-10-23 15:51:42 -0700285 self.result.num_failures += 1
Adele Zhoue4c35612015-10-16 15:34:23 -0700286 self.result.retries = self._timeout_retries + self._retries
Craig Tiller91318bc2015-09-24 08:58:39 -0700287 self.start()
288 else:
289 self._state = _FAILURE
Jan Tattermusch91ad0182015-10-01 09:22:03 -0700290 if not self._suppress_failure_message:
291 message('FAILED', '%s [ret=%d, pid=%d]' % (
292 self._spec.shortname, self._process.returncode, self._process.pid),
Craig Tillerdb218992016-01-05 09:28:46 -0800293 stdout(), do_newline=True)
Adele Zhoue4c35612015-10-16 15:34:23 -0700294 self.result.state = 'FAILED'
Adele Zhoud5fffa52015-10-23 15:51:42 -0700295 self.result.num_failures += 1
Adele Zhoue4c35612015-10-16 15:34:23 -0700296 self.result.returncode = self._process.returncode
ctiller3040cb72015-01-07 12:13:17 -0800297 else:
298 self._state = _SUCCESS
Craig Tiller5f735a62016-01-20 09:31:15 -0800299 measurement = ''
300 if measure_cpu_costs:
301 m = re.search(r'real ([0-9.]+)\nuser ([0-9.]+)\nsys ([0-9.]+)', stdout())
302 real = float(m.group(1))
303 user = float(m.group(2))
304 sys = float(m.group(3))
305 if real > 0.5:
306 cores = (user + sys) / real
Craig Tillerbfe69362016-01-20 09:38:21 -0800307 measurement = '; cpu_cost=%.01f; estimated=%.01f' % (cores, self._spec.cpu_cost)
Jan Tattermusch68e27bf2016-12-16 14:09:03 +0100308 if not self._quiet_success:
309 message('PASSED', '%s [time=%.1fsec; retries=%d:%d%s]' % (
310 self._spec.shortname, elapsed, self._retries, self._timeout_retries, measurement),
311 stdout() if self._spec.verbose_success else None,
312 do_newline=self._newline_on_success or self._travis)
Adele Zhoue4c35612015-10-16 15:34:23 -0700313 self.result.state = 'PASSED'
Craig Tiller5f735a62016-01-20 09:31:15 -0800314 elif (self._state == _RUNNING and
315 self._spec.timeout_seconds is not None and
Craig Tiller590105a2016-01-19 13:03:46 -0800316 time.time() - self._start > self._spec.timeout_seconds):
Craig Tiller95cc07b2015-09-28 13:41:30 -0700317 if self._timeout_retries < self._spec.timeout_retries:
Craig Tillerdb218992016-01-05 09:28:46 -0800318 message('TIMEOUT_FLAKE', '%s [pid=%d]' % (self._spec.shortname, self._process.pid), stdout(), do_newline=True)
Craig Tiller95cc07b2015-09-28 13:41:30 -0700319 self._timeout_retries += 1
Adele Zhoud5fffa52015-10-23 15:51:42 -0700320 self.result.num_failures += 1
Adele Zhoue4c35612015-10-16 15:34:23 -0700321 self.result.retries = self._timeout_retries + self._retries
Jan Tattermusch39e3cb32015-10-22 18:21:08 -0700322 if self._spec.kill_handler:
323 self._spec.kill_handler(self)
Craig Tiller3dc1e4f2015-09-25 11:46:56 -0700324 self._process.terminate()
325 self.start()
326 else:
Craig Tillerdb218992016-01-05 09:28:46 -0800327 message('TIMEOUT', '%s [pid=%d]' % (self._spec.shortname, self._process.pid), stdout(), do_newline=True)
Craig Tiller3dc1e4f2015-09-25 11:46:56 -0700328 self.kill()
Adele Zhoue4c35612015-10-16 15:34:23 -0700329 self.result.state = 'TIMEOUT'
Adele Zhoud5fffa52015-10-23 15:51:42 -0700330 self.result.num_failures += 1
ctiller3040cb72015-01-07 12:13:17 -0800331 return self._state
332
333 def kill(self):
334 if self._state == _RUNNING:
335 self._state = _KILLED
Jan Tattermusche2686282015-10-08 16:27:07 -0700336 if self._spec.kill_handler:
337 self._spec.kill_handler(self)
ctiller3040cb72015-01-07 12:13:17 -0800338 self._process.terminate()
339
Jan Tattermusch91ad0182015-10-01 09:22:03 -0700340 def suppress_failure_message(self):
341 self._suppress_failure_message = True
Craig Tillerdb218992016-01-05 09:28:46 -0800342
ctiller3040cb72015-01-07 12:13:17 -0800343
Nicolas Nobleddef2462015-01-06 18:08:25 -0800344class Jobset(object):
345 """Manages one run of jobs."""
346
Craig Tiller533b1a22015-05-29 08:41:29 -0700347 def __init__(self, check_cancelled, maxjobs, newline_on_success, travis,
Jan Tattermusch68e27bf2016-12-16 14:09:03 +0100348 stop_on_failure, add_env, quiet_success):
ctiller3040cb72015-01-07 12:13:17 -0800349 self._running = set()
350 self._check_cancelled = check_cancelled
351 self._cancelled = False
Nicolas Nobleddef2462015-01-06 18:08:25 -0800352 self._failures = 0
Craig Tiller738c3342015-01-12 14:28:33 -0800353 self._completed = 0
ctiller94e5dde2015-01-09 10:41:59 -0800354 self._maxjobs = maxjobs
Nicolas Noble044db742015-01-14 16:57:24 -0800355 self._newline_on_success = newline_on_success
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100356 self._travis = travis
Craig Tiller533b1a22015-05-29 08:41:29 -0700357 self._stop_on_failure = stop_on_failure
Craig Tillerf53d9c82015-08-04 14:19:43 -0700358 self._add_env = add_env
Jan Tattermusch68e27bf2016-12-16 14:09:03 +0100359 self._quiet_success = quiet_success
Adele Zhoue4c35612015-10-16 15:34:23 -0700360 self.resultset = {}
Craig Tiller6364dcb2015-11-24 16:29:06 -0800361 self._remaining = None
Craig Tillered735102016-04-06 12:59:23 -0700362 self._start_time = time.time()
Craig Tiller6364dcb2015-11-24 16:29:06 -0800363
364 def set_remaining(self, remaining):
365 self._remaining = remaining
366
Adele Zhoue4c35612015-10-16 15:34:23 -0700367 def get_num_failures(self):
Craig Tiller6364dcb2015-11-24 16:29:06 -0800368 return self._failures
Nicolas Nobleddef2462015-01-06 18:08:25 -0800369
Craig Tiller56c6b6a2016-01-20 08:27:37 -0800370 def cpu_cost(self):
371 c = 0
372 for job in self._running:
373 c += job._spec.cpu_cost
374 return c
375
Craig Tiller547db2b2015-01-30 14:08:39 -0800376 def start(self, spec):
ctiller3040cb72015-01-07 12:13:17 -0800377 """Start a job. Return True on success, False on failure."""
Craig Tiller56c6b6a2016-01-20 08:27:37 -0800378 while True:
ctiller3040cb72015-01-07 12:13:17 -0800379 if self.cancelled(): return False
Craig Tiller56c6b6a2016-01-20 08:27:37 -0800380 current_cpu_cost = self.cpu_cost()
381 if current_cpu_cost == 0: break
Craig Tiller3e301a32016-01-27 15:36:52 -0800382 if current_cpu_cost + spec.cpu_cost <= self._maxjobs: break
ctiller3040cb72015-01-07 12:13:17 -0800383 self.reap()
384 if self.cancelled(): return False
Craig Tiller74189cd2016-06-23 15:39:06 -0700385 job = Job(spec,
386 self._newline_on_success,
387 self._travis,
Jan Tattermusch68e27bf2016-12-16 14:09:03 +0100388 self._add_env,
389 self._quiet_success)
Craig Tiller74189cd2016-06-23 15:39:06 -0700390 self._running.add(job)
siddharthshukla0589e532016-07-07 16:08:01 +0200391 if job.GetSpec().shortname not in self.resultset:
Craig Tiller74189cd2016-06-23 15:39:06 -0700392 self.resultset[job.GetSpec().shortname] = []
Craig Tillerddf02c12016-06-24 14:04:01 -0700393 return True
Nicolas Nobleddef2462015-01-06 18:08:25 -0800394
ctiller3040cb72015-01-07 12:13:17 -0800395 def reap(self):
396 """Collect the dead jobs."""
397 while self._running:
398 dead = set()
399 for job in self._running:
Craig Tiller74189cd2016-06-23 15:39:06 -0700400 st = job.state()
ctiller3040cb72015-01-07 12:13:17 -0800401 if st == _RUNNING: continue
Craig Tiller533b1a22015-05-29 08:41:29 -0700402 if st == _FAILURE or st == _KILLED:
403 self._failures += 1
404 if self._stop_on_failure:
405 self._cancelled = True
406 for job in self._running:
407 job.kill()
ctiller3040cb72015-01-07 12:13:17 -0800408 dead.add(job)
Craig Tiller74e770d2015-06-11 09:38:09 -0700409 break
ctiller3040cb72015-01-07 12:13:17 -0800410 for job in dead:
Craig Tiller738c3342015-01-12 14:28:33 -0800411 self._completed += 1
Jan Tattermusch68e27bf2016-12-16 14:09:03 +0100412 if not self._quiet_success or job.result.state != 'PASSED':
413 self.resultset[job.GetSpec().shortname].append(job.result)
ctiller3040cb72015-01-07 12:13:17 -0800414 self._running.remove(job)
Craig Tiller3b083062015-01-12 13:51:28 -0800415 if dead: return
Jan Tattermusch9fe161c2016-12-09 12:04:34 +0100416 if not self._travis and platform_string() != 'windows':
Craig Tiller2c23ad52015-12-04 06:50:38 -0800417 rstr = '' if self._remaining is None else '%d queued, ' % self._remaining
Craig Tillered735102016-04-06 12:59:23 -0700418 if self._remaining is not None and self._completed > 0:
419 now = time.time()
420 sofar = now - self._start_time
421 remaining = sofar / self._completed * (self._remaining + len(self._running))
422 rstr = 'ETA %.1f sec; %s' % (remaining, rstr)
Craig Tiller2c23ad52015-12-04 06:50:38 -0800423 message('WAITING', '%s%d jobs running, %d complete, %d failed' % (
424 rstr, len(self._running), self._completed, self._failures))
Nicolas "Pixel" Noblef72d7b52015-12-03 03:07:43 +0100425 if platform_string() == 'windows':
Craig Tiller5058c692015-04-08 09:42:04 -0700426 time.sleep(0.1)
427 else:
428 global have_alarm
429 if not have_alarm:
430 have_alarm = True
431 signal.alarm(10)
432 signal.pause()
ctiller3040cb72015-01-07 12:13:17 -0800433
434 def cancelled(self):
435 """Poll for cancellation."""
436 if self._cancelled: return True
437 if not self._check_cancelled(): return False
438 for job in self._running:
439 job.kill()
440 self._cancelled = True
441 return True
442
443 def finish(self):
444 while self._running:
445 if self.cancelled(): pass # poll cancellation
446 self.reap()
447 return not self.cancelled() and self._failures == 0
Nicolas Nobleddef2462015-01-06 18:08:25 -0800448
449
ctiller3040cb72015-01-07 12:13:17 -0800450def _never_cancelled():
451 return False
452
453
Craig Tiller6364dcb2015-11-24 16:29:06 -0800454def tag_remaining(xs):
455 staging = []
456 for x in xs:
457 staging.append(x)
Craig Tillered735102016-04-06 12:59:23 -0700458 if len(staging) > 5000:
Craig Tiller6364dcb2015-11-24 16:29:06 -0800459 yield (staging.pop(0), None)
460 n = len(staging)
461 for i, x in enumerate(staging):
462 yield (x, n - i - 1)
463
464
Nicolas Nobleb09078f2015-01-14 18:06:05 -0800465def run(cmdlines,
466 check_cancelled=_never_cancelled,
467 maxjobs=None,
Craig Tiller71735182015-01-15 17:07:13 -0800468 newline_on_success=False,
Nicolas "Pixel" Noblea7df3f92015-02-26 22:07:04 +0100469 travis=False,
David Garcia Quintase90cd372015-05-31 18:15:26 -0700470 infinite_runs=False,
Craig Tiller533b1a22015-05-29 08:41:29 -0700471 stop_on_failure=False,
Matt Kwong5c691c62016-10-20 17:11:18 -0700472 add_env={},
Jan Tattermusch68e27bf2016-12-16 14:09:03 +0100473 skip_jobs=False,
474 quiet_success=False):
Matt Kwong5c691c62016-10-20 17:11:18 -0700475 if skip_jobs:
476 results = {}
477 skipped_job_result = JobResult()
478 skipped_job_result.state = 'SKIPPED'
479 for job in cmdlines:
480 message('SKIPPED', job.shortname, do_newline=True)
481 results[job.shortname] = [skipped_job_result]
482 return results
ctiller94e5dde2015-01-09 10:41:59 -0800483 js = Jobset(check_cancelled,
Nicolas Noble044db742015-01-14 16:57:24 -0800484 maxjobs if maxjobs is not None else _DEFAULT_MAX_JOBS,
Jan Tattermusch68e27bf2016-12-16 14:09:03 +0100485 newline_on_success, travis, stop_on_failure, add_env,
486 quiet_success)
Craig Tiller6364dcb2015-11-24 16:29:06 -0800487 for cmdline, remaining in tag_remaining(cmdlines):
ctiller3040cb72015-01-07 12:13:17 -0800488 if not js.start(cmdline):
489 break
Craig Tiller6364dcb2015-11-24 16:29:06 -0800490 if remaining is not None:
491 js.set_remaining(remaining)
492 js.finish()
Adele Zhoue4c35612015-10-16 15:34:23 -0700493 return js.get_num_failures(), js.resultset