aboutsummaryrefslogtreecommitdiff
path: root/third_party/gtest-parallel/gtest-parallel
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/gtest-parallel/gtest-parallel')
-rwxr-xr-xthird_party/gtest-parallel/gtest-parallel155
1 files changed, 121 insertions, 34 deletions
diff --git a/third_party/gtest-parallel/gtest-parallel b/third_party/gtest-parallel/gtest-parallel
index 0be59e4b4e..3e2fdb4ba8 100755
--- a/third_party/gtest-parallel/gtest-parallel
+++ b/third_party/gtest-parallel/gtest-parallel
@@ -13,16 +13,67 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import cPickle
+import errno
import gzip
import multiprocessing
import optparse
import os
+import signal
import subprocess
import sys
+import tempfile
+import thread
import threading
import time
import zlib
+# An object that catches SIGINT sent to the Python process and notices
+# if processes passed to wait() die by SIGINT (we need to look for
+# both of those cases, because pressing Ctrl+C can result in either
+# the main process or one of the subprocesses getting the signal).
+#
+# Before a SIGINT is seen, wait(p) will simply call p.wait() and
+# return the result. Once a SIGINT has been seen (in the main process
+# or a subprocess, including the one the current call is waiting for),
+# wait(p) will call p.terminate() and raise ProcessWasInterrupted.
+class SigintHandler(object):
+ class ProcessWasInterrupted(Exception): pass
+ sigint_returncodes = {-signal.SIGINT, # Unix
+ -1073741510, # Windows
+ }
+ def __init__(self):
+ self.__lock = threading.Lock()
+ self.__processes = set()
+ self.__got_sigint = False
+ signal.signal(signal.SIGINT, self.__sigint_handler)
+ def __on_sigint(self):
+ self.__got_sigint = True
+ while self.__processes:
+ try:
+ self.__processes.pop().terminate()
+ except OSError:
+ pass
+ def __sigint_handler(self, signal_num, frame):
+ with self.__lock:
+ self.__on_sigint()
+ def got_sigint(self):
+ with self.__lock:
+ return self.__got_sigint
+ def wait(self, p):
+ with self.__lock:
+ if self.__got_sigint:
+ p.terminate()
+ self.__processes.add(p)
+ code = p.wait()
+ with self.__lock:
+ self.__processes.discard(p)
+ if code in self.sigint_returncodes:
+ self.__on_sigint()
+ if self.__got_sigint:
+ raise self.ProcessWasInterrupted
+ return code
+sigint_handler = SigintHandler()
+
# Return the width of the terminal, or None if it couldn't be
# determined (e.g. because we're not being run interactively).
def term_width(out):
@@ -53,15 +104,21 @@ class Outputter(object):
else:
self.__out_file.write("\r" + msg[:self.__width].ljust(self.__width))
self.__previous_line_was_transient = True
- def permanent_line(self, msg):
+ def flush_transient_output(self):
if self.__previous_line_was_transient:
self.__out_file.write("\n")
self.__previous_line_was_transient = False
+ def permanent_line(self, msg):
+ self.flush_transient_output()
self.__out_file.write(msg + "\n")
stdout_lock = threading.Lock()
class FilterFormat:
+ if sys.stdout.isatty():
+ # stdout needs to be unbuffered since the output is interactive.
+ sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
+
out = Outputter(sys.stdout)
total_tests = 0
finished_tests = 0
@@ -80,7 +137,6 @@ class FilterFormat:
if command == "TEST":
(binary, test) = arg.split(' ', 1)
self.tests[job_id] = (binary, test.strip())
- self.outputs[job_id] = []
elif command == "EXIT":
(exit_code, time_ms) = [int(x) for x in arg.split(' ', 1)]
self.finished_tests += 1
@@ -88,8 +144,9 @@ class FilterFormat:
self.print_test_status(test, time_ms)
if exit_code != 0:
self.failures.append(self.tests[job_id])
- for line in self.outputs[job_id]:
- self.out.permanent_line(line)
+ with open(self.outputs[job_id]) as f:
+ for line in f.readlines():
+ self.out.permanent_line(line.rstrip())
self.out.permanent_line(
"[%d/%d] %s returned/aborted with exit code %d (%d ms)"
% (self.finished_tests, self.total_tests, test, exit_code, time_ms))
@@ -97,17 +154,15 @@ class FilterFormat:
self.total_tests = int(arg.split(' ', 1)[1])
self.out.transient_line("[0/%d] Running tests..." % self.total_tests)
- def add_stdout(self, job_id, output):
- self.outputs[job_id].append(output)
+ def logfile(self, job_id, name):
+ self.outputs[job_id] = name
def log(self, line):
stdout_lock.acquire()
(prefix, output) = line.split(' ', 1)
- if prefix[-1] == ':':
- self.handle_meta(int(prefix[:-1]), output)
- else:
- self.add_stdout(int(prefix[:-1]), output)
+ assert prefix[-1] == ':'
+ self.handle_meta(int(prefix[:-1]), output)
stdout_lock.release()
def end(self):
@@ -116,6 +171,7 @@ class FilterFormat:
% (len(self.failures), self.total_tests))
for (binary, test) in self.failures:
self.out.permanent_line(" " + binary + ": " + test)
+ self.out.flush_transient_output()
class RawFormat:
def log(self, line):
@@ -123,6 +179,10 @@ class RawFormat:
sys.stdout.write(line + "\n")
sys.stdout.flush()
stdout_lock.release()
+ def logfile(self, job_id, name):
+ with open(self.outputs[job_id]) as f:
+ for line in f.readlines():
+ self.log(str(job_id) + '> ' + line.rstrip())
def end(self):
pass
@@ -149,17 +209,19 @@ class TestTimes(object):
return
for ((test_binary, test_name), runtime) in times.items():
if (type(test_binary) is not str or type(test_name) is not str
- or type(runtime) not in {int, long}):
+ or type(runtime) not in {int, long, type(None)}):
return
self.__times = times
def get_test_time(self, binary, testname):
- "Return the last duration for the given test, or 0 if there's no record."
- return self.__times.get((binary, testname), 0)
+ """Return the last duration for the given test as an integer number of
+ milliseconds, or None if the test failed or if there's no record for it."""
+ return self.__times.get((binary, testname), None)
def record_test_time(self, binary, testname, runtime_ms):
- "Record that the given test ran in the specified number of milliseconds."
+ """Record that the given test ran in the specified number of
+ milliseconds. If the test failed, runtime_ms should be None."""
with self.__lock:
self.__times[(binary, testname)] = runtime_ms
@@ -184,6 +246,9 @@ for i in range(len(sys.argv)):
parser = optparse.OptionParser(
usage = 'usage: %prog [options] binary [binary ...] -- [additional args]')
+parser.add_option('-d', '--output_dir', type='string',
+ default=os.path.join(tempfile.gettempdir(), "gtest-parallel"),
+ help='output directory for test logs')
parser.add_option('-r', '--repeat', type='int', default=1,
help='repeat tests')
parser.add_option('-w', '--workers', type='int',
@@ -197,6 +262,8 @@ parser.add_option('--gtest_also_run_disabled_tests', action='store_true',
default=False, help='run disabled tests too')
parser.add_option('--format', type='string', default='filter',
help='output format (raw,filter)')
+parser.add_option('--print_test_times', action='store_true', default=False,
+ help='When done, list the run time of each test')
(options, binaries) = parser.parse_args()
@@ -240,17 +307,21 @@ for test_binary in binaries:
if line[0] != " ":
test_group = line.strip()
continue
- line = line.strip()
- if not options.gtest_also_run_disabled_tests and 'DISABLED' in line:
- continue
+ # Remove comments for parameterized tests and strip whitespace.
line = line.split('#')[0].strip()
if not line:
continue
test = test_group + line
+ if not options.gtest_also_run_disabled_tests and 'DISABLED_' in test:
+ continue
tests.append((times.get_test_time(test_binary, test),
test_binary, test, command))
-tests.sort(reverse=True)
+
+# Sort tests by falling runtime (with None, which is what we get for
+# new and failing tests, being considered larger than any real
+# runtime).
+tests.sort(reverse=True, key=lambda x: ((1 if x[0] is None else 0), x))
# Repeat tests (-r flag).
tests *= options.repeat
@@ -260,31 +331,41 @@ logger.log(str(-1) + ': TESTCNT ' + ' ' + str(len(tests)))
exit_code = 0
+# Create directory for test log output.
+try:
+ os.makedirs(options.output_dir)
+except OSError as e:
+ # Ignore errors if this directory already exists.
+ if e.errno != errno.EEXIST or not os.path.isdir(options.output_dir):
+ raise e
+# Remove files from old test runs.
+for logfile in os.listdir(options.output_dir):
+ os.remove(os.path.join(options.output_dir, logfile))
+
# Run the specified job. Return the elapsed time in milliseconds if
-# the job succeeds, or a very large number (larger than any reasonable
-# elapsed time) if the job fails. (This ensures that failing tests
-# will run first the next time.)
+# the job succeeds, or None if the job fails. (This ensures that
+# failing tests will run first the next time.)
def run_job((command, job_id, test)):
begin = time.time()
- sub = subprocess.Popen(command + ['--gtest_filter=' + test] +
- ['--gtest_color=' + options.gtest_color],
- stdout = subprocess.PIPE,
- stderr = subprocess.STDOUT)
- while True:
- line = sub.stdout.readline()
- if line == '':
- break
- logger.log(str(job_id) + '> ' + line.rstrip())
+ with tempfile.NamedTemporaryFile(dir=options.output_dir, delete=False) as log:
+ sub = subprocess.Popen(command + ['--gtest_filter=' + test] +
+ ['--gtest_color=' + options.gtest_color],
+ stdout=log.file,
+ stderr=log.file)
+ try:
+ code = sigint_handler.wait(sub)
+ except sigint_handler.ProcessWasInterrupted:
+ thread.exit()
+ runtime_ms = int(1000 * (time.time() - begin))
+ logger.logfile(job_id, log.name)
- code = sub.wait()
- runtime_ms = int(1000 * (time.time() - begin))
logger.log("%s: EXIT %s %d" % (job_id, code, runtime_ms))
if code == 0:
return runtime_ms
global exit_code
exit_code = code
- return sys.maxint
+ return None
def worker():
global job_id
@@ -312,4 +393,10 @@ workers = [start_daemon(worker) for i in range(options.workers)]
[t.join() for t in workers]
logger.end()
times.write_to_file(save_file)
-sys.exit(exit_code)
+if options.print_test_times:
+ ts = sorted((times.get_test_time(test_binary, test), test_binary, test)
+ for (_, test_binary, test, _) in tests
+ if times.get_test_time(test_binary, test) is not None)
+ for (time_ms, test_binary, test) in ts:
+ print "%8s %s" % ("%dms" % time_ms, test)
+sys.exit(-signal.SIGINT if sigint_handler.got_sigint() else exit_code)