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