| Index: third_party/gtest-parallel/gtest-parallel
|
| diff --git a/third_party/gtest-parallel/gtest-parallel b/third_party/gtest-parallel/gtest-parallel
|
| deleted file mode 100755
|
| index 4e5a9fc0ed71489897afa08fbe6c70dc7d06f8ee..0000000000000000000000000000000000000000
|
| --- a/third_party/gtest-parallel/gtest-parallel
|
| +++ /dev/null
|
| @@ -1,478 +0,0 @@
|
| -#!/usr/bin/env python2
|
| -# Copyright 2013 Google Inc. All rights reserved.
|
| -#
|
| -# Licensed under the Apache License, Version 2.0 (the "License");
|
| -# you may not use this file except in compliance with the License.
|
| -# You may obtain a copy of the License at
|
| -#
|
| -# http://www.apache.org/licenses/LICENSE-2.0
|
| -#
|
| -# Unless required by applicable law or agreed to in writing, software
|
| -# distributed under the License is distributed on an "AS IS" BASIS,
|
| -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| -# See the License for the specific language governing permissions and
|
| -# limitations under the License.
|
| -import cPickle
|
| -import errno
|
| -import gzip
|
| -import json
|
| -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):
|
| - if not out.isatty():
|
| - return None
|
| - try:
|
| - p = subprocess.Popen(["stty", "size"],
|
| - stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| - (out, err) = p.communicate()
|
| - if p.returncode != 0 or err:
|
| - return None
|
| - return int(out.split()[1])
|
| - except (IndexError, OSError, ValueError):
|
| - return None
|
| -
|
| -# Output transient and permanent lines of text. If several transient
|
| -# lines are written in sequence, the new will overwrite the old. We
|
| -# use this to ensure that lots of unimportant info (tests passing)
|
| -# won't drown out important info (tests failing).
|
| -class Outputter(object):
|
| - def __init__(self, out_file):
|
| - self.__out_file = out_file
|
| - self.__previous_line_was_transient = False
|
| - self.__width = term_width(out_file) # Line width, or None if not a tty.
|
| - def transient_line(self, msg):
|
| - if self.__width is None:
|
| - self.__out_file.write(msg + "\n")
|
| - else:
|
| - self.__out_file.write("\r" + msg[:self.__width].ljust(self.__width))
|
| - self.__previous_line_was_transient = True
|
| - 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
|
| -
|
| - tests = {}
|
| - outputs = {}
|
| - failures = []
|
| -
|
| - def print_test_status(self, last_finished_test, time_ms):
|
| - self.out.transient_line("[%d/%d] %s (%d ms)"
|
| - % (self.finished_tests, self.total_tests,
|
| - last_finished_test, time_ms))
|
| -
|
| - def handle_meta(self, job_id, args):
|
| - (command, arg) = args.split(' ', 1)
|
| - if command == "TEST":
|
| - (binary, test) = arg.split(' ', 1)
|
| - self.tests[job_id] = (binary, test.strip())
|
| - elif command == "EXIT":
|
| - (exit_code, time_ms) = [int(x) for x in arg.split(' ', 1)]
|
| - self.finished_tests += 1
|
| - (binary, test) = self.tests[job_id]
|
| - self.print_test_status(test, time_ms)
|
| - if exit_code != 0:
|
| - self.failures.append(self.tests[job_id])
|
| - 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))
|
| - elif command == "TESTCNT":
|
| - self.total_tests = int(arg.split(' ', 1)[1])
|
| - self.out.transient_line("[0/%d] Running tests..." % self.total_tests)
|
| -
|
| - def logfile(self, job_id, name):
|
| - self.outputs[job_id] = name
|
| -
|
| - def log(self, line):
|
| - stdout_lock.acquire()
|
| - (prefix, output) = line.split(' ', 1)
|
| -
|
| - assert prefix[-1] == ':'
|
| - self.handle_meta(int(prefix[:-1]), output)
|
| - stdout_lock.release()
|
| -
|
| - def end(self):
|
| - if self.failures:
|
| - self.out.permanent_line("FAILED TESTS (%d/%d):"
|
| - % (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):
|
| - stdout_lock.acquire()
|
| - sys.stdout.write(line + "\n")
|
| - sys.stdout.flush()
|
| - stdout_lock.release()
|
| - def logfile(self, job_id, name):
|
| - with open(name) as f:
|
| - for line in f.readlines():
|
| - self.log(str(job_id) + '> ' + line.rstrip())
|
| - def end(self):
|
| - pass
|
| -
|
| -class CollectTestResults(object):
|
| - def __init__(self, json_dump_filepath):
|
| - self.test_results_lock = threading.Lock()
|
| - self.json_dump_file = open(json_dump_filepath, 'w')
|
| - self.test_results = {
|
| - "interrupted": False,
|
| - "path_delimiter": ".",
|
| - # Third version of the file format. See the link in the flag description
|
| - # for details.
|
| - "version": 3,
|
| - "seconds_since_epoch": int(time.time()),
|
| - "num_failures_by_type": {
|
| - "PASS": 0,
|
| - "FAIL": 0,
|
| - },
|
| - "tests": {},
|
| - }
|
| -
|
| - def log(self, test, result):
|
| - with self.test_results_lock:
|
| - self.test_results['num_failures_by_type'][result['actual']] += 1
|
| - results = self.test_results['tests']
|
| - for name in test.split('.'):
|
| - results = results.setdefault(name, {})
|
| - results.update(result)
|
| -
|
| - def dump_to_file_and_close(self):
|
| - json.dump(self.test_results, self.json_dump_file)
|
| - self.json_dump_file.close()
|
| -
|
| -class IgnoreTestResults(object):
|
| - def log(self, test, result):
|
| - pass
|
| - def dump_to_file_and_close(self):
|
| - pass
|
| -
|
| -# Record of test runtimes. Has built-in locking.
|
| -class TestTimes(object):
|
| - def __init__(self, save_file):
|
| - "Create new object seeded with saved test times from the given file."
|
| - self.__times = {} # (test binary, test name) -> runtime in ms
|
| -
|
| - # Protects calls to record_test_time(); other calls are not
|
| - # expected to be made concurrently.
|
| - self.__lock = threading.Lock()
|
| -
|
| - try:
|
| - with gzip.GzipFile(save_file, "rb") as f:
|
| - times = cPickle.load(f)
|
| - except (EOFError, IOError, cPickle.UnpicklingError, zlib.error):
|
| - # File doesn't exist, isn't readable, is malformed---whatever.
|
| - # Just ignore it.
|
| - return
|
| -
|
| - # Discard saved times if the format isn't right.
|
| - if type(times) is not dict:
|
| - 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, type(None)}):
|
| - return
|
| -
|
| - self.__times = times
|
| -
|
| - def get_test_time(self, binary, testname):
|
| - """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. If the test failed, runtime_ms should be None."""
|
| - with self.__lock:
|
| - self.__times[(binary, testname)] = runtime_ms
|
| -
|
| - def write_to_file(self, save_file):
|
| - "Write all the times to file."
|
| - try:
|
| - with open(save_file, "wb") as f:
|
| - with gzip.GzipFile("", "wb", 9, f) as gzf:
|
| - cPickle.dump(self.__times, gzf, cPickle.HIGHEST_PROTOCOL)
|
| - except IOError:
|
| - pass # ignore errors---saving the times isn't that important
|
| -
|
| -# Remove additional arguments (anything after --).
|
| -additional_args = []
|
| -
|
| -for i in range(len(sys.argv)):
|
| - if sys.argv[i] == '--':
|
| - additional_args = sys.argv[i+1:]
|
| - sys.argv = sys.argv[:i]
|
| - break
|
| -
|
| -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('--failed', action='store_true', default=False,
|
| - help='run only failed and new tests')
|
| -parser.add_option('-w', '--workers', type='int',
|
| - default=multiprocessing.cpu_count(),
|
| - help='number of workers to spawn')
|
| -parser.add_option('--gtest_color', type='string', default='yes',
|
| - help='color output')
|
| -parser.add_option('--gtest_filter', type='string', default='',
|
| - help='test filter')
|
| -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='list the run time of each test at the end of execution')
|
| -parser.add_option('--shard_count', type='int', default=1,
|
| - help='total number of shards (for sharding test execution '
|
| - 'between multiple machines)')
|
| -parser.add_option('--shard_index', type='int', default=0,
|
| - help='zero-indexed number identifying this shard (for '
|
| - 'sharding test execution between multiple machines)')
|
| -parser.add_option('--dump_json_test_results', type='string', default=None,
|
| - help='Saves the results of the tests as a JSON machine-'
|
| - 'readable file. The format of the file is specified at '
|
| - 'https://www.chromium.org/developers/the-json-test-results-format')
|
| -
|
| -(options, binaries) = parser.parse_args()
|
| -
|
| -if binaries == []:
|
| - parser.print_usage()
|
| - sys.exit(1)
|
| -
|
| -logger = RawFormat()
|
| -if options.format == 'raw':
|
| - pass
|
| -elif options.format == 'filter':
|
| - logger = FilterFormat()
|
| -else:
|
| - parser.error("Unknown output format: " + options.format)
|
| -
|
| -if options.shard_count < 1:
|
| - parser.error("Invalid number of shards: %d. Must be at least 1." %
|
| - options.shard_count)
|
| -if not (0 <= options.shard_index < options.shard_count):
|
| - parser.error("Invalid shard index: %d. Must be between 0 and %d "
|
| - "(less than the number of shards)." %
|
| - (options.shard_index, options.shard_count - 1))
|
| -
|
| -test_results = (IgnoreTestResults() if options.dump_json_test_results is None
|
| - else CollectTestResults(options.dump_json_test_results))
|
| -
|
| -# Find tests.
|
| -save_file = os.path.join(os.path.expanduser("~"), ".gtest-parallel-times")
|
| -times = TestTimes(save_file)
|
| -tests = []
|
| -for test_binary in binaries:
|
| - command = [test_binary]
|
| - if options.gtest_also_run_disabled_tests:
|
| - command += ['--gtest_also_run_disabled_tests']
|
| -
|
| - list_command = list(command)
|
| - if options.gtest_filter != '':
|
| - list_command += ['--gtest_filter=' + options.gtest_filter]
|
| -
|
| - try:
|
| - test_list = subprocess.Popen(list_command + ['--gtest_list_tests'],
|
| - stdout=subprocess.PIPE).communicate()[0]
|
| - except OSError as e:
|
| - sys.exit("%s: %s" % (test_binary, str(e)))
|
| -
|
| - command += additional_args
|
| -
|
| - test_group = ''
|
| - for line in test_list.split('\n'):
|
| - if not line.strip():
|
| - continue
|
| - if line[0] != " ":
|
| - # Remove comments for typed tests and strip whitespace.
|
| - test_group = line.split('#')[0].strip()
|
| - 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 = tests[options.shard_index::options.shard_count]
|
| -
|
| -if options.failed:
|
| - # The first element of each entry is the runtime of the most recent
|
| - # run if it was successful, or None if the test is new or the most
|
| - # recent run failed.
|
| - tests = [x for x in tests if x[0] is None]
|
| -
|
| -# 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
|
| -test_lock = threading.Lock()
|
| -job_id = 0
|
| -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 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()
|
| -
|
| - 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)
|
| -
|
| - test_results.log(test, {
|
| - "expected": "PASS",
|
| - "actual": "PASS" if code == 0 else "FAIL",
|
| - "time": runtime_ms,
|
| - })
|
| - logger.log("%s: EXIT %s %d" % (job_id, code, runtime_ms))
|
| - if code == 0:
|
| - return runtime_ms
|
| - global exit_code
|
| - exit_code = code
|
| - return None
|
| -
|
| -def worker():
|
| - global job_id
|
| - while True:
|
| - job = None
|
| - test_lock.acquire()
|
| - if job_id < len(tests):
|
| - (_, test_binary, test, command) = tests[job_id]
|
| - logger.log(str(job_id) + ': TEST ' + test_binary + ' ' + test)
|
| - job = (command, job_id, test)
|
| - job_id += 1
|
| - test_lock.release()
|
| - if job is None:
|
| - return
|
| - times.record_test_time(test_binary, test, run_job(job))
|
| -
|
| -def start_daemon(func):
|
| - t = threading.Thread(target=func)
|
| - t.daemon = True
|
| - t.start()
|
| - return t
|
| -
|
| -workers = [start_daemon(worker) for i in range(options.workers)]
|
| -
|
| -[t.join() for t in workers]
|
| -logger.end()
|
| -times.write_to_file(save_file)
|
| -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)
|
| -
|
| -test_results.dump_to_file_and_close()
|
| -sys.exit(-signal.SIGINT if sigint_handler.got_sigint() else exit_code)
|
|
|