xref: /aosp_15_r20/build/make/ci/test_mapping_module_retriever.py (revision 9e94795a3d4ef5c1d47486f9a02bb378756cea8a)
1*9e94795aSAndroid Build Coastguard Worker# Copyright 2024, The Android Open Source Project
2*9e94795aSAndroid Build Coastguard Worker#
3*9e94795aSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
4*9e94795aSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
5*9e94795aSAndroid Build Coastguard Worker# You may obtain a copy of the License at
6*9e94795aSAndroid Build Coastguard Worker#
7*9e94795aSAndroid Build Coastguard Worker#     http://www.apache.org/licenses/LICENSE-2.0
8*9e94795aSAndroid Build Coastguard Worker#
9*9e94795aSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
10*9e94795aSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
11*9e94795aSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*9e94795aSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
13*9e94795aSAndroid Build Coastguard Worker# limitations under the License.
14*9e94795aSAndroid Build Coastguard Worker
15*9e94795aSAndroid Build Coastguard Worker"""
16*9e94795aSAndroid Build Coastguard WorkerSimple parsing code to scan test_mapping files and determine which
17*9e94795aSAndroid Build Coastguard Workermodules are needed to build for the given list of changed files.
18*9e94795aSAndroid Build Coastguard WorkerTODO(lucafarsi): Deduplicate from artifact_helper.py
19*9e94795aSAndroid Build Coastguard Worker"""
20*9e94795aSAndroid Build Coastguard Worker# TODO(lucafarsi): Share this logic with the original logic in
21*9e94795aSAndroid Build Coastguard Worker# test_mapping_test_retriever.py
22*9e94795aSAndroid Build Coastguard Worker
23*9e94795aSAndroid Build Coastguard Workerimport json
24*9e94795aSAndroid Build Coastguard Workerimport os
25*9e94795aSAndroid Build Coastguard Workerimport re
26*9e94795aSAndroid Build Coastguard Workerfrom typing import Any
27*9e94795aSAndroid Build Coastguard Worker
28*9e94795aSAndroid Build Coastguard Worker# Regex to extra test name from the path of test config file.
29*9e94795aSAndroid Build Coastguard WorkerTEST_NAME_REGEX = r'(?:^|.*/)([^/]+)\.config'
30*9e94795aSAndroid Build Coastguard Worker
31*9e94795aSAndroid Build Coastguard Worker# Key name for TEST_MAPPING imports
32*9e94795aSAndroid Build Coastguard WorkerKEY_IMPORTS = 'imports'
33*9e94795aSAndroid Build Coastguard WorkerKEY_IMPORT_PATH = 'path'
34*9e94795aSAndroid Build Coastguard Worker
35*9e94795aSAndroid Build Coastguard Worker# Name of TEST_MAPPING file.
36*9e94795aSAndroid Build Coastguard WorkerTEST_MAPPING = 'TEST_MAPPING'
37*9e94795aSAndroid Build Coastguard Worker
38*9e94795aSAndroid Build Coastguard Worker# Pattern used to identify double-quoted strings and '//'-format comments in
39*9e94795aSAndroid Build Coastguard Worker# TEST_MAPPING file, but only double-quoted strings are included within the
40*9e94795aSAndroid Build Coastguard Worker# matching group.
41*9e94795aSAndroid Build Coastguard Worker_COMMENTS_RE = re.compile(r'(\"(?:[^\"\\]|\\.)*\"|(?=//))(?://.*)?')
42*9e94795aSAndroid Build Coastguard Worker
43*9e94795aSAndroid Build Coastguard Worker
44*9e94795aSAndroid Build Coastguard Workerdef FilterComments(test_mapping_file: str) -> str:
45*9e94795aSAndroid Build Coastguard Worker  """Remove comments in TEST_MAPPING file to valid format.
46*9e94795aSAndroid Build Coastguard Worker
47*9e94795aSAndroid Build Coastguard Worker  Only '//' is regarded as comments.
48*9e94795aSAndroid Build Coastguard Worker
49*9e94795aSAndroid Build Coastguard Worker  Args:
50*9e94795aSAndroid Build Coastguard Worker    test_mapping_file: Path to a TEST_MAPPING file.
51*9e94795aSAndroid Build Coastguard Worker
52*9e94795aSAndroid Build Coastguard Worker  Returns:
53*9e94795aSAndroid Build Coastguard Worker    Valid json string without comments.
54*9e94795aSAndroid Build Coastguard Worker  """
55*9e94795aSAndroid Build Coastguard Worker  return re.sub(_COMMENTS_RE, r'\1', test_mapping_file)
56*9e94795aSAndroid Build Coastguard Worker
57*9e94795aSAndroid Build Coastguard Workerdef GetTestMappings(paths: set[str],
58*9e94795aSAndroid Build Coastguard Worker                    checked_paths: set[str]) -> dict[str, dict[str, Any]]:
59*9e94795aSAndroid Build Coastguard Worker  """Get the affected TEST_MAPPING files.
60*9e94795aSAndroid Build Coastguard Worker
61*9e94795aSAndroid Build Coastguard Worker  TEST_MAPPING files in source code are packaged into a build artifact
62*9e94795aSAndroid Build Coastguard Worker  `test_mappings.zip`. Inside the zip file, the path of each TEST_MAPPING file
63*9e94795aSAndroid Build Coastguard Worker  is preserved. From all TEST_MAPPING files in the source code, this method
64*9e94795aSAndroid Build Coastguard Worker  locates the affected TEST_MAPPING files based on the given paths list.
65*9e94795aSAndroid Build Coastguard Worker
66*9e94795aSAndroid Build Coastguard Worker  A TEST_MAPPING file may also contain `imports` that import TEST_MAPPING files
67*9e94795aSAndroid Build Coastguard Worker  from a different location, e.g.,
68*9e94795aSAndroid Build Coastguard Worker    "imports": [
69*9e94795aSAndroid Build Coastguard Worker      {
70*9e94795aSAndroid Build Coastguard Worker        "path": "../folder2"
71*9e94795aSAndroid Build Coastguard Worker      }
72*9e94795aSAndroid Build Coastguard Worker    ]
73*9e94795aSAndroid Build Coastguard Worker  In that example, TEST_MAPPING files inside ../folder2 (relative to the
74*9e94795aSAndroid Build Coastguard Worker  TEST_MAPPING file containing that imports section) and its parent directories
75*9e94795aSAndroid Build Coastguard Worker  will also be included.
76*9e94795aSAndroid Build Coastguard Worker
77*9e94795aSAndroid Build Coastguard Worker  Args:
78*9e94795aSAndroid Build Coastguard Worker    paths: A set of paths with related TEST_MAPPING files for given changes.
79*9e94795aSAndroid Build Coastguard Worker    checked_paths: A set of paths that have been checked for TEST_MAPPING file
80*9e94795aSAndroid Build Coastguard Worker      already. The set is updated after processing each TEST_MAPPING file. It's
81*9e94795aSAndroid Build Coastguard Worker      used to prevent infinite loop when the method is called recursively.
82*9e94795aSAndroid Build Coastguard Worker
83*9e94795aSAndroid Build Coastguard Worker  Returns:
84*9e94795aSAndroid Build Coastguard Worker    A dictionary of Test Mapping containing the content of the affected
85*9e94795aSAndroid Build Coastguard Worker      TEST_MAPPING files, indexed by the path containing the TEST_MAPPING file.
86*9e94795aSAndroid Build Coastguard Worker  """
87*9e94795aSAndroid Build Coastguard Worker  test_mappings = {}
88*9e94795aSAndroid Build Coastguard Worker
89*9e94795aSAndroid Build Coastguard Worker  # Search for TEST_MAPPING files in each modified path and its parent
90*9e94795aSAndroid Build Coastguard Worker  # directories.
91*9e94795aSAndroid Build Coastguard Worker  all_paths = set()
92*9e94795aSAndroid Build Coastguard Worker  for path in paths:
93*9e94795aSAndroid Build Coastguard Worker    dir_names = path.split(os.path.sep)
94*9e94795aSAndroid Build Coastguard Worker    all_paths |= set(
95*9e94795aSAndroid Build Coastguard Worker        [os.path.sep.join(dir_names[:i + 1]) for i in range(len(dir_names))])
96*9e94795aSAndroid Build Coastguard Worker  # Add root directory to the paths to search for TEST_MAPPING file.
97*9e94795aSAndroid Build Coastguard Worker  all_paths.add('')
98*9e94795aSAndroid Build Coastguard Worker
99*9e94795aSAndroid Build Coastguard Worker  all_paths.difference_update(checked_paths)
100*9e94795aSAndroid Build Coastguard Worker  checked_paths |= all_paths
101*9e94795aSAndroid Build Coastguard Worker  # Try to load TEST_MAPPING file in each possible path.
102*9e94795aSAndroid Build Coastguard Worker  for path in all_paths:
103*9e94795aSAndroid Build Coastguard Worker    try:
104*9e94795aSAndroid Build Coastguard Worker      test_mapping_file = os.path.join(os.path.join(os.getcwd(), path), 'TEST_MAPPING')
105*9e94795aSAndroid Build Coastguard Worker      # Read content of TEST_MAPPING file.
106*9e94795aSAndroid Build Coastguard Worker      content = FilterComments(open(test_mapping_file, "r").read())
107*9e94795aSAndroid Build Coastguard Worker      test_mapping = json.loads(content)
108*9e94795aSAndroid Build Coastguard Worker      test_mappings[path] = test_mapping
109*9e94795aSAndroid Build Coastguard Worker
110*9e94795aSAndroid Build Coastguard Worker      import_paths = set()
111*9e94795aSAndroid Build Coastguard Worker      for import_detail in test_mapping.get(KEY_IMPORTS, []):
112*9e94795aSAndroid Build Coastguard Worker        import_path = import_detail[KEY_IMPORT_PATH]
113*9e94795aSAndroid Build Coastguard Worker        # Try the import path as absolute path.
114*9e94795aSAndroid Build Coastguard Worker        import_paths.add(import_path)
115*9e94795aSAndroid Build Coastguard Worker        # Try the import path as relative path based on the test mapping file
116*9e94795aSAndroid Build Coastguard Worker        # containing the import.
117*9e94795aSAndroid Build Coastguard Worker        norm_import_path = os.path.normpath(os.path.join(path, import_path))
118*9e94795aSAndroid Build Coastguard Worker        import_paths.add(norm_import_path)
119*9e94795aSAndroid Build Coastguard Worker      import_paths.difference_update(checked_paths)
120*9e94795aSAndroid Build Coastguard Worker      if import_paths:
121*9e94795aSAndroid Build Coastguard Worker        import_test_mappings = GetTestMappings(import_paths, checked_paths)
122*9e94795aSAndroid Build Coastguard Worker        test_mappings.update(import_test_mappings)
123*9e94795aSAndroid Build Coastguard Worker    except (KeyError, FileNotFoundError, NotADirectoryError):
124*9e94795aSAndroid Build Coastguard Worker      # TEST_MAPPING file doesn't exist in path
125*9e94795aSAndroid Build Coastguard Worker      pass
126*9e94795aSAndroid Build Coastguard Worker
127*9e94795aSAndroid Build Coastguard Worker  return test_mappings
128*9e94795aSAndroid Build Coastguard Worker
129*9e94795aSAndroid Build Coastguard Worker
130*9e94795aSAndroid Build Coastguard Workerdef FindAffectedModules(
131*9e94795aSAndroid Build Coastguard Worker    test_mappings: dict[str, Any],
132*9e94795aSAndroid Build Coastguard Worker    changed_files: set[str],
133*9e94795aSAndroid Build Coastguard Worker    test_mapping_test_groups: set[str],
134*9e94795aSAndroid Build Coastguard Worker) -> set[str]:
135*9e94795aSAndroid Build Coastguard Worker  """Find affected test modules.
136*9e94795aSAndroid Build Coastguard Worker
137*9e94795aSAndroid Build Coastguard Worker  Find the affected set of test modules that would run in a test mapping run based on the given test mappings, changed files, and test mapping test group.
138*9e94795aSAndroid Build Coastguard Worker
139*9e94795aSAndroid Build Coastguard Worker  Args:
140*9e94795aSAndroid Build Coastguard Worker    test_mappings: A set of test mappings returned by GetTestMappings in the following format:
141*9e94795aSAndroid Build Coastguard Worker      {
142*9e94795aSAndroid Build Coastguard Worker        'test_mapping_file_path': {
143*9e94795aSAndroid Build Coastguard Worker          'group_name' : [
144*9e94795aSAndroid Build Coastguard Worker            'name': 'module_name',
145*9e94795aSAndroid Build Coastguard Worker          ],
146*9e94795aSAndroid Build Coastguard Worker        }
147*9e94795aSAndroid Build Coastguard Worker      }
148*9e94795aSAndroid Build Coastguard Worker    changed_files: A set of files changed for the given run.
149*9e94795aSAndroid Build Coastguard Worker    test_mapping_test_groups: A set of test mapping test groups that are being considered for the given run.
150*9e94795aSAndroid Build Coastguard Worker
151*9e94795aSAndroid Build Coastguard Worker  Returns:
152*9e94795aSAndroid Build Coastguard Worker    A set of test module names which would run for a test mapping test run with the given parameters.
153*9e94795aSAndroid Build Coastguard Worker  """
154*9e94795aSAndroid Build Coastguard Worker
155*9e94795aSAndroid Build Coastguard Worker  modules = set()
156*9e94795aSAndroid Build Coastguard Worker
157*9e94795aSAndroid Build Coastguard Worker  for test_mapping in test_mappings.values():
158*9e94795aSAndroid Build Coastguard Worker    for group_name, group in test_mapping.items():
159*9e94795aSAndroid Build Coastguard Worker      # If a module is not in any of the test mapping groups being tested skip
160*9e94795aSAndroid Build Coastguard Worker      # it.
161*9e94795aSAndroid Build Coastguard Worker      if group_name not in test_mapping_test_groups:
162*9e94795aSAndroid Build Coastguard Worker        continue
163*9e94795aSAndroid Build Coastguard Worker
164*9e94795aSAndroid Build Coastguard Worker      for entry in group:
165*9e94795aSAndroid Build Coastguard Worker        module_name = entry.get('name')
166*9e94795aSAndroid Build Coastguard Worker
167*9e94795aSAndroid Build Coastguard Worker        if not module_name:
168*9e94795aSAndroid Build Coastguard Worker          continue
169*9e94795aSAndroid Build Coastguard Worker
170*9e94795aSAndroid Build Coastguard Worker        file_patterns = entry.get('file_patterns')
171*9e94795aSAndroid Build Coastguard Worker        if not file_patterns:
172*9e94795aSAndroid Build Coastguard Worker          modules.add(module_name)
173*9e94795aSAndroid Build Coastguard Worker          continue
174*9e94795aSAndroid Build Coastguard Worker
175*9e94795aSAndroid Build Coastguard Worker        if matches_file_patterns(file_patterns, changed_files):
176*9e94795aSAndroid Build Coastguard Worker          modules.add(module_name)
177*9e94795aSAndroid Build Coastguard Worker
178*9e94795aSAndroid Build Coastguard Worker  return modules
179*9e94795aSAndroid Build Coastguard Worker
180*9e94795aSAndroid Build Coastguard Workerdef MatchesFilePatterns(
181*9e94795aSAndroid Build Coastguard Worker    file_patterns: list[set], changed_files: set[str]
182*9e94795aSAndroid Build Coastguard Worker) -> bool:
183*9e94795aSAndroid Build Coastguard Worker  """Checks if any of the changed files match any of the file patterns.
184*9e94795aSAndroid Build Coastguard Worker
185*9e94795aSAndroid Build Coastguard Worker  Args:
186*9e94795aSAndroid Build Coastguard Worker    file_patterns: A list of file patterns to match against.
187*9e94795aSAndroid Build Coastguard Worker    changed_files: A set of files to check against the file patterns.
188*9e94795aSAndroid Build Coastguard Worker
189*9e94795aSAndroid Build Coastguard Worker  Returns:
190*9e94795aSAndroid Build Coastguard Worker    True if any of the changed files match any of the file patterns.
191*9e94795aSAndroid Build Coastguard Worker  """
192*9e94795aSAndroid Build Coastguard Worker  return any(re.search(pattern, "|".join(changed_files)) for pattern in file_patterns)
193