diff options
Diffstat (limited to 'third_party/gtest-parallel/gtest-parallel')
-rwxr-xr-x | third_party/gtest-parallel/gtest-parallel | 155 |
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) |