blob: 5bda14050bae1e058181143e33566d54743ee7cf [file] [log] [blame]
Kevin Marshallab4f810b2022-01-31 20:18:431#!/usr/bin/env python
Avi Drissmandfd880852022-09-15 20:11:092# Copyright 2018 The Chromium Authors
Kevin Marshallab4f810b2022-01-31 20:18:433# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Reads lines from files or stdin and identifies C++ tests.
6
7Outputs a filter that can be used with --gtest_filter or a filter file to
8run only the tests identified.
9
10Usage:
11
12Outputs filter for all test fixtures in a directory. --class-only avoids an
13overly long filter string.
Roman Sorokin34f5e2a2022-02-02 16:31:2714$ cat components/mycomp/**test.cc | make_gtest_filter.py --class-only
Kevin Marshallab4f810b2022-01-31 20:18:4315
16Outputs filter for all tests in a file.
Roman Sorokin34f5e2a2022-02-02 16:31:2717$ make_gtest_filter.py ./myfile_unittest.cc
Kevin Marshallab4f810b2022-01-31 20:18:4318
19Outputs filter for only test at line 123
Roman Sorokin34f5e2a2022-02-02 16:31:2720$ make_gtest_filter.py --line=123 ./myfile_unittest.cc
Kevin Marshallab4f810b2022-01-31 20:18:4321
22Formats output as a GTest filter file.
Roman Sorokin34f5e2a2022-02-02 16:31:2723$ make_gtest_filter.py ./myfile_unittest.cc --as-filter-file
Kevin Marshallab4f810b2022-01-31 20:18:4324
25Use a JSON failure summary as the input.
Roman Sorokin34f5e2a2022-02-02 16:31:2726$ make_gtest_filter.py summary.json --from-failure-summary
Kevin Marshallab4f810b2022-01-31 20:18:4327
28Elide the filter list using wildcards when possible.
Roman Sorokin34f5e2a2022-02-02 16:31:2729$ make_gtest_filter.py summary.json --from-failure-summary --wildcard-compress
Kevin Marshallab4f810b2022-01-31 20:18:4330"""
31
32from __future__ import print_function
33
34import argparse
35import collections
36import fileinput
37import json
38import re
39import sys
40
41
42class TrieNode:
43 def __init__(self):
44 # The number of strings which terminated on or underneath this node.
45 self.num_strings = 0
46
47 # The prefix subtries which follow |this|, keyed by their next character.
48 self.children = {}
49
50
51def PascalCaseSplit(input_string):
52 current_term = []
53 prev_char = ''
54
55 for current_char in input_string:
56 is_boundary = prev_char != '' and \
57 ((current_char.isupper() and prev_char.islower()) or \
58 (current_char.isalpha() != prev_char.isalpha()) or \
59 (current_char.isalnum() != prev_char.isalnum()))
60 prev_char = current_char
61
62 if is_boundary:
63 yield ''.join(current_term)
64 current_term = []
65
66 current_term.append(current_char)
67
68 if len(current_term) > 0:
69 yield ''.join(current_term)
70
71
72def TrieInsert(trie, value):
73 """Inserts the characters of 'value' into a trie, with every edge representing
74 a single character. An empty child set indicates end-of-string."""
75
76 for term in PascalCaseSplit(value):
77 trie.num_strings = trie.num_strings + 1
78 if term in trie.children:
79 trie = trie.children[term]
80 else:
81 subtrie = TrieNode()
82 trie.children[term] = subtrie
83 trie = subtrie
84
85 trie.num_strings = trie.num_strings + 1
86
87
88def ComputeWildcardsFromTrie(trie, min_depth, min_cases):
89 """Computes a list of wildcarded test case names from a trie using a depth
90 first traversal."""
91
92 WILDCARD = '*'
93
94 # Stack of values to process, initialized with the root node.
95 # The first item of the tuple is the substring represented by the traversal so
96 # far.
97 # The second item of the tuple is the TrieNode itself.
98 # The third item is the depth of the traversal so far.
99 to_process = [('', trie, 0)]
100
101 while len(to_process) > 0:
102 cur_prefix, cur_trie, cur_depth = to_process.pop()
103 assert (cur_trie.num_strings != 0)
104
105 if len(cur_trie.children) == 0:
106 # No more children == we're at the end of a string.
107 yield cur_prefix
108
109 elif (cur_depth == min_depth) and \
110 cur_trie.num_strings > min_cases:
111 # Trim traversal of this path if the path is deep enough and there
112 # are enough entries to warrant elision.
113 yield cur_prefix + WILDCARD
114
115 else:
116 # Traverse all children of this node.
117 for term, subtrie in cur_trie.children.items():
118 to_process.append((cur_prefix + term, subtrie, cur_depth + 1))
119
120
121def CompressWithWildcards(test_list, min_depth, min_cases):
122 """Given a list of SUITE.CASE names, generates an exclusion list using
123 wildcards to reduce redundancy.
124 For example:
125 Foo.TestOne
126 Foo.TestTwo
127 becomes:
128 Foo.Test*"""
129
130 suite_tries = {}
131
132 # First build up a trie based representations of all test case names,
133 # partitioned per-suite.
134 for case in test_list:
135 suite_name, test = case.split('.')
136 if not suite_name in suite_tries:
137 suite_tries[suite_name] = TrieNode()
138 TrieInsert(suite_tries[suite_name], test)
139
140 output = []
141 # Go through the suites' tries and generate wildcarded representations
142 # of the cases.
143 for suite in suite_tries.items():
144 suite_name, cases_trie = suite
145 for case_wildcard in ComputeWildcardsFromTrie(cases_trie, min_depth, \
146 min_cases):
147 output.append("{}.{}".format(suite_name, case_wildcard))
148
149 output.sort()
150 return output
151
152
153def GetFailedTestsFromTestLauncherSummary(summary):
154 failures = set()
155 for iteration in summary['per_iteration_data']:
156 for case_name, results in iteration.items():
157 for result in results:
158 if result['status'] == 'FAILURE':
159 failures.add(case_name)
160 return list(failures)
161
162
James Lee77f3c7ff2023-04-13 15:06:26163def GetFiltersForTests(tests, class_only):
164 # Note: Test names have the following structures:
165 # * FixtureName.TestName
James Lee2e9a30b2023-04-13 15:51:38166 # * InstantiationName/FixtureName.TestName/## (for TEST_P)
Tomasz Jurkiewicz0c853c72023-07-27 08:11:17167 # * InstantiationName/FixtureName/ParameterId.TestName (for TYPED_TEST_P)
James Lee77f3c7ff2023-04-13 15:06:26168 # * FixtureName.TestName/##
James Lee2e9a30b2023-04-13 15:51:38169 # * FixtureName/##.TestName (for TYPED_TEST)
James Lee77f3c7ff2023-04-13 15:06:26170 # Since this script doesn't parse instantiations, we generate filters to
171 # match either regular tests or instantiated tests.
172 if class_only:
173 fixtures = set([t.split('.')[0] for t in tests])
174 return [c + '.*' for c in fixtures] + \
Ming-Ying Chung2da9f252023-07-24 04:26:15175 ['*/' + c + '.*/*' for c in fixtures] + \
Tomasz Jurkiewicz0c853c72023-07-27 08:11:17176 ['*/' + c + '/*.*' for c in fixtures] + \
James Lee2e9a30b2023-04-13 15:51:38177 [c + '.*/*' for c in fixtures] + \
178 [c + '/*.*' for c in fixtures]
James Lee77f3c7ff2023-04-13 15:06:26179 else:
James Lee2e9a30b2023-04-13 15:51:38180 fixtures_and_tcs = [test.split('.', 1) for test in tests]
James Lee77f3c7ff2023-04-13 15:06:26181 return [c for c in tests] + \
182 ['*/' + c + '/*' for c in tests] + \
James Lee2e9a30b2023-04-13 15:51:38183 [c + '/*' for c in tests] + \
184 [fixture + '/*.' + tc for fixture, tc in fixtures_and_tcs]
James Lee77f3c7ff2023-04-13 15:06:26185
186
Kevin Marshallab4f810b2022-01-31 20:18:43187def main():
188 parser = argparse.ArgumentParser()
189 parser.add_argument(
190 '--input-format',
191 choices=['swarming_summary', 'test_launcher_summary', 'test_file'],
192 default='test_file')
193 parser.add_argument('--output-format',
194 choices=['file', 'args'],
195 default='args')
196 parser.add_argument('--wildcard-compress', action='store_true')
197 parser.add_argument(
198 '--wildcard-min-depth',
Bryant Chandler3b4a1372022-03-14 23:09:35199 type=int,
Kevin Marshallab4f810b2022-01-31 20:18:43200 default=1,
201 help="Minimum number of terms in a case before a wildcard may be " +
202 "used, so that prefixes are not excessively broad.")
203 parser.add_argument(
204 '--wildcard-min-cases',
Bryant Chandler3b4a1372022-03-14 23:09:35205 type=int,
Kevin Marshallab4f810b2022-01-31 20:18:43206 default=3,
207 help="Minimum number of cases in a filter before folding into a " +
208 "wildcard, so as to not create wildcards needlessly for small "
209 "numbers of similarly named test failures.")
210 parser.add_argument('--line', type=int)
211 parser.add_argument('--class-only', action='store_true')
212 parser.add_argument(
213 '--as-exclusions',
214 action='store_true',
215 help='Generate exclusion rules for test cases, instead of inclusions.')
216 args, left = parser.parse_known_args()
217
218 test_filters = []
219 if args.input_format == 'swarming_summary':
220 # Decode the JSON files separately and combine their contents.
221 test_filters = []
222 for json_file in left:
223 test_filters.extend(json.loads('\n'.join(open(json_file, 'r'))))
224
225 if args.wildcard_compress:
226 test_filters = CompressWithWildcards(test_filters,
227 args.wildcard_min_depth,
228 args.wildcard_min_cases)
229
230 elif args.input_format == 'test_launcher_summary':
231 # Decode the JSON files separately and combine their contents.
232 test_filters = []
233 for json_file in left:
234 test_filters.extend(
235 GetFailedTestsFromTestLauncherSummary(
236 json.loads('\n'.join(open(json_file, 'r')))))
237
238 if args.wildcard_compress:
239 test_filters = CompressWithWildcards(test_filters,
240 args.wildcard_min_depth,
241 args.wildcard_min_cases)
242
243 else:
244 file_input = fileinput.input(left)
245 if args.line:
246 # If --line is used, restrict text to a few lines around the requested
247 # line.
248 requested_line = args.line
249 selected_lines = []
250 for line in file_input:
251 if (fileinput.lineno() >= requested_line
252 and fileinput.lineno() <= requested_line + 1):
253 selected_lines.append(line)
254 txt = ''.join(selected_lines)
255 else:
256 txt = ''.join(list(file_input))
257
258 # This regex is not exhaustive, and should be updated as needed.
259 rx = re.compile(
260 r'^(?:TYPED_)?(?:IN_PROC_BROWSER_)?TEST(_F|_P)?\(\s*(\w+)\s*' + \
261 r',\s*(\w+)\s*\)',
262 flags=re.DOTALL | re.M)
263 tests = []
264 for m in rx.finditer(txt):
Dan Harrington86bb0652025-08-19 17:04:24265 # Try to include partially disabled tests.
266 fixture, test_name = m.group(2), m.group(3)
267 fixture = fixture.removeprefix('MAYBE_')
268 test_name = test_name.removeprefix('MAYBE_')
269 tests.append(fixture + '.' + test_name)
Kevin Marshallab4f810b2022-01-31 20:18:43270
Kevin Marshallab4f810b2022-01-31 20:18:43271 if args.wildcard_compress:
272 test_filters = CompressWithWildcards(tests, args.wildcard_min_depth,
273 args.wildcard_min_cases)
Kevin Marshallab4f810b2022-01-31 20:18:43274 else:
James Lee77f3c7ff2023-04-13 15:06:26275 test_filters = GetFiltersForTests(tests, args.class_only)
Kevin Marshallab4f810b2022-01-31 20:18:43276
277 if args.as_exclusions:
278 test_filters = ['-' + x for x in test_filters]
279
280 if args.output_format == 'file':
281 print('\n'.join(test_filters))
282 else:
283 print(':'.join(test_filters))
284
285 return 0
286
287
288if __name__ == '__main__':
289 sys.exit(main())