Merge "Atest: stop running if there are mixed type filters" am: b08c7d0f09

Original change: https://android-review.googlesource.com/c/platform/tools/asuite/+/2031523

Change-Id: I5d756c8aa695cffe52e9fc693d9e6c19f6800df3
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/atest/atest_enum.py b/atest/atest_enum.py
index 48a8fbf..f4405b3 100644
--- a/atest/atest_enum.py
+++ b/atest/atest_enum.py
@@ -16,7 +16,7 @@
 Atest custom enum class.
 """
 
-from enum import IntEnum, unique
+from enum import IntEnum, unique, Enum
 
 @unique
 class DetectType(IntEnum):
@@ -63,6 +63,13 @@
     AVD_INVALID_ARGS = 9
     EXIT_BEFORE_MAIN = 10
     DEVICE_NOT_FOUND = 11
+    MIXED_TYPE_FILTER = 12
+
+@unique
+class FilterType(Enum):
+    """An Enum class for filter types"""
+    WILDCARD_FILTER = 'wildcard class_method'
+    REGULAR_FILTER = 'regular class_method'
 
 # TODO: (b/218441706) Convert AtestEnum to a real Enum class.
 class AtestEnum(tuple):
diff --git a/atest/atest_utils.py b/atest/atest_utils.py
index a835add..0059ca0 100644
--- a/atest/atest_utils.py
+++ b/atest/atest_utils.py
@@ -44,7 +44,7 @@
 
 import xml.etree.ElementTree as ET
 
-from atest_enum import DetectType
+from atest_enum import DetectType, FilterType
 
 # This is a workaround of b/144743252, where the http.client failed to loaded
 # because the googleapiclient was found before the built-in libs; enabling
@@ -143,6 +143,9 @@
 
 _ROOT_PREPARER = "com.android.tradefed.targetprep.RootTargetPreparer"
 
+_WILDCARD_FILTER_RE = re.compile(r'.*[?|*]$')
+_REGULAR_FILTER_RE = re.compile(r'.*\w$')
+
 def get_build_cmd(dump=False):
     """Compose build command with no-absolute path and flag "--make-mode".
 
@@ -1718,3 +1721,54 @@
                 if match:
                     return match.group('fqcn')
     return ""
+
+def has_mixed_type_filters(test_infos):
+    """ There are different types in a test module.
+
+    Dict test_to_types is mapping module name and the set of types.
+    For example,
+    {
+        'module_1': {'wildcard class_method'},
+        'module_2': {'wildcard class_method', 'regular class_method'},
+        'module_3': set()
+        }
+
+    Args:
+        test_infos: A set of TestInfos.
+
+    Returns:
+        True if more than one filter type in a test module, False otherwise.
+    """
+    test_to_types = dict()
+    for test_info in test_infos:
+        filters = test_info.data.get(constants.TI_FILTER, [])
+        filter_types = set()
+        for flt in filters:
+            filter_types |= get_filter_types(flt.to_set_of_tf_strings())
+        filter_types |= test_to_types.get(test_info.test_name, set())
+        test_to_types[test_info.test_name] = filter_types
+    for _, types in test_to_types.items():
+        if len(types) > 1:
+            return True
+    return False
+
+def get_filter_types(tf_filter_set):
+    """ Get filter types.
+
+    Args:
+        tf_filter_set: A set of tf filter strings.
+
+    Returns:
+        A set of FilterType.
+    """
+    type_set = set()
+    for tf_filter in tf_filter_set:
+        if _WILDCARD_FILTER_RE.match(tf_filter):
+            logging.debug('Filter and type: (%s, %s)',
+                          tf_filter, FilterType.WILDCARD_FILTER.value)
+            type_set.add(FilterType.WILDCARD_FILTER.value)
+        if _REGULAR_FILTER_RE.match(tf_filter):
+            logging.debug('Filter and type: (%s, %s)',
+                         tf_filter, FilterType.REGULAR_FILTER.value)
+            type_set.add(FilterType.REGULAR_FILTER.value)
+    return type_set
diff --git a/atest/atest_utils_unittest.py b/atest/atest_utils_unittest.py
index c63443c..c0a5de0 100755
--- a/atest/atest_utils_unittest.py
+++ b/atest/atest_utils_unittest.py
@@ -16,6 +16,7 @@
 
 """Unittests for atest_utils."""
 
+# pylint: disable=invalid-name
 # pylint: disable=line-too-long
 
 import hashlib
@@ -35,6 +36,7 @@
 import unittest_constants
 
 from test_finders import test_info
+from atest_enum import FilterType
 
 
 TEST_MODULE_NAME_A = 'ModuleNameA'
@@ -690,5 +692,62 @@
         self.assertNotEqual(
             atest_utils.get_full_annotation_class_name(module_info, 'android.platform.test.annotations.pres'), presubmit)
 
+    def test_has_mixed_type_filters_one_module_with_one_type_return_false(self):
+        """Test method of has_mixed_type_filters"""
+        filter_1 = test_info.TestFilter('CLASS', frozenset(['METHOD']))
+        test_data_1 = {constants.TI_FILTER: [filter_1]}
+        test_info_1 = test_info.TestInfo('MODULE', 'RUNNER',
+                                set(), test_data_1,
+                                'SUITE', '',
+                                set())
+        self.assertFalse(atest_utils.has_mixed_type_filters([test_info_1]))
+
+    def test_has_mixed_type_filters_one_module_with_mixed_types_return_true(self):
+        """Test method of has_mixed_type_filters"""
+        filter_1 = test_info.TestFilter('CLASS', frozenset(['METHOD']))
+        filter_2 = test_info.TestFilter('CLASS', frozenset(['METHOD*']))
+        test_data_2 = {constants.TI_FILTER: [filter_1, filter_2]}
+        test_info_2 = test_info.TestInfo('MODULE', 'RUNNER',
+                                set(), test_data_2,
+                                'SUITE', '',
+                                set())
+        self.assertTrue(atest_utils.has_mixed_type_filters([test_info_2]))
+
+    def test_has_mixed_type_filters_two_module_with_mixed_types_return_false(self):
+        """Test method of has_mixed_type_filters"""
+        filter_1 = test_info.TestFilter('CLASS', frozenset(['METHOD']))
+        test_data_1 = {constants.TI_FILTER: [filter_1]}
+        test_info_1 = test_info.TestInfo('MODULE', 'RUNNER',
+                                set(), test_data_1,
+                                'SUITE', '',
+                                set())
+        filter_3 = test_info.TestFilter('CLASS', frozenset(['METHOD*']))
+        test_data_3 = {constants.TI_FILTER: [filter_3]}
+        test_info_3 = test_info.TestInfo('MODULE3', 'RUNNER',
+                                set(), test_data_3,
+                                'SUITE', '',
+                                set())
+        self.assertFalse(atest_utils.has_mixed_type_filters(
+            [test_info_1, test_info_3]))
+
+    def test_get_filter_types(self):
+        """Test method of get_filter_types."""
+        filters = set(['CLASS#METHOD'])
+        expect_types = set([FilterType.REGULAR_FILTER.value])
+        self.assertEqual(atest_utils.get_filter_types(filters), expect_types)
+
+        filters = set(['CLASS#METHOD*'])
+        expect_types = set([FilterType.WILDCARD_FILTER.value])
+        self.assertEqual(atest_utils.get_filter_types(filters), expect_types)
+
+        filters = set(['CLASS#METHOD', 'CLASS#METHOD*'])
+        expect_types = set([FilterType.WILDCARD_FILTER.value,
+                          FilterType.REGULAR_FILTER.value])
+        self.assertEqual(atest_utils.get_filter_types(filters), expect_types)
+
+        filters = set(['CLASS#METHOD?', 'CLASS#METHOD*'])
+        expect_types = set([FilterType.WILDCARD_FILTER.value])
+        self.assertEqual(atest_utils.get_filter_types(filters), expect_types)
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/atest/cli_translator.py b/atest/cli_translator.py
index 55cc410..a10f9b5 100644
--- a/atest/cli_translator.py
+++ b/atest/cli_translator.py
@@ -642,6 +642,12 @@
             host_unit_test_infos = self._get_test_infos(host_unit_tests,
                                                         host_unit_test_details)
             test_infos.update(host_unit_test_infos)
+        if atest_utils.has_mixed_type_filters(test_infos):
+            atest_utils.colorful_print(
+                'Mixed type filters found. '
+                'Please separate tests into different runs.',
+                constants.YELLOW)
+            sys.exit(ExitCode.MIXED_TYPE_FILTER)
         finished_time = time.time() - start
         logging.debug('Finding tests finished in %ss', finished_time)
         metrics.LocalDetectEvent(