blob: d2c13c0e7d0616ca41d3afe684e15bdb63d04a0e [file] [log] [blame]
Luca Farsi5717d6f2023-12-28 15:09:28 -08001# Copyright 2024, The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""
16Simple parsing code to scan test_mapping files and determine which
17modules are needed to build for the given list of changed files.
18TODO(lucafarsi): Deduplicate from artifact_helper.py
19"""
20
21from typing import Any, Dict, Set, Text
22import json
23import os
24import re
25
26# Regex to extra test name from the path of test config file.
27TEST_NAME_REGEX = r'(?:^|.*/)([^/]+)\.config'
28
29# Key name for TEST_MAPPING imports
30KEY_IMPORTS = 'imports'
31KEY_IMPORT_PATH = 'path'
32
33# Name of TEST_MAPPING file.
34TEST_MAPPING = 'TEST_MAPPING'
35
36# Pattern used to identify double-quoted strings and '//'-format comments in
37# TEST_MAPPING file, but only double-quoted strings are included within the
38# matching group.
39_COMMENTS_RE = re.compile(r'(\"(?:[^\"\\]|\\.)*\"|(?=//))(?://.*)?')
40
41
42def FilterComments(test_mapping_file: Text) -> Text:
43 """Remove comments in TEST_MAPPING file to valid format.
44
45 Only '//' is regarded as comments.
46
47 Args:
48 test_mapping_file: Path to a TEST_MAPPING file.
49
50 Returns:
51 Valid json string without comments.
52 """
53 return re.sub(_COMMENTS_RE, r'\1', test_mapping_file)
54
55def GetTestMappings(paths: Set[Text],
56 checked_paths: Set[Text]) -> Dict[Text, Dict[Text, Any]]:
57 """Get the affected TEST_MAPPING files.
58
59 TEST_MAPPING files in source code are packaged into a build artifact
60 `test_mappings.zip`. Inside the zip file, the path of each TEST_MAPPING file
61 is preserved. From all TEST_MAPPING files in the source code, this method
62 locates the affected TEST_MAPPING files based on the given paths list.
63
64 A TEST_MAPPING file may also contain `imports` that import TEST_MAPPING files
65 from a different location, e.g.,
66 "imports": [
67 {
68 "path": "../folder2"
69 }
70 ]
71 In that example, TEST_MAPPING files inside ../folder2 (relative to the
72 TEST_MAPPING file containing that imports section) and its parent directories
73 will also be included.
74
75 Args:
76 paths: A set of paths with related TEST_MAPPING files for given changes.
77 checked_paths: A set of paths that have been checked for TEST_MAPPING file
78 already. The set is updated after processing each TEST_MAPPING file. It's
79 used to prevent infinite loop when the method is called recursively.
80
81 Returns:
82 A dictionary of Test Mapping containing the content of the affected
83 TEST_MAPPING files, indexed by the path containing the TEST_MAPPING file.
84 """
85 test_mappings = {}
86
87 # Search for TEST_MAPPING files in each modified path and its parent
88 # directories.
89 all_paths = set()
90 for path in paths:
91 dir_names = path.split(os.path.sep)
92 all_paths |= set(
93 [os.path.sep.join(dir_names[:i + 1]) for i in range(len(dir_names))])
94 # Add root directory to the paths to search for TEST_MAPPING file.
95 all_paths.add('')
96
97 all_paths.difference_update(checked_paths)
98 checked_paths |= all_paths
99 # Try to load TEST_MAPPING file in each possible path.
100 for path in all_paths:
101 try:
102 test_mapping_file = os.path.join(os.path.join(os.getcwd(), path), 'TEST_MAPPING')
103 # Read content of TEST_MAPPING file.
104 content = FilterComments(open(test_mapping_file, "r").read())
105 test_mapping = json.loads(content)
106 test_mappings[path] = test_mapping
107
108 import_paths = set()
109 for import_detail in test_mapping.get(KEY_IMPORTS, []):
110 import_path = import_detail[KEY_IMPORT_PATH]
111 # Try the import path as absolute path.
112 import_paths.add(import_path)
113 # Try the import path as relative path based on the test mapping file
114 # containing the import.
115 norm_import_path = os.path.normpath(os.path.join(path, import_path))
116 import_paths.add(norm_import_path)
117 import_paths.difference_update(checked_paths)
118 if import_paths:
119 import_test_mappings = GetTestMappings(import_paths, checked_paths)
120 test_mappings.update(import_test_mappings)
121 except (KeyError, FileNotFoundError, NotADirectoryError):
122 # TEST_MAPPING file doesn't exist in path
123 pass
124
125 return test_mappings