| Index: tools/isolate/run_test_cases.py
|
| diff --git a/tools/isolate/run_test_cases.py b/tools/isolate/run_test_cases.py
|
| deleted file mode 100755
|
| index 2520cc00676719b985eb5c9f54ad907b47e0b65b..0000000000000000000000000000000000000000
|
| --- a/tools/isolate/run_test_cases.py
|
| +++ /dev/null
|
| @@ -1,904 +0,0 @@
|
| -#!/usr/bin/env python
|
| -# Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| -# Use of this source code is governed by a BSD-style license that can be
|
| -# found in the LICENSE file.
|
| -
|
| -"""Runs each test cases as a single shard, single process execution.
|
| -
|
| -Similar to sharding_supervisor.py but finer grained. Runs multiple instances in
|
| -parallel.
|
| -"""
|
| -
|
| -import fnmatch
|
| -import json
|
| -import logging
|
| -import optparse
|
| -import os
|
| -import Queue
|
| -import subprocess
|
| -import sys
|
| -import threading
|
| -import time
|
| -
|
| -
|
| -# These are known to influence the way the output is generated.
|
| -KNOWN_GTEST_ENV_VARS = [
|
| - 'GTEST_ALSO_RUN_DISABLED_TESTS',
|
| - 'GTEST_BREAK_ON_FAILURE',
|
| - 'GTEST_CATCH_EXCEPTIONS',
|
| - 'GTEST_COLOR',
|
| - 'GTEST_FILTER',
|
| - 'GTEST_OUTPUT',
|
| - 'GTEST_PRINT_TIME',
|
| - 'GTEST_RANDOM_SEED',
|
| - 'GTEST_REPEAT',
|
| - 'GTEST_SHARD_INDEX',
|
| - 'GTEST_SHARD_STATUS_FILE',
|
| - 'GTEST_SHUFFLE',
|
| - 'GTEST_THROW_ON_FAILURE',
|
| - 'GTEST_TOTAL_SHARDS',
|
| -]
|
| -
|
| -# These needs to be poped out before running a test.
|
| -GTEST_ENV_VARS_TO_REMOVE = [
|
| - # TODO(maruel): Handle.
|
| - 'GTEST_ALSO_RUN_DISABLED_TESTS',
|
| - 'GTEST_FILTER',
|
| - # TODO(maruel): Handle.
|
| - 'GTEST_OUTPUT',
|
| - # TODO(maruel): Handle.
|
| - 'GTEST_RANDOM_SEED',
|
| - # TODO(maruel): Handle.
|
| - 'GTEST_REPEAT',
|
| - 'GTEST_SHARD_INDEX',
|
| - # TODO(maruel): Handle.
|
| - 'GTEST_SHUFFLE',
|
| - 'GTEST_TOTAL_SHARDS',
|
| -]
|
| -
|
| -
|
| -def num_processors():
|
| - """Returns the number of processors.
|
| -
|
| - Python on OSX 10.6 raises a NotImplementedError exception.
|
| - """
|
| - try:
|
| - # Multiprocessing
|
| - import multiprocessing
|
| - return multiprocessing.cpu_count()
|
| - except: # pylint: disable=W0702
|
| - # Mac OS 10.6
|
| - return int(os.sysconf('SC_NPROCESSORS_ONLN'))
|
| -
|
| -
|
| -if subprocess.mswindows:
|
| - import msvcrt # pylint: disable=F0401
|
| - from ctypes import wintypes
|
| - from ctypes import windll
|
| -
|
| - def ReadFile(handle, desired_bytes):
|
| - """Calls kernel32.ReadFile()."""
|
| - c_read = wintypes.DWORD()
|
| - buff = wintypes.create_string_buffer(desired_bytes+1)
|
| - windll.kernel32.ReadFile(
|
| - handle, buff, desired_bytes, wintypes.byref(c_read), None)
|
| - # NULL terminate it.
|
| - buff[c_read.value] = '\x00'
|
| - return wintypes.GetLastError(), buff.value
|
| -
|
| - def PeekNamedPipe(handle):
|
| - """Calls kernel32.PeekNamedPipe(). Simplified version."""
|
| - c_avail = wintypes.DWORD()
|
| - c_message = wintypes.DWORD()
|
| - success = windll.kernel32.PeekNamedPipe(
|
| - handle, None, 0, None, wintypes.byref(c_avail),
|
| - wintypes.byref(c_message))
|
| - if not success:
|
| - raise OSError(wintypes.GetLastError())
|
| - return c_avail.value
|
| -
|
| - def recv_impl(conn, maxsize, timeout):
|
| - """Reads from a pipe without blocking."""
|
| - if timeout:
|
| - start = time.time()
|
| - x = msvcrt.get_osfhandle(conn.fileno())
|
| - try:
|
| - while True:
|
| - avail = min(PeekNamedPipe(x), maxsize)
|
| - if avail:
|
| - return ReadFile(x, avail)[1]
|
| - if not timeout or (time.time() - start) >= timeout:
|
| - return
|
| - # Polling rocks.
|
| - time.sleep(0.001)
|
| - except OSError:
|
| - # Not classy but fits our needs.
|
| - return None
|
| -
|
| -else:
|
| - import fcntl
|
| - import select
|
| -
|
| - def recv_impl(conn, maxsize, timeout):
|
| - """Reads from a pipe without blocking."""
|
| - if not select.select([conn], [], [], timeout)[0]:
|
| - return None
|
| -
|
| - # Temporarily make it non-blocking.
|
| - flags = fcntl.fcntl(conn, fcntl.F_GETFL)
|
| - if not conn.closed:
|
| - fcntl.fcntl(conn, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
| - try:
|
| - return conn.read(maxsize)
|
| - finally:
|
| - if not conn.closed:
|
| - fcntl.fcntl(conn, fcntl.F_SETFL, flags)
|
| -
|
| -
|
| -class Failure(Exception):
|
| - pass
|
| -
|
| -
|
| -class Popen(subprocess.Popen):
|
| - """Adds timeout support on stdout and stderr.
|
| -
|
| - Inspired by
|
| - http://code.activestate.com/recipes/440554-module-to-allow-asynchronous-subprocess-use-on-win/
|
| - """
|
| - def recv(self, maxsize=None, timeout=None):
|
| - """Reads from stdout asynchronously."""
|
| - return self._recv('stdout', maxsize, timeout)
|
| -
|
| - def recv_err(self, maxsize=None, timeout=None):
|
| - """Reads from stderr asynchronously."""
|
| - return self._recv('stderr', maxsize, timeout)
|
| -
|
| - def _close(self, which):
|
| - getattr(self, which).close()
|
| - setattr(self, which, None)
|
| -
|
| - def _recv(self, which, maxsize, timeout):
|
| - conn = getattr(self, which)
|
| - if conn is None:
|
| - return None
|
| - data = recv_impl(conn, max(maxsize or 1024, 1), timeout or 0)
|
| - if not data:
|
| - return self._close(which)
|
| - if self.universal_newlines:
|
| - data = self._translate_newlines(data)
|
| - return data
|
| -
|
| -
|
| -def call_with_timeout(cmd, timeout, **kwargs):
|
| - """Runs an executable with an optional timeout."""
|
| - proc = Popen(
|
| - cmd,
|
| - stdin=subprocess.PIPE,
|
| - stdout=subprocess.PIPE,
|
| - **kwargs)
|
| - if timeout:
|
| - start = time.time()
|
| - output = ''
|
| - while proc.poll() is None:
|
| - remaining = max(timeout - (time.time() - start), 0.001)
|
| - data = proc.recv(timeout=remaining)
|
| - if data:
|
| - output += data
|
| - if (time.time() - start) >= timeout:
|
| - break
|
| - if (time.time() - start) >= timeout and proc.poll() is None:
|
| - logging.debug('Kill %s %s' % ((time.time() - start) , timeout))
|
| - proc.kill()
|
| - proc.wait()
|
| - # Try reading a last time.
|
| - while True:
|
| - data = proc.recv()
|
| - if not data:
|
| - break
|
| - output += data
|
| - else:
|
| - # This code path is much faster.
|
| - output = proc.communicate()[0]
|
| - return output, proc.returncode
|
| -
|
| -
|
| -class QueueWithTimeout(Queue.Queue):
|
| - """Implements timeout support in join()."""
|
| -
|
| - # QueueWithTimeout.join: Arguments number differs from overridden method
|
| - # pylint: disable=W0221
|
| - def join(self, timeout=None):
|
| - """Returns True if all tasks are finished."""
|
| - if not timeout:
|
| - return Queue.Queue.join(self)
|
| - start = time.time()
|
| - self.all_tasks_done.acquire()
|
| - try:
|
| - while self.unfinished_tasks:
|
| - remaining = time.time() - start - timeout
|
| - if remaining <= 0:
|
| - break
|
| - self.all_tasks_done.wait(remaining)
|
| - return not self.unfinished_tasks
|
| - finally:
|
| - self.all_tasks_done.release()
|
| -
|
| -
|
| -class WorkerThread(threading.Thread):
|
| - """Keeps the results of each task in a thread-local outputs variable."""
|
| - def __init__(self, tasks, *args, **kwargs):
|
| - super(WorkerThread, self).__init__(*args, **kwargs)
|
| - self._tasks = tasks
|
| - self.outputs = []
|
| - self.exceptions = []
|
| -
|
| - self.daemon = True
|
| - self.start()
|
| -
|
| - def run(self):
|
| - """Runs until a None task is queued."""
|
| - while True:
|
| - task = self._tasks.get()
|
| - if task is None:
|
| - # We're done.
|
| - return
|
| - try:
|
| - func, args, kwargs = task
|
| - self.outputs.append(func(*args, **kwargs))
|
| - except Exception, e:
|
| - logging.error('Caught exception! %s' % e)
|
| - self.exceptions.append(sys.exc_info())
|
| - finally:
|
| - self._tasks.task_done()
|
| -
|
| -
|
| -class ThreadPool(object):
|
| - """Implements a multithreaded worker pool oriented for mapping jobs with
|
| - thread-local result storage.
|
| - """
|
| - def __init__(self, num_threads):
|
| - self._tasks = QueueWithTimeout()
|
| - self._workers = [
|
| - WorkerThread(self._tasks, name='worker-%d' % i)
|
| - for i in range(num_threads)
|
| - ]
|
| -
|
| - def add_task(self, func, *args, **kwargs):
|
| - """Adds a task, a function to be executed by a worker.
|
| -
|
| - The function's return value will be stored in the the worker's thread local
|
| - outputs list.
|
| - """
|
| - self._tasks.put((func, args, kwargs))
|
| -
|
| - def join(self, progress=None, timeout=None):
|
| - """Extracts all the results from each threads unordered."""
|
| - if progress and timeout:
|
| - while not self._tasks.join(timeout):
|
| - progress.print_update()
|
| - progress.print_update()
|
| - else:
|
| - self._tasks.join()
|
| - out = []
|
| - for w in self._workers:
|
| - if w.exceptions:
|
| - raise w.exceptions[0][0], w.exceptions[0][1], w.exceptions[0][2]
|
| - out.extend(w.outputs)
|
| - w.outputs = []
|
| - # Look for exceptions.
|
| - return out
|
| -
|
| - def close(self):
|
| - """Closes all the threads."""
|
| - for _ in range(len(self._workers)):
|
| - # Enqueueing None causes the worker to stop.
|
| - self._tasks.put(None)
|
| - for t in self._workers:
|
| - t.join()
|
| -
|
| - def __enter__(self):
|
| - """Enables 'with' statement."""
|
| - return self
|
| -
|
| - def __exit__(self, exc_type, exc_value, traceback):
|
| - """Enables 'with' statement."""
|
| - self.close()
|
| -
|
| -
|
| -class Progress(object):
|
| - """Prints progress and accepts updates thread-safely."""
|
| - def __init__(self, size):
|
| - # To be used in the primary thread
|
| - self.last_printed_line = ''
|
| - self.index = 0
|
| - self.start = time.time()
|
| - self.size = size
|
| -
|
| - # To be used in all threads.
|
| - self.queued_lines = Queue.Queue()
|
| -
|
| - def update_item(self, name, index=True, size=False):
|
| - self.queued_lines.put((name, index, size))
|
| -
|
| - def print_update(self):
|
| - """Prints the current status."""
|
| - while True:
|
| - try:
|
| - name, index, size = self.queued_lines.get_nowait()
|
| - except Queue.Empty:
|
| - break
|
| -
|
| - if size:
|
| - self.size += 1
|
| - if index:
|
| - self.index += 1
|
| - alignment = str(len(str(self.size)))
|
| - next_line = ('[%' + alignment + 'd/%d] %6.2fs %s') % (
|
| - self.index,
|
| - self.size,
|
| - time.time() - self.start,
|
| - name)
|
| - # Fill it with whitespace.
|
| - # TODO(maruel): Read the console width when prossible and trim
|
| - # next_line.
|
| - # TODO(maruel): When not a console is used, do not fill with whitepace
|
| - # but use \n instead.
|
| - prefix = '\r' if self.last_printed_line else ''
|
| - line = '%s%s%s' % (
|
| - prefix,
|
| - next_line,
|
| - ' ' * max(0, len(self.last_printed_line) - len(next_line)))
|
| - self.last_printed_line = next_line
|
| - else:
|
| - line = '\n%s\n' % name.strip('\n')
|
| - self.last_printed_line = ''
|
| -
|
| - sys.stdout.write(line)
|
| -
|
| -
|
| -def fix_python_path(cmd):
|
| - """Returns the fixed command line to call the right python executable."""
|
| - out = cmd[:]
|
| - if out[0] == 'python':
|
| - out[0] = sys.executable
|
| - elif out[0].endswith('.py'):
|
| - out.insert(0, sys.executable)
|
| - return out
|
| -
|
| -
|
| -def setup_gtest_env():
|
| - """Copy the enviroment variables and setup for running a gtest."""
|
| - env = os.environ.copy()
|
| - for name in GTEST_ENV_VARS_TO_REMOVE:
|
| - env.pop(name, None)
|
| -
|
| - # Forcibly enable color by default, if not already disabled.
|
| - env.setdefault('GTEST_COLOR', 'on')
|
| -
|
| - return env
|
| -
|
| -
|
| -def gtest_list_tests(cmd):
|
| - """List all the test cases for a google test.
|
| -
|
| - See more info at http://code.google.com/p/googletest/.
|
| - """
|
| - cmd = cmd[:]
|
| - cmd.append('--gtest_list_tests')
|
| - env = setup_gtest_env()
|
| - try:
|
| - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
| - env=env)
|
| - except OSError, e:
|
| - raise Failure('Failed to run %s\n%s' % (' '.join(cmd), str(e)))
|
| - out, err = p.communicate()
|
| - if p.returncode:
|
| - raise Failure(
|
| - 'Failed to run %s\nstdout:\n%s\nstderr:\n%s' %
|
| - (' '.join(cmd), out, err), p.returncode)
|
| - # pylint: disable=E1103
|
| - if err and not err.startswith('Xlib: extension "RANDR" missing on display '):
|
| - logging.error('Unexpected spew in gtest_list_tests:\n%s\n%s', err, cmd)
|
| - return out
|
| -
|
| -
|
| -def filter_shards(tests, index, shards):
|
| - """Filters the shards.
|
| -
|
| - Watch out about integer based arithmetics.
|
| - """
|
| - # The following code could be made more terse but I liked the extra clarity.
|
| - assert 0 <= index < shards
|
| - total = len(tests)
|
| - quotient, remainder = divmod(total, shards)
|
| - # 1 item of each remainder is distributed over the first 0:remainder shards.
|
| - # For example, with total == 5, index == 1, shards == 3
|
| - # min_bound == 2, max_bound == 4.
|
| - min_bound = quotient * index + min(index, remainder)
|
| - max_bound = quotient * (index + 1) + min(index + 1, remainder)
|
| - return tests[min_bound:max_bound]
|
| -
|
| -
|
| -def filter_bad_tests(tests, disabled=False, fails=False, flaky=False):
|
| - """Filters out DISABLED_, FAILS_ or FLAKY_ tests."""
|
| - def starts_with(a, b, prefix):
|
| - return a.startswith(prefix) or b.startswith(prefix)
|
| -
|
| - def valid(test):
|
| - fixture, case = test.split('.', 1)
|
| - if not disabled and starts_with(fixture, case, 'DISABLED_'):
|
| - return False
|
| - if not fails and starts_with(fixture, case, 'FAILS_'):
|
| - return False
|
| - if not flaky and starts_with(fixture, case, 'FLAKY_'):
|
| - return False
|
| - return True
|
| -
|
| - return [test for test in tests if valid(test)]
|
| -
|
| -
|
| -def parse_gtest_cases(out):
|
| - """Returns the flattened list of test cases in the executable.
|
| -
|
| - The returned list is sorted so it is not dependent on the order of the linked
|
| - objects.
|
| -
|
| - Expected format is a concatenation of this:
|
| - TestFixture1
|
| - TestCase1
|
| - TestCase2
|
| - """
|
| - tests = []
|
| - fixture = None
|
| - lines = out.splitlines()
|
| - while lines:
|
| - line = lines.pop(0)
|
| - if not line:
|
| - break
|
| - if not line.startswith(' '):
|
| - fixture = line
|
| - else:
|
| - case = line[2:]
|
| - if case.startswith('YOU HAVE'):
|
| - # It's a 'YOU HAVE foo bar' line. We're done.
|
| - break
|
| - assert ' ' not in case
|
| - tests.append(fixture + case)
|
| - return sorted(tests)
|
| -
|
| -
|
| -def list_test_cases(cmd, index, shards, disabled, fails, flaky):
|
| - """Returns the list of test cases according to the specified criterias."""
|
| - tests = parse_gtest_cases(gtest_list_tests(cmd))
|
| - if shards:
|
| - tests = filter_shards(tests, index, shards)
|
| - return filter_bad_tests(tests, disabled, fails, flaky)
|
| -
|
| -
|
| -class RunSome(object):
|
| - """Thread-safe object deciding if testing should continue."""
|
| - def __init__(self, expected_count, retries, min_failures, max_failure_ratio):
|
| - """Determines if it is better to give up testing after an amount of failures
|
| - and successes.
|
| -
|
| - Arguments:
|
| - - expected_count is the expected number of elements to run.
|
| - - retries is how many time a failing element can be retried. retries should
|
| - be set to the maximum number of retries per failure. This permits
|
| - dampening the curve to determine threshold where to stop.
|
| - - min_failures is the minimal number of failures to tolerate, to put a lower
|
| - limit when expected_count is small. This value is multiplied by the number
|
| - of retries.
|
| - - max_failure_ratio is the the ratio of permitted failures, e.g. 0.1 to stop
|
| - after 10% of failed test cases.
|
| -
|
| - For large values of expected_count, the number of tolerated failures will be
|
| - at maximum "(expected_count * retries) * max_failure_ratio".
|
| -
|
| - For small values of expected_count, the number of tolerated failures will be
|
| - at least "min_failures * retries".
|
| - """
|
| - assert 0 < expected_count
|
| - assert 0 <= retries < 100
|
| - assert 0 <= min_failures
|
| - assert 0. < max_failure_ratio < 1.
|
| - # Constants.
|
| - self._expected_count = expected_count
|
| - self._retries = retries
|
| - self._min_failures = min_failures
|
| - self._max_failure_ratio = max_failure_ratio
|
| -
|
| - self._min_failures_tolerated = self._min_failures * self._retries
|
| - # Pre-calculate the maximum number of allowable failures. Note that
|
| - # _max_failures can be lower than _min_failures.
|
| - self._max_failures_tolerated = round(
|
| - (expected_count * retries) * max_failure_ratio)
|
| -
|
| - # Variables.
|
| - self._lock = threading.Lock()
|
| - self._passed = 0
|
| - self._failures = 0
|
| -
|
| - def should_stop(self):
|
| - """Stops once a threshold was reached. This includes retries."""
|
| - with self._lock:
|
| - # Accept at least the minimum number of failures.
|
| - if self._failures <= self._min_failures_tolerated:
|
| - return False
|
| - return self._failures >= self._max_failures_tolerated
|
| -
|
| - def got_result(self, passed):
|
| - with self._lock:
|
| - if passed:
|
| - self._passed += 1
|
| - else:
|
| - self._failures += 1
|
| -
|
| - def __str__(self):
|
| - return '%s(%d, %d, %d, %.3f)' % (
|
| - self.__class__.__name__,
|
| - self._expected_count,
|
| - self._retries,
|
| - self._min_failures,
|
| - self._max_failure_ratio)
|
| -
|
| -
|
| -class RunAll(object):
|
| - """Never fails."""
|
| - @staticmethod
|
| - def should_stop():
|
| - return False
|
| - @staticmethod
|
| - def got_result(_):
|
| - pass
|
| -
|
| -
|
| -class Runner(object):
|
| - def __init__(self, cmd, cwd_dir, timeout, progress, retry_count, decider):
|
| - # Constants
|
| - self.cmd = cmd[:]
|
| - self.cwd_dir = cwd_dir
|
| - self.timeout = timeout
|
| - self.progress = progress
|
| - self.retry_count = retry_count
|
| - # It is important to remove the shard environment variables since it could
|
| - # conflict with --gtest_filter.
|
| - self.env = setup_gtest_env()
|
| - self.decider = decider
|
| -
|
| - def map(self, test_case):
|
| - """Traces a single test case and returns its output."""
|
| - cmd = self.cmd[:]
|
| - cmd.append('--gtest_filter=%s' % test_case)
|
| - out = []
|
| - for retry in range(self.retry_count):
|
| - if self.decider.should_stop():
|
| - break
|
| -
|
| - start = time.time()
|
| - output, returncode = call_with_timeout(
|
| - cmd,
|
| - self.timeout,
|
| - cwd=self.cwd_dir,
|
| - stderr=subprocess.STDOUT,
|
| - env=self.env)
|
| - duration = time.time() - start
|
| - data = {
|
| - 'test_case': test_case,
|
| - 'returncode': returncode,
|
| - 'duration': duration,
|
| - # It needs to be valid utf-8 otherwise it can't be store.
|
| - 'output': output.decode('ascii', 'ignore').encode('utf-8'),
|
| - }
|
| - if '[ RUN ]' not in output:
|
| - # Can't find gtest marker, mark it as invalid.
|
| - returncode = returncode or 1
|
| - self.decider.got_result(not bool(returncode))
|
| - out.append(data)
|
| - if sys.platform == 'win32':
|
| - output = output.replace('\r\n', '\n')
|
| - size = returncode and retry != self.retry_count - 1
|
| - if retry:
|
| - self.progress.update_item(
|
| - '%s (%.2fs) - retry #%d' % (test_case, duration, retry),
|
| - True,
|
| - size)
|
| - else:
|
| - self.progress.update_item(
|
| - '%s (%.2fs)' % (test_case, duration), True, size)
|
| - if logging.getLogger().isEnabledFor(logging.INFO):
|
| - self.progress.update_item(output, False, False)
|
| - if not returncode:
|
| - break
|
| - else:
|
| - # The test failed. Print its output. No need to print it with logging
|
| - # level at INFO since it was already printed above.
|
| - if not logging.getLogger().isEnabledFor(logging.INFO):
|
| - self.progress.update_item(output, False, False)
|
| - return out
|
| -
|
| -
|
| -def get_test_cases(cmd, whitelist, blacklist, index, shards):
|
| - """Returns the filtered list of test cases.
|
| -
|
| - This is done synchronously.
|
| - """
|
| - try:
|
| - tests = list_test_cases(cmd, index, shards, False, False, False)
|
| - except Failure, e:
|
| - print e.args[0]
|
| - return None
|
| -
|
| - if shards:
|
| - # This is necessary for Swarm log parsing.
|
| - print 'Note: This is test shard %d of %d.' % (index+1, shards)
|
| -
|
| - # Filters the test cases with the two lists.
|
| - if blacklist:
|
| - tests = [
|
| - t for t in tests if not any(fnmatch.fnmatch(t, s) for s in blacklist)
|
| - ]
|
| - if whitelist:
|
| - tests = [
|
| - t for t in tests if any(fnmatch.fnmatch(t, s) for s in whitelist)
|
| - ]
|
| - logging.info('Found %d test cases in %s' % (len(tests), ' '.join(cmd)))
|
| - return tests
|
| -
|
| -
|
| -def LogResults(result_file, results):
|
| - """Write the results out to a file if one is given."""
|
| - if not result_file:
|
| - return
|
| - with open(result_file, 'wb') as f:
|
| - json.dump(results, f, sort_keys=True, indent=2)
|
| -
|
| -
|
| -def run_test_cases(cmd, test_cases, jobs, timeout, run_all, result_file):
|
| - """Traces test cases one by one."""
|
| - if not test_cases:
|
| - return 0
|
| - progress = Progress(len(test_cases))
|
| - retries = 3
|
| - if run_all:
|
| - decider = RunAll()
|
| - else:
|
| - # If 10% of test cases fail, just too bad.
|
| - decider = RunSome(len(test_cases), retries, 2, 0.1)
|
| - with ThreadPool(jobs) as pool:
|
| - function = Runner(cmd, os.getcwd(), timeout, progress, retries, decider).map
|
| - for test_case in test_cases:
|
| - pool.add_task(function, test_case)
|
| - results = pool.join(progress, 0.1)
|
| - duration = time.time() - progress.start
|
| - results = dict((item[0]['test_case'], item) for item in results if item)
|
| - LogResults(result_file, results)
|
| - sys.stdout.write('\n')
|
| - total = len(results)
|
| - if not total:
|
| - return 1
|
| -
|
| - # Classify the results
|
| - success = []
|
| - flaky = []
|
| - fail = []
|
| - nb_runs = 0
|
| - for test_case in sorted(results):
|
| - items = results[test_case]
|
| - nb_runs += len(items)
|
| - if not any(not i['returncode'] for i in items):
|
| - fail.append(test_case)
|
| - elif len(items) > 1 and any(not i['returncode'] for i in items):
|
| - flaky.append(test_case)
|
| - elif len(items) == 1 and items[0]['returncode'] == 0:
|
| - success.append(test_case)
|
| - else:
|
| - assert False, items
|
| -
|
| - print 'Summary:'
|
| - for test_case in sorted(flaky):
|
| - items = results[test_case]
|
| - print '%s is flaky (tried %d times)' % (test_case, len(items))
|
| -
|
| - for test_case in sorted(fail):
|
| - print '%s failed' % (test_case)
|
| -
|
| - if decider.should_stop():
|
| - print '** STOPPED EARLY due to high failure rate **'
|
| - print 'Success: %4d %5.2f%%' % (len(success), len(success) * 100. / total)
|
| - print 'Flaky: %4d %5.2f%%' % (len(flaky), len(flaky) * 100. / total)
|
| - print 'Fail: %4d %5.2f%%' % (len(fail), len(fail) * 100. / total)
|
| - print '%.1fs Done running %d tests with %d executions. %.1f test/s' % (
|
| - duration,
|
| - len(results),
|
| - nb_runs,
|
| - nb_runs / duration)
|
| - return int(bool(fail))
|
| -
|
| -
|
| -class OptionParserWithLogging(optparse.OptionParser):
|
| - """Adds --verbose option."""
|
| - def __init__(self, verbose=0, **kwargs):
|
| - optparse.OptionParser.__init__(self, **kwargs)
|
| - self.add_option(
|
| - '-v', '--verbose',
|
| - action='count',
|
| - default=verbose,
|
| - help='Use multiple times to increase verbosity')
|
| -
|
| - def parse_args(self, *args, **kwargs):
|
| - options, args = optparse.OptionParser.parse_args(self, *args, **kwargs)
|
| - levels = [logging.ERROR, logging.INFO, logging.DEBUG]
|
| - logging.basicConfig(
|
| - level=levels[min(len(levels)-1, options.verbose)],
|
| - format='%(levelname)5s %(module)15s(%(lineno)3d): %(message)s')
|
| - return options, args
|
| -
|
| -
|
| -class OptionParserWithTestSharding(OptionParserWithLogging):
|
| - """Adds automatic handling of test sharding"""
|
| - def __init__(self, **kwargs):
|
| - OptionParserWithLogging.__init__(self, **kwargs)
|
| -
|
| - def as_digit(variable, default):
|
| - return int(variable) if variable.isdigit() else default
|
| -
|
| - group = optparse.OptionGroup(self, 'Which shard to run')
|
| - group.add_option(
|
| - '-I', '--index',
|
| - type='int',
|
| - default=as_digit(os.environ.get('GTEST_SHARD_INDEX', ''), None),
|
| - help='Shard index to run')
|
| - group.add_option(
|
| - '-S', '--shards',
|
| - type='int',
|
| - default=as_digit(os.environ.get('GTEST_TOTAL_SHARDS', ''), None),
|
| - help='Total number of shards to calculate from the --index to run')
|
| - self.add_option_group(group)
|
| -
|
| - def parse_args(self, *args, **kwargs):
|
| - options, args = OptionParserWithLogging.parse_args(self, *args, **kwargs)
|
| - if bool(options.shards) != bool(options.index is not None):
|
| - self.error('Use both --index X --shards Y or none of them')
|
| - return options, args
|
| -
|
| -
|
| -class OptionParserWithTestShardingAndFiltering(OptionParserWithTestSharding):
|
| - """Adds automatic handling of test sharding and filtering."""
|
| - def __init__(self, *args, **kwargs):
|
| - OptionParserWithTestSharding.__init__(self, *args, **kwargs)
|
| -
|
| - group = optparse.OptionGroup(self, 'Which test cases to run')
|
| - group.add_option(
|
| - '-w', '--whitelist',
|
| - default=[],
|
| - action='append',
|
| - help='filter to apply to test cases to run, wildcard-style, defaults '
|
| - 'to all test')
|
| - group.add_option(
|
| - '-b', '--blacklist',
|
| - default=[],
|
| - action='append',
|
| - help='filter to apply to test cases to skip, wildcard-style, defaults '
|
| - 'to no test')
|
| - group.add_option(
|
| - '-T', '--test-case-file',
|
| - help='File containing the exact list of test cases to run')
|
| - group.add_option(
|
| - '--gtest_filter',
|
| - default=os.environ.get('GTEST_FILTER', ''),
|
| - help='Runs a single test, provideded to keep compatibility with '
|
| - 'other tools')
|
| - self.add_option_group(group)
|
| -
|
| - def parse_args(self, *args, **kwargs):
|
| - options, args = OptionParserWithTestSharding.parse_args(
|
| - self, *args, **kwargs)
|
| -
|
| - if options.gtest_filter:
|
| - # Override any other option.
|
| - # Based on UnitTestOptions::FilterMatchesTest() in
|
| - # http://code.google.com/p/googletest/source/browse/#svn%2Ftrunk%2Fsrc
|
| - if '-' in options.gtest_filter:
|
| - options.whitelist, options.blacklist = options.gtest_filter.split('-',
|
| - 1)
|
| - else:
|
| - options.whitelist = options.gtest_filter
|
| - options.blacklist = ''
|
| - options.whitelist = [i for i in options.whitelist.split(':') if i]
|
| - options.blacklist = [i for i in options.blacklist.split(':') if i]
|
| -
|
| - return options, args
|
| -
|
| - @staticmethod
|
| - def process_gtest_options(cmd, options):
|
| - """Grabs the test cases."""
|
| - if options.test_case_file:
|
| - with open(options.test_case_file, 'r') as f:
|
| - return sorted(filter(None, f.read().splitlines()))
|
| - else:
|
| - return get_test_cases(
|
| - cmd,
|
| - options.whitelist,
|
| - options.blacklist,
|
| - options.index,
|
| - options.shards)
|
| -
|
| -
|
| -class OptionParserTestCases(OptionParserWithTestShardingAndFiltering):
|
| - def __init__(self, *args, **kwargs):
|
| - OptionParserWithTestShardingAndFiltering.__init__(self, *args, **kwargs)
|
| - self.add_option(
|
| - '-j', '--jobs',
|
| - type='int',
|
| - default=num_processors(),
|
| - help='number of parallel jobs; default=%default')
|
| - self.add_option(
|
| - '-t', '--timeout',
|
| - type='int',
|
| - default=120,
|
| - help='Timeout for a single test case, in seconds default:%default')
|
| -
|
| -
|
| -def main(argv):
|
| - """CLI frontend to validate arguments."""
|
| - parser = OptionParserTestCases(
|
| - usage='%prog <options> [gtest]',
|
| - verbose=int(os.environ.get('ISOLATE_DEBUG', 0)))
|
| - parser.add_option(
|
| - '--run-all',
|
| - action='store_true',
|
| - default=bool(int(os.environ.get('RUN_TEST_CASES_RUN_ALL', '0'))),
|
| - help='Do not fail early when a large number of test cases fail')
|
| - parser.add_option(
|
| - '--no-dump',
|
| - action='store_true',
|
| - help='do not generate a .run_test_cases file')
|
| - parser.add_option(
|
| - '--result',
|
| - default=os.environ.get('RUN_TEST_CASES_RESULT_FILE', ''),
|
| - help='Override the default name of the generated .run_test_cases file')
|
| - parser.add_option(
|
| - '--gtest_list_tests',
|
| - action='store_true',
|
| - help='List all the test cases unformatted. Keeps compatibility with the '
|
| - 'executable itself.')
|
| - options, args = parser.parse_args(argv)
|
| -
|
| - if not args:
|
| - parser.error(
|
| - 'Please provide the executable line to run, if you need fancy things '
|
| - 'like xvfb, start this script from *inside* xvfb, it\'ll be much faster'
|
| - '.')
|
| -
|
| - cmd = fix_python_path(args)
|
| -
|
| - if options.gtest_list_tests:
|
| - # Special case, return the output of the target unmodified.
|
| - return subprocess.call(args + ['--gtest_list_tests'])
|
| -
|
| - test_cases = parser.process_gtest_options(cmd, options)
|
| - if not test_cases:
|
| - # If test_cases is None then there was a problem generating the tests to
|
| - # run, so this should be considered a failure.
|
| - return int(test_cases is None)
|
| -
|
| - if options.no_dump:
|
| - result_file = None
|
| - else:
|
| - if options.result:
|
| - result_file = options.result
|
| - else:
|
| - result_file = '%s.run_test_cases' % args[-1]
|
| -
|
| - return run_test_cases(
|
| - cmd,
|
| - test_cases,
|
| - options.jobs,
|
| - options.timeout,
|
| - options.run_all,
|
| - result_file)
|
| -
|
| -
|
| -if __name__ == '__main__':
|
| - sys.exit(main(sys.argv[1:]))
|
|
|