# 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)