make_gtest_filter.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. #!/usr/bin/env python
  2. # Copyright (c) 2018 The Chromium Authors. All rights reserved.
  3. # 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. Outputs a filter that can be used with --gtest_filter or a filter file to
  7. run only the tests identified.
  8. Usage:
  9. Outputs filter for all test fixtures in a directory. --class-only avoids an
  10. overly long filter string.
  11. $ cat components/mycomp/**test.cc | make_gtest_filter.py --class-only
  12. Outputs filter for all tests in a file.
  13. $ make_gtest_filter.py ./myfile_unittest.cc
  14. Outputs filter for only test at line 123
  15. $ make_gtest_filter.py --line=123 ./myfile_unittest.cc
  16. Formats output as a GTest filter file.
  17. $ make_gtest_filter.py ./myfile_unittest.cc --as-filter-file
  18. Use a JSON failure summary as the input.
  19. $ make_gtest_filter.py summary.json --from-failure-summary
  20. Elide the filter list using wildcards when possible.
  21. $ make_gtest_filter.py summary.json --from-failure-summary --wildcard-compress
  22. """
  23. from __future__ import print_function
  24. import argparse
  25. import collections
  26. import fileinput
  27. import json
  28. import re
  29. import sys
  30. class TrieNode:
  31. def __init__(self):
  32. # The number of strings which terminated on or underneath this node.
  33. self.num_strings = 0
  34. # The prefix subtries which follow |this|, keyed by their next character.
  35. self.children = {}
  36. def PascalCaseSplit(input_string):
  37. current_term = []
  38. prev_char = ''
  39. for current_char in input_string:
  40. is_boundary = prev_char != '' and \
  41. ((current_char.isupper() and prev_char.islower()) or \
  42. (current_char.isalpha() != prev_char.isalpha()) or \
  43. (current_char.isalnum() != prev_char.isalnum()))
  44. prev_char = current_char
  45. if is_boundary:
  46. yield ''.join(current_term)
  47. current_term = []
  48. current_term.append(current_char)
  49. if len(current_term) > 0:
  50. yield ''.join(current_term)
  51. def TrieInsert(trie, value):
  52. """Inserts the characters of 'value' into a trie, with every edge representing
  53. a single character. An empty child set indicates end-of-string."""
  54. for term in PascalCaseSplit(value):
  55. trie.num_strings = trie.num_strings + 1
  56. if term in trie.children:
  57. trie = trie.children[term]
  58. else:
  59. subtrie = TrieNode()
  60. trie.children[term] = subtrie
  61. trie = subtrie
  62. trie.num_strings = trie.num_strings + 1
  63. def ComputeWildcardsFromTrie(trie, min_depth, min_cases):
  64. """Computes a list of wildcarded test case names from a trie using a depth
  65. first traversal."""
  66. WILDCARD = '*'
  67. # Stack of values to process, initialized with the root node.
  68. # The first item of the tuple is the substring represented by the traversal so
  69. # far.
  70. # The second item of the tuple is the TrieNode itself.
  71. # The third item is the depth of the traversal so far.
  72. to_process = [('', trie, 0)]
  73. while len(to_process) > 0:
  74. cur_prefix, cur_trie, cur_depth = to_process.pop()
  75. assert (cur_trie.num_strings != 0)
  76. if len(cur_trie.children) == 0:
  77. # No more children == we're at the end of a string.
  78. yield cur_prefix
  79. elif (cur_depth == min_depth) and \
  80. cur_trie.num_strings > min_cases:
  81. # Trim traversal of this path if the path is deep enough and there
  82. # are enough entries to warrant elision.
  83. yield cur_prefix + WILDCARD
  84. else:
  85. # Traverse all children of this node.
  86. for term, subtrie in cur_trie.children.items():
  87. to_process.append((cur_prefix + term, subtrie, cur_depth + 1))
  88. def CompressWithWildcards(test_list, min_depth, min_cases):
  89. """Given a list of SUITE.CASE names, generates an exclusion list using
  90. wildcards to reduce redundancy.
  91. For example:
  92. Foo.TestOne
  93. Foo.TestTwo
  94. becomes:
  95. Foo.Test*"""
  96. suite_tries = {}
  97. # First build up a trie based representations of all test case names,
  98. # partitioned per-suite.
  99. for case in test_list:
  100. suite_name, test = case.split('.')
  101. if not suite_name in suite_tries:
  102. suite_tries[suite_name] = TrieNode()
  103. TrieInsert(suite_tries[suite_name], test)
  104. output = []
  105. # Go through the suites' tries and generate wildcarded representations
  106. # of the cases.
  107. for suite in suite_tries.items():
  108. suite_name, cases_trie = suite
  109. for case_wildcard in ComputeWildcardsFromTrie(cases_trie, min_depth, \
  110. min_cases):
  111. output.append("{}.{}".format(suite_name, case_wildcard))
  112. output.sort()
  113. return output
  114. def GetFailedTestsFromTestLauncherSummary(summary):
  115. failures = set()
  116. for iteration in summary['per_iteration_data']:
  117. for case_name, results in iteration.items():
  118. for result in results:
  119. if result['status'] == 'FAILURE':
  120. failures.add(case_name)
  121. return list(failures)
  122. def main():
  123. parser = argparse.ArgumentParser()
  124. parser.add_argument(
  125. '--input-format',
  126. choices=['swarming_summary', 'test_launcher_summary', 'test_file'],
  127. default='test_file')
  128. parser.add_argument('--output-format',
  129. choices=['file', 'args'],
  130. default='args')
  131. parser.add_argument('--wildcard-compress', action='store_true')
  132. parser.add_argument(
  133. '--wildcard-min-depth',
  134. type=int,
  135. default=1,
  136. help="Minimum number of terms in a case before a wildcard may be " +
  137. "used, so that prefixes are not excessively broad.")
  138. parser.add_argument(
  139. '--wildcard-min-cases',
  140. type=int,
  141. default=3,
  142. help="Minimum number of cases in a filter before folding into a " +
  143. "wildcard, so as to not create wildcards needlessly for small "
  144. "numbers of similarly named test failures.")
  145. parser.add_argument('--line', type=int)
  146. parser.add_argument('--class-only', action='store_true')
  147. parser.add_argument(
  148. '--as-exclusions',
  149. action='store_true',
  150. help='Generate exclusion rules for test cases, instead of inclusions.')
  151. args, left = parser.parse_known_args()
  152. test_filters = []
  153. if args.input_format == 'swarming_summary':
  154. # Decode the JSON files separately and combine their contents.
  155. test_filters = []
  156. for json_file in left:
  157. test_filters.extend(json.loads('\n'.join(open(json_file, 'r'))))
  158. if args.wildcard_compress:
  159. test_filters = CompressWithWildcards(test_filters,
  160. args.wildcard_min_depth,
  161. args.wildcard_min_cases)
  162. elif args.input_format == 'test_launcher_summary':
  163. # Decode the JSON files separately and combine their contents.
  164. test_filters = []
  165. for json_file in left:
  166. test_filters.extend(
  167. GetFailedTestsFromTestLauncherSummary(
  168. json.loads('\n'.join(open(json_file, 'r')))))
  169. if args.wildcard_compress:
  170. test_filters = CompressWithWildcards(test_filters,
  171. args.wildcard_min_depth,
  172. args.wildcard_min_cases)
  173. else:
  174. file_input = fileinput.input(left)
  175. if args.line:
  176. # If --line is used, restrict text to a few lines around the requested
  177. # line.
  178. requested_line = args.line
  179. selected_lines = []
  180. for line in file_input:
  181. if (fileinput.lineno() >= requested_line
  182. and fileinput.lineno() <= requested_line + 1):
  183. selected_lines.append(line)
  184. txt = ''.join(selected_lines)
  185. else:
  186. txt = ''.join(list(file_input))
  187. # This regex is not exhaustive, and should be updated as needed.
  188. rx = re.compile(
  189. r'^(?:TYPED_)?(?:IN_PROC_BROWSER_)?TEST(_F|_P)?\(\s*(\w+)\s*' + \
  190. r',\s*(\w+)\s*\)',
  191. flags=re.DOTALL | re.M)
  192. tests = []
  193. for m in rx.finditer(txt):
  194. tests.append(m.group(2) + '.' + m.group(3))
  195. # Note: Test names have the following structures:
  196. # * FixtureName.TestName
  197. # * InstantiationName/FixtureName.TestName/##
  198. # Since this script doesn't parse instantiations, we generate filters to
  199. # match either regular tests or instantiated tests.
  200. if args.wildcard_compress:
  201. test_filters = CompressWithWildcards(tests, args.wildcard_min_depth,
  202. args.wildcard_min_cases)
  203. elif args.class_only:
  204. fixtures = set([t.split('.')[0] for t in tests])
  205. test_filters = [c + '.*' for c in fixtures] + \
  206. ['*/' + c + '.*/*' for c in fixtures]
  207. else:
  208. test_filters = ['*/' + c + '/*' for c in tests]
  209. if args.as_exclusions:
  210. test_filters = ['-' + x for x in test_filters]
  211. if args.output_format == 'file':
  212. print('\n'.join(test_filters))
  213. else:
  214. print(':'.join(test_filters))
  215. return 0
  216. if __name__ == '__main__':
  217. sys.exit(main())