123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- #!/usr/bin/env python
- # Copyright (c) 2018 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.
- """Reads lines from files or stdin and identifies C++ tests.
- Outputs a filter that can be used with --gtest_filter or a filter file to
- run only the tests identified.
- Usage:
- Outputs filter for all test fixtures in a directory. --class-only avoids an
- overly long filter string.
- $ cat components/mycomp/**test.cc | make_gtest_filter.py --class-only
- Outputs filter for all tests in a file.
- $ make_gtest_filter.py ./myfile_unittest.cc
- Outputs filter for only test at line 123
- $ make_gtest_filter.py --line=123 ./myfile_unittest.cc
- Formats output as a GTest filter file.
- $ make_gtest_filter.py ./myfile_unittest.cc --as-filter-file
- Use a JSON failure summary as the input.
- $ make_gtest_filter.py summary.json --from-failure-summary
- Elide the filter list using wildcards when possible.
- $ make_gtest_filter.py summary.json --from-failure-summary --wildcard-compress
- """
- from __future__ import print_function
- import argparse
- import collections
- import fileinput
- import json
- import re
- import sys
- class TrieNode:
- def __init__(self):
- # The number of strings which terminated on or underneath this node.
- self.num_strings = 0
- # The prefix subtries which follow |this|, keyed by their next character.
- self.children = {}
- def PascalCaseSplit(input_string):
- current_term = []
- prev_char = ''
- for current_char in input_string:
- is_boundary = prev_char != '' and \
- ((current_char.isupper() and prev_char.islower()) or \
- (current_char.isalpha() != prev_char.isalpha()) or \
- (current_char.isalnum() != prev_char.isalnum()))
- prev_char = current_char
- if is_boundary:
- yield ''.join(current_term)
- current_term = []
- current_term.append(current_char)
- if len(current_term) > 0:
- yield ''.join(current_term)
- def TrieInsert(trie, value):
- """Inserts the characters of 'value' into a trie, with every edge representing
- a single character. An empty child set indicates end-of-string."""
- for term in PascalCaseSplit(value):
- trie.num_strings = trie.num_strings + 1
- if term in trie.children:
- trie = trie.children[term]
- else:
- subtrie = TrieNode()
- trie.children[term] = subtrie
- trie = subtrie
- trie.num_strings = trie.num_strings + 1
- def ComputeWildcardsFromTrie(trie, min_depth, min_cases):
- """Computes a list of wildcarded test case names from a trie using a depth
- first traversal."""
- WILDCARD = '*'
- # Stack of values to process, initialized with the root node.
- # The first item of the tuple is the substring represented by the traversal so
- # far.
- # The second item of the tuple is the TrieNode itself.
- # The third item is the depth of the traversal so far.
- to_process = [('', trie, 0)]
- while len(to_process) > 0:
- cur_prefix, cur_trie, cur_depth = to_process.pop()
- assert (cur_trie.num_strings != 0)
- if len(cur_trie.children) == 0:
- # No more children == we're at the end of a string.
- yield cur_prefix
- elif (cur_depth == min_depth) and \
- cur_trie.num_strings > min_cases:
- # Trim traversal of this path if the path is deep enough and there
- # are enough entries to warrant elision.
- yield cur_prefix + WILDCARD
- else:
- # Traverse all children of this node.
- for term, subtrie in cur_trie.children.items():
- to_process.append((cur_prefix + term, subtrie, cur_depth + 1))
- def CompressWithWildcards(test_list, min_depth, min_cases):
- """Given a list of SUITE.CASE names, generates an exclusion list using
- wildcards to reduce redundancy.
- For example:
- Foo.TestOne
- Foo.TestTwo
- becomes:
- Foo.Test*"""
- suite_tries = {}
- # First build up a trie based representations of all test case names,
- # partitioned per-suite.
- for case in test_list:
- suite_name, test = case.split('.')
- if not suite_name in suite_tries:
- suite_tries[suite_name] = TrieNode()
- TrieInsert(suite_tries[suite_name], test)
- output = []
- # Go through the suites' tries and generate wildcarded representations
- # of the cases.
- for suite in suite_tries.items():
- suite_name, cases_trie = suite
- for case_wildcard in ComputeWildcardsFromTrie(cases_trie, min_depth, \
- min_cases):
- output.append("{}.{}".format(suite_name, case_wildcard))
- output.sort()
- return output
- def GetFailedTestsFromTestLauncherSummary(summary):
- failures = set()
- for iteration in summary['per_iteration_data']:
- for case_name, results in iteration.items():
- for result in results:
- if result['status'] == 'FAILURE':
- failures.add(case_name)
- return list(failures)
- def main():
- parser = argparse.ArgumentParser()
- parser.add_argument(
- '--input-format',
- choices=['swarming_summary', 'test_launcher_summary', 'test_file'],
- default='test_file')
- parser.add_argument('--output-format',
- choices=['file', 'args'],
- default='args')
- parser.add_argument('--wildcard-compress', action='store_true')
- parser.add_argument(
- '--wildcard-min-depth',
- type=int,
- default=1,
- help="Minimum number of terms in a case before a wildcard may be " +
- "used, so that prefixes are not excessively broad.")
- parser.add_argument(
- '--wildcard-min-cases',
- type=int,
- default=3,
- help="Minimum number of cases in a filter before folding into a " +
- "wildcard, so as to not create wildcards needlessly for small "
- "numbers of similarly named test failures.")
- parser.add_argument('--line', type=int)
- parser.add_argument('--class-only', action='store_true')
- parser.add_argument(
- '--as-exclusions',
- action='store_true',
- help='Generate exclusion rules for test cases, instead of inclusions.')
- args, left = parser.parse_known_args()
- test_filters = []
- if args.input_format == 'swarming_summary':
- # Decode the JSON files separately and combine their contents.
- test_filters = []
- for json_file in left:
- test_filters.extend(json.loads('\n'.join(open(json_file, 'r'))))
- if args.wildcard_compress:
- test_filters = CompressWithWildcards(test_filters,
- args.wildcard_min_depth,
- args.wildcard_min_cases)
- elif args.input_format == 'test_launcher_summary':
- # Decode the JSON files separately and combine their contents.
- test_filters = []
- for json_file in left:
- test_filters.extend(
- GetFailedTestsFromTestLauncherSummary(
- json.loads('\n'.join(open(json_file, 'r')))))
- if args.wildcard_compress:
- test_filters = CompressWithWildcards(test_filters,
- args.wildcard_min_depth,
- args.wildcard_min_cases)
- else:
- file_input = fileinput.input(left)
- if args.line:
- # If --line is used, restrict text to a few lines around the requested
- # line.
- requested_line = args.line
- selected_lines = []
- for line in file_input:
- if (fileinput.lineno() >= requested_line
- and fileinput.lineno() <= requested_line + 1):
- selected_lines.append(line)
- txt = ''.join(selected_lines)
- else:
- txt = ''.join(list(file_input))
- # This regex is not exhaustive, and should be updated as needed.
- rx = re.compile(
- r'^(?:TYPED_)?(?:IN_PROC_BROWSER_)?TEST(_F|_P)?\(\s*(\w+)\s*' + \
- r',\s*(\w+)\s*\)',
- flags=re.DOTALL | re.M)
- tests = []
- for m in rx.finditer(txt):
- tests.append(m.group(2) + '.' + m.group(3))
- # Note: Test names have the following structures:
- # * FixtureName.TestName
- # * InstantiationName/FixtureName.TestName/##
- # Since this script doesn't parse instantiations, we generate filters to
- # match either regular tests or instantiated tests.
- if args.wildcard_compress:
- test_filters = CompressWithWildcards(tests, args.wildcard_min_depth,
- args.wildcard_min_cases)
- elif args.class_only:
- fixtures = set([t.split('.')[0] for t in tests])
- test_filters = [c + '.*' for c in fixtures] + \
- ['*/' + c + '.*/*' for c in fixtures]
- else:
- test_filters = ['*/' + c + '/*' for c in tests]
- if args.as_exclusions:
- test_filters = ['-' + x for x in test_filters]
- if args.output_format == 'file':
- print('\n'.join(test_filters))
- else:
- print(':'.join(test_filters))
- return 0
- if __name__ == '__main__':
- sys.exit(main())
|