0

Improve android test filtering

If you look at these two patchsets the only difference is this change
https://crrev.com/c/3089797/16
https://crrev.com/c/3089797/17
However 17 fails without it due to a timeout

Bug: 1241651
Change-Id: I95b27b8fe4831e170ee7c9f5ae22435b2425c885
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3111092
Commit-Queue: Struan Shrimpton <sshrimp@google.com>
Reviewed-by: Gregory Guterman <guterman@google.com>
Reviewed-by: Dirk Pranke <dpranke@google.com>
Cr-Commit-Position: refs/heads/main@{#914856}
This commit is contained in:
Struan Shrimpton
2021-08-24 19:15:16 +00:00
committed by Chromium LUCI CQ
parent 3e236a2fed
commit 19d16a4bc9
2 changed files with 200 additions and 32 deletions

@ -242,39 +242,147 @@ def FilterTests(tests, filter_str=None, annotations=None,
{'annotations": {}, 'class': 'com.example.TestB', 'method':'test2'}]
filter_str: googletest-style filter string.
annotations: a dict of wanted annotations for test methods.
exclude_annotations: a dict of annotations to exclude.
excluded_annotations: a dict of annotations to exclude.
Return:
A list of filtered tests
"""
def gtest_filter(t):
if not filter_str:
return True
def test_names_from_pattern(combined_pattern, test_names):
patterns = combined_pattern.split(':')
hashable_patterns = set()
filename_patterns = []
for pattern in patterns:
if ('*' in pattern or '?' in pattern or '[' in pattern):
filename_patterns.append(pattern)
else:
hashable_patterns.add(pattern)
filter_test_names = set(
unittest_util.FilterTestNames(test_names, ':'.join(
filename_patterns))) if len(filename_patterns) > 0 else set()
for test_name in test_names:
if test_name in hashable_patterns:
filter_test_names.add(test_name)
return filter_test_names
def get_test_names(test):
test_names = set()
# Allow fully-qualified name as well as an omitted package.
unqualified_class_test = {
'class': t['class'].split('.')[-1],
'method': t['method']
'class': test['class'].split('.')[-1],
'method': test['method']
}
names = [
GetTestName(t, sep='.'),
GetTestName(unqualified_class_test, sep='.'),
GetUniqueTestName(t, sep='.')
]
if t['is_junit4']:
names += [
GetTestNameWithoutParameterPostfix(t, sep='.'),
GetTestNameWithoutParameterPostfix(unqualified_class_test, sep='.')
]
test_name = GetTestName(test, sep='.')
test_names.add(test_name)
unqualified_class_test_name = GetTestName(unqualified_class_test, sep='.')
test_names.add(unqualified_class_test_name)
unique_test_name = GetUniqueTestName(test, sep='.')
test_names.add(unique_test_name)
if test['is_junit4']:
junit4_test_name = GetTestNameWithoutParameterPostfix(test, sep='.')
test_names.add(junit4_test_name)
unqualified_junit4_test_name = \
GetTestNameWithoutParameterPostfix(unqualified_class_test, sep='.')
test_names.add(unqualified_junit4_test_name)
return test_names
def get_tests_from_names(tests, test_names, tests_to_names):
''' Returns the tests for which the given names apply
Args:
tests: a list of tests. e.g. [
{'annotations": {}, 'class': 'com.example.TestA', 'method':'test1'},
{'annotations": {}, 'class': 'com.example.TestB', 'method':'test2'}]
test_names: a collection of names determining tests to return.
Return:
A list of tests that match the given test names
'''
filtered_tests = []
for t in tests:
current_test_names = tests_to_names[id(t)]
for current_test_name in current_test_names:
if current_test_name in test_names:
filtered_tests.append(t)
break
return filtered_tests
def remove_tests_from_names(tests, remove_test_names, tests_to_names):
''' Returns the tests from the given list with given names removed
Args:
tests: a list of tests. e.g. [
{'annotations": {}, 'class': 'com.example.TestA', 'method':'test1'},
{'annotations": {}, 'class': 'com.example.TestB', 'method':'test2'}]
remove_test_names: a collection of names determining tests to remove.
tests_to_names: a dcitionary of test ids to a collection of applicable
names for that test
Return:
A list of tests that don't match the given test names
'''
filtered_tests = []
for t in tests:
for name in tests_to_names[id(t)]:
if name in remove_test_names:
break
else:
filtered_tests.append(t)
return filtered_tests
def gtests_filter(tests, combined_filter):
''' Returns the tests after the filter_str has been applied
Args:
tests: a list of tests. e.g. [
{'annotations": {}, 'class': 'com.example.TestA', 'method':'test1'},
{'annotations": {}, 'class': 'com.example.TestB', 'method':'test2'}]
combined_filter: the filter string representing tests to exclude
Return:
A list of tests that should still be included after the filter_str is
applied to their names
'''
if not combined_filter:
return tests
# Collect all test names
all_test_names = set()
tests_to_names = {}
for t in tests:
tests_to_names[id(t)] = get_test_names(t)
for name in tests_to_names[id(t)]:
all_test_names.add(name)
pattern_groups = filter_str.split('-')
if len(pattern_groups) > 1:
negative_filter = pattern_groups[1]
if unittest_util.FilterTestNames(names, negative_filter):
return []
negative_pattern = pattern_groups[1] if len(pattern_groups) > 1 else None
positive_pattern = pattern_groups[0]
positive_filter = pattern_groups[0]
return unittest_util.FilterTestNames(names, positive_filter)
if positive_pattern:
# Only use the test names that match the positive pattern
positive_test_names = test_names_from_pattern(positive_pattern,
all_test_names)
tests = get_tests_from_names(tests, positive_test_names, tests_to_names)
if negative_pattern:
# Remove any test the negative filter matches
remove_names = test_names_from_pattern(negative_pattern, all_test_names)
tests = remove_tests_from_names(tests, remove_names, tests_to_names)
return tests
def annotation_filter(all_annotations):
if not annotations:
@ -308,12 +416,8 @@ def FilterTests(tests, filter_str=None, annotations=None,
return filter_av in av
return filter_av == av
filtered_tests = []
for t in tests:
# Gtest filtering
if not gtest_filter(t):
continue
return_tests = []
for t in gtests_filter(tests, filter_str):
# Enforce that all tests declare their size.
if not any(a in _VALID_ANNOTATIONS for a in t['annotations']):
raise MissingSizeAnnotationError(GetTestName(t))
@ -321,11 +425,9 @@ def FilterTests(tests, filter_str=None, annotations=None,
if (not annotation_filter(t['annotations'])
or not excluded_annotation_filter(t['annotations'])):
continue
return_tests.append(t)
filtered_tests.append(t)
return filtered_tests
return return_tests
# TODO(yolandyan): remove this once the tests are converted to junit4
def GetAllTestsFromJar(test_jar):

@ -198,6 +198,72 @@ class InstrumentationTestInstanceTest(unittest.TestCase):
self.assertEqual(actual_tests, expected_tests)
def testGetTests_simpleGtestPositiveAndNegativeFilter(self):
o = self.createTestInstance()
raw_tests = [{
'annotations': {
'Feature': {
'value': ['Foo']
}
},
'class':
'org.chromium.test.SampleTest',
'superclass':
'java.lang.Object',
'methods': [
{
'annotations': {
'SmallTest': None
},
'method': 'testMethod1',
},
{
'annotations': {
'MediumTest': None
},
'method': 'testMethod2',
},
],
}, {
'annotations': {
'Feature': {
'value': ['Foo']
}
},
'class':
'org.chromium.test.SampleTest2',
'superclass':
'java.lang.Object',
'methods': [{
'annotations': {
'SmallTest': None
},
'method': 'testMethod1',
}],
}]
expected_tests = [
{
'annotations': {
'Feature': {
'value': ['Foo']
},
'SmallTest': None,
},
'class': 'org.chromium.test.SampleTest',
'is_junit4': True,
'method': 'testMethod1',
},
]
o._test_filter = \
'org.chromium.test.SampleTest.*-org.chromium.test.SampleTest.testMethod2'
o._test_jar = 'path/to/test.jar'
o._junit4_runner_class = 'J4Runner'
actual_tests = o.ProcessRawTests(raw_tests)
self.assertEqual(actual_tests, expected_tests)
def testGetTests_simpleGtestUnqualifiedNameFilter(self):
o = self.createTestInstance()
raw_tests = [