This changes `./mach gtest` to, by default, run a separate process per test suite. It adds the `--combine-suites` flag to use the old behavior. If `--jobs` is specified, at most that many processes will be running at one time. Running a separate process per test suite ensures that tests are more repeatable and don't affect each other. It also interestingly results in a faster runtime than running all tests from one process, even when run serially (perhaps due to memory allocator behavior?). Differential Revision: https://phabricator.services.mozilla.com/D254995
133 lines
3.8 KiB
Python
133 lines
3.8 KiB
Python
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, # You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
import itertools
|
|
import os
|
|
import re
|
|
import subprocess
|
|
|
|
|
|
def get_gtest_suites(args, cwd, gtest_env):
|
|
"""
|
|
Get a list of gtest suite names from a gtest program.
|
|
|
|
* args - The arguments (including executable) for the gtest program.
|
|
* cwd - The working directory to use.
|
|
* gtest_env - Additional environment variables to set.
|
|
|
|
Returns a list of the suite names.
|
|
"""
|
|
# List the tests to get the suite names
|
|
args.append("--gtest_list_tests")
|
|
|
|
env = {}
|
|
env.update(os.environ)
|
|
env.update(gtest_env)
|
|
completed_proc = subprocess.run(
|
|
args, cwd=cwd, env=env, capture_output=True, check=True, text=True
|
|
)
|
|
output = completed_proc.stdout
|
|
|
|
# Suite names are exclusively text without whitespace, and followed by
|
|
# a '.', optionally with ` #` and type parameter information. This is
|
|
# specific enough to reasonably filter out some extra strings output by
|
|
# firefox.
|
|
SUITE_REGEX = re.compile(r"(\S+).( # .*)?")
|
|
|
|
def get_suite_name(line):
|
|
match = SUITE_REGEX.fullmatch(line)
|
|
if match:
|
|
return match[1]
|
|
|
|
suites = list(
|
|
filter(lambda x: x is not None, map(get_suite_name, output.splitlines()))
|
|
)
|
|
|
|
# Remove the `--gtest_list_tests` arg that we added
|
|
args.pop()
|
|
|
|
return suites
|
|
|
|
|
|
class _JoinedSubsetOfStrings:
|
|
"""
|
|
Efficient creation of joined strings for subsets of a list of strings.
|
|
|
|
This allows creation of joined strings in O(1) instead of O(n) each time (n = list
|
|
length), with a one-time O(n) cost.
|
|
"""
|
|
|
|
def __init__(self, between, strs):
|
|
"""
|
|
Arguments:
|
|
* between - the string with which to join the strings
|
|
* strs - an iterable of strings
|
|
"""
|
|
strs = list(strs)
|
|
self._string = between.join(strs)
|
|
betweenlen = len(between)
|
|
self._offsets = list(
|
|
itertools.accumulate(map(lambda s: len(s) + betweenlen, strs), initial=0)
|
|
)
|
|
|
|
def without(self, index):
|
|
"""Create a joined string excluding the given index."""
|
|
return (
|
|
self._string[: self._offsets[index]]
|
|
+ self._string[self._offsets[index + 1] :]
|
|
)
|
|
|
|
|
|
class SuiteFilter:
|
|
def __init__(self, joined, index, suite):
|
|
self._joined = joined
|
|
self.index = index
|
|
self.suite = suite
|
|
|
|
def create(self, existing_filter=None):
|
|
"""Create a filter to only run this suite."""
|
|
if existing_filter is None or existing_filter == "*":
|
|
return f"{self.suite}.*"
|
|
else:
|
|
return (
|
|
existing_filter
|
|
+ (":" if "-" in existing_filter else "-")
|
|
+ self._joined.without(self.index)
|
|
)
|
|
|
|
def set_in_env(self, env):
|
|
"""
|
|
Set the filter to only run this suite in an environment mapping.
|
|
|
|
Returns the passed env.
|
|
"""
|
|
env["GTEST_FILTER"] = self.create(env.get("GTEST_FILTER"))
|
|
return env
|
|
|
|
def __call__(self, val):
|
|
"""
|
|
If called on a dict, creates a copy and forwards to `set_in_env`,
|
|
otherwise forwards to `create`.
|
|
"""
|
|
if isinstance(val, dict):
|
|
return self.set_in_env(val.copy())
|
|
else:
|
|
return self.create(val)
|
|
|
|
|
|
def suite_filters(suites):
|
|
"""
|
|
Form gtest filters to limit tests to a single suite.
|
|
|
|
This is a generator that yields a SuiteFilter for each suite.
|
|
|
|
Arguments:
|
|
* suites - an iterable of the suite names
|
|
"""
|
|
suites = list(suites)
|
|
joined = _JoinedSubsetOfStrings(":", map(lambda s: f"{s}.*", suites))
|
|
|
|
for i, suite in enumerate(suites):
|
|
yield SuiteFilter(joined, i, suite)
|