@@ -100,19 +100,64 @@ def execute_reference_driver_tests(results: Results, ref_component: str, driver_
100100 if ret_val != 0 :
101101 results .error ("failed to run reference/driver components" )
102102
103- IgnoreEntry = typing .Union [str , typing .Pattern ]
104103
105- def name_matches_pattern (name : str , str_or_re : IgnoreEntry ) -> bool :
106- """Check if name matches a pattern, that may be a string or regex.
107- - If the pattern is a string, name must be equal to match.
108- - If the pattern is a regex, name must fully match.
109- """
110- # The CI's python is too old for re.Pattern
111- #if isinstance(str_or_re, re.Pattern):
112- if not isinstance (str_or_re , str ):
113- return str_or_re .fullmatch (name ) is not None
114- else :
115- return str_or_re == name
104+ TestCaseMatcher = typing .Union [str , typing .Pattern ]
105+
106+ # Map test suite names (with the test_suite prefix) to a list of ignored
107+ # test cases. Each element in the list can be either a string or a
108+ # compiled regex (Pattern). Strings only match themselves. Regexes must
109+ # match the full test case description (not just a prefix or other
110+ # substring).
111+ TestCaseSetDescription = typing .Mapping [str , typing .Sequence [TestCaseMatcher ]]
112+
113+ class TestCaseSet :
114+ """A set of test cases, indexed by their test suite."""
115+ #pylint: disable=too-few-public-methods
116+
117+ def __init__ (self , description : TestCaseSetDescription ) -> None :
118+ """Construct a set of test cases from a list of matches for each test suite.
119+ """
120+ # Construct new mutable objects, to avoid mutating the parameter,
121+ # which could be confusing.
122+ self .matchers = {key : list (entries )
123+ for key , entries in description .items ()}
124+
125+ def extend (self , description : TestCaseSetDescription ) -> None :
126+ """Add more matchers to this test case set."""
127+ for key , entries in description .items ():
128+ self .matchers .setdefault (key , [])
129+ self .matchers [key ] += entries
130+
131+ @staticmethod
132+ def _name_matches_pattern (name : str , str_or_re : TestCaseMatcher ) -> bool :
133+ """Check if name matches a pattern, that may be a string or regex.
134+ - If the pattern is a string, name must be equal to match.
135+ - If the pattern is a regex, name must fully match.
136+ """
137+ # The CI's python is too old for re.Pattern
138+ #if isinstance(str_or_re, re.Pattern):
139+ if not isinstance (str_or_re , str ):
140+ return str_or_re .fullmatch (name ) is not None
141+ else :
142+ return str_or_re == name
143+
144+ def _suite_matchers (self , test_suite : str ) -> typing .Iterator [TestCaseMatcher ]:
145+ """Generate the matcher list for the specified test suite."""
146+ if test_suite in self .matchers :
147+ yield from self .matchers [test_suite ]
148+ pos = test_suite .find ('.' )
149+ if pos != - 1 :
150+ base_test_suite = test_suite [:pos ]
151+ if base_test_suite in self .matchers :
152+ yield from self .matchers [base_test_suite ]
153+
154+ def contains (self , test_suite : str , test_string : str ) -> bool :
155+ """Check if the specified test case is in the set."""
156+ for str_or_re in self ._suite_matchers (test_suite ):
157+ if self ._name_matches_pattern (test_string , str_or_re ):
158+ return True
159+ return False
160+
116161
117162def open_outcome_file (outcome_file : str ) -> typing .TextIO :
118163 if outcome_file .endswith ('.gz' ):
@@ -146,11 +191,22 @@ def read_outcome_file(outcome_file: str) -> Outcomes:
146191class Task :
147192 """Base class for outcome analysis tasks."""
148193
149- # Override the following in child classes.
150- # Map test suite names (with the test_suite_prefix) to a list of ignored
151- # test cases. Each element in the list can be either a string or a regex;
152- # see the `name_matches_pattern` function.
153- IGNORED_TESTS = {} #type: typing.Dict[str, typing.List[IgnoreEntry]]
194+ @staticmethod
195+ def _has_word_re (words : typing .Iterable [str ],
196+ exclude : typing .Optional [str ] = None ) -> typing .Pattern :
197+ """Construct a regex that matches if any of the words appears.
198+
199+ The occurrence must start and end at a word boundary.
200+
201+ If exclude is specified, strings containing a match for that
202+ regular expression will not match the returned pattern.
203+ """
204+ exclude_clause = r''
205+ if exclude :
206+ exclude_clause = r'(?!.*' + exclude + ')'
207+ return re .compile (exclude_clause +
208+ r'.*\b(?:' + r'|' .join (words ) + r')\b.*' ,
209+ re .DOTALL )
154210
155211 def __init__ (self , options ) -> None :
156212 """Pass command line options to the tasks.
@@ -163,23 +219,6 @@ def section_name(self) -> str:
163219 """The section name to use in results."""
164220 raise NotImplementedError
165221
166- def ignored_tests (self , test_suite : str ) -> typing .Iterator [IgnoreEntry ]:
167- """Generate the ignore list for the specified test suite."""
168- if test_suite in self .IGNORED_TESTS :
169- yield from self .IGNORED_TESTS [test_suite ]
170- pos = test_suite .find ('.' )
171- if pos != - 1 :
172- base_test_suite = test_suite [:pos ]
173- if base_test_suite in self .IGNORED_TESTS :
174- yield from self .IGNORED_TESTS [base_test_suite ]
175-
176- def is_test_case_ignored (self , test_suite : str , test_string : str ) -> bool :
177- """Check if the specified test case is ignored."""
178- for str_or_re in self .ignored_tests (test_suite ):
179- if name_matches_pattern (test_string , str_or_re ):
180- return True
181- return False
182-
183222 def run (self , results : Results , outcomes : Outcomes ):
184223 """Run the analysis on the specified outcomes.
185224
@@ -192,17 +231,30 @@ class CoverageTask(Task):
192231 """Analyze test coverage."""
193232
194233 # Test cases whose suite and description are matched by an entry in
195- # IGNORED_TESTS are expected to be never executed.
234+ # UNCOVERED_TESTS are expected to be never executed.
235+ # Tests matched by IGNORED_TESTS are ignored entirely.
196236 # All other test cases are expected to be executed at least once.
197237
238+ UNCOVERED_TESTS : TestCaseSetDescription = {}
239+ IGNORED_TESTS : TestCaseSetDescription = {}
240+
198241 def __init__ (self , options ) -> None :
199242 super ().__init__ (options )
200243 self .full_coverage = options .full_coverage #type: bool
244+ self .uncovered_tests = TestCaseSet (self .UNCOVERED_TESTS )
245+ self .ignored_tests = TestCaseSet (self .IGNORED_TESTS )
201246
202247 @staticmethod
203248 def section_name () -> str :
204249 return "Analyze coverage"
205250
251+ def note_ignored_test (self , results : Results ,
252+ test_suite : str , test_description : str ) -> None :
253+ # pylint: disable=no-self-use # derived classes may need self
254+ """This method runs for each test case that's available and ignored."""
255+ results .info ('Test case was ignored: {};{}' ,
256+ test_suite , test_description )
257+
206258 def run (self , results : Results , outcomes : Outcomes ) -> None :
207259 """Check that all available test cases are executed at least once."""
208260 # Make sure that the generated data files are present (and up-to-date).
@@ -222,22 +274,28 @@ def run(self, results: Results, outcomes: Outcomes) -> None:
222274 suite_case in comp_outcomes .failures
223275 for comp_outcomes in outcomes .values ())
224276 (test_suite , test_description ) = suite_case .split (';' )
225- ignored = self .is_test_case_ignored (test_suite , test_description )
277+ ignored = self .ignored_tests .contains (test_suite , test_description )
278+ if ignored :
279+ self .note_ignored_test (results , test_suite , test_description )
280+ continue
226281
227- if not hit and not ignored :
282+ uncovered = self .uncovered_tests .contains (test_suite , test_description )
283+ if not hit and not uncovered :
228284 if self .full_coverage :
229285 results .error ('Test case not executed: {}' , suite_case )
230286 else :
231287 results .warning ('Test case not executed: {}' , suite_case )
232- elif hit and ignored :
288+ elif hit and uncovered :
233289 # If a test case is no longer always skipped, we should remove
234290 # it from the ignore list.
235291 if self .full_coverage :
236- results .error ('Test case was executed but marked as ignored for coverage: {}' ,
237- suite_case )
292+ results .error (
293+ 'Test case was executed but marked as uncovered for coverage: {}' ,
294+ suite_case )
238295 else :
239- results .warning ('Test case was executed but marked as ignored for coverage: {}' ,
240- suite_case )
296+ results .warning (
297+ 'Test case was executed but marked as uncovered for coverage: {}' ,
298+ suite_case )
241299
242300
243301class DriverVSReference (Task ):
@@ -257,12 +315,18 @@ class DriverVSReference(Task):
257315 # Configuration name (all.sh component) used as the driver.
258316 DRIVER = ''
259317 # Ignored test suites (without the test_suite_ prefix).
260- IGNORED_SUITES = [] #type: typing.List[str]
318+ IGNORED_SUITES = [] #type: typing.Sequence[str]
319+ # Ignored test cases. Despite the name, these test cases are not
320+ # completely ignored: they must be skipped by driver tests. If they
321+ # are not skipped, this indicates a spurious entry and the analysis will
322+ # complain.
323+ IGNORED_TESTS : TestCaseSetDescription = {}
261324
262325 def __init__ (self , options ) -> None :
263326 super ().__init__ (options )
264327 self .ignored_suites = frozenset ('test_suite_' + x
265328 for x in self .IGNORED_SUITES )
329+ self .ignored_tests = TestCaseSet (self .IGNORED_TESTS )
266330
267331 def section_name (self ) -> str :
268332 return f"Analyze driver { self .DRIVER } vs reference { self .REFERENCE } "
@@ -299,7 +363,7 @@ def run(self, results: Results, outcomes: Outcomes) -> None:
299363 # For ignored test cases inside test suites, just remember and:
300364 # don't issue an error if they're skipped with drivers,
301365 # but issue an error if they're not (means we have a bad entry).
302- ignored = self .is_test_case_ignored (full_test_suite , test_string )
366+ ignored = self .ignored_tests . contains (full_test_suite , test_string )
303367
304368 if not ignored and not suite_case in driver_outcomes .successes :
305369 results .error ("SKIP/FAIL -> PASS: {}" , suite_case )
0 commit comments