blob: 9143cbf2f1da57f78be9108b5ac5d0cfaa215a88 [file] [log] [blame]
Luca Farsi040fabe2024-05-22 17:21:47 -07001#
2# Copyright 2024, The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16from abc import ABC
Luca Farsi70a53bd2024-08-07 17:29:16 -070017import argparse
18import functools
Luca Farsib130e792024-08-22 12:04:41 -070019from build_context import BuildContext
Luca Farsib9c54642024-08-13 17:16:33 -070020import json
21import logging
22import os
23from typing import Self
24
25import test_mapping_module_retriever
Luca Farsi040fabe2024-05-22 17:21:47 -070026
27
28class OptimizedBuildTarget(ABC):
29 """A representation of an optimized build target.
30
31 This class will determine what targets to build given a given build_cotext and
32 will have a packaging function to generate any necessary output zips for the
33 build.
34 """
35
Luca Farsi70a53bd2024-08-07 17:29:16 -070036 def __init__(
37 self,
38 target: str,
Luca Farsib130e792024-08-22 12:04:41 -070039 build_context: BuildContext,
Luca Farsi70a53bd2024-08-07 17:29:16 -070040 args: argparse.Namespace,
41 ):
42 self.target = target
Luca Farsi040fabe2024-05-22 17:21:47 -070043 self.build_context = build_context
44 self.args = args
45
Luca Farsi70a53bd2024-08-07 17:29:16 -070046 def get_build_targets(self) -> set[str]:
Luca Farsib130e792024-08-22 12:04:41 -070047 features = self.build_context.enabled_build_features
Luca Farsi70a53bd2024-08-07 17:29:16 -070048 if self.get_enabled_flag() in features:
Luca Farsib9c54642024-08-13 17:16:33 -070049 self.modules_to_build = self.get_build_targets_impl()
50 return self.modules_to_build
51
52 self.modules_to_build = {self.target}
Luca Farsi70a53bd2024-08-07 17:29:16 -070053 return {self.target}
Luca Farsi040fabe2024-05-22 17:21:47 -070054
Luca Farsid4e4b642024-09-10 16:37:51 -070055 def get_package_outputs_commands(self) -> list[list[str]]:
Luca Farsib130e792024-08-22 12:04:41 -070056 features = self.build_context.enabled_build_features
Luca Farsi70a53bd2024-08-07 17:29:16 -070057 if self.get_enabled_flag() in features:
Luca Farsid4e4b642024-09-10 16:37:51 -070058 return self.get_package_outputs_commands_impl()
Luca Farsi70a53bd2024-08-07 17:29:16 -070059
Luca Farsid4e4b642024-09-10 16:37:51 -070060 return []
61
62 def get_package_outputs_commands_impl(self) -> list[list[str]]:
Luca Farsi70a53bd2024-08-07 17:29:16 -070063 raise NotImplementedError(
Luca Farsid4e4b642024-09-10 16:37:51 -070064 'get_package_outputs_commands_impl not implemented in'
65 f' {type(self).__name__}'
Luca Farsi70a53bd2024-08-07 17:29:16 -070066 )
67
68 def get_enabled_flag(self):
69 raise NotImplementedError(
70 f'get_enabled_flag not implemented in {type(self).__name__}'
71 )
72
73 def get_build_targets_impl(self) -> set[str]:
74 raise NotImplementedError(
75 f'get_build_targets_impl not implemented in {type(self).__name__}'
76 )
Luca Farsi040fabe2024-05-22 17:21:47 -070077
78
79class NullOptimizer(OptimizedBuildTarget):
80 """No-op target optimizer.
81
82 This will simply build the same target it was given and do nothing for the
83 packaging step.
84 """
85
86 def __init__(self, target):
87 self.target = target
88
89 def get_build_targets(self):
90 return {self.target}
91
Luca Farsid4e4b642024-09-10 16:37:51 -070092 def get_package_outputs_commands(self):
93 return []
Luca Farsi040fabe2024-05-22 17:21:47 -070094
95
Luca Farsib9c54642024-08-13 17:16:33 -070096class ChangeInfo:
97
98 def __init__(self, change_info_file_path):
99 try:
100 with open(change_info_file_path) as change_info_file:
101 change_info_contents = json.load(change_info_file)
102 except json.decoder.JSONDecodeError:
103 logging.error(f'Failed to load CHANGE_INFO: {change_info_file_path}')
104 raise
105
106 self._change_info_contents = change_info_contents
107
108 def find_changed_files(self) -> set[str]:
109 changed_files = set()
110
111 for change in self._change_info_contents['changes']:
112 project_path = change.get('projectPath') + '/'
113
114 for revision in change.get('revisions'):
115 for file_info in revision.get('fileInfos'):
116 changed_files.add(project_path + file_info.get('path'))
117
118 return changed_files
119
Luca Farsid4e4b642024-09-10 16:37:51 -0700120
Luca Farsi70a53bd2024-08-07 17:29:16 -0700121class GeneralTestsOptimizer(OptimizedBuildTarget):
122 """general-tests optimizer
Luca Farsi040fabe2024-05-22 17:21:47 -0700123
Luca Farsi70a53bd2024-08-07 17:29:16 -0700124 TODO(b/358215235): Implement
125
126 This optimizer reads in the list of changed files from the file located in
127 env[CHANGE_INFO] and uses this list alongside the normal TEST MAPPING logic to
128 determine what test mapping modules will run for the given changes. It then
129 builds those modules and packages them in the same way general-tests.zip is
130 normally built.
131 """
132
Luca Farsib9c54642024-08-13 17:16:33 -0700133 # List of modules that are always required to be in general-tests.zip.
134 _REQUIRED_MODULES = frozenset(
135 ['cts-tradefed', 'vts-tradefed', 'compatibility-host-util']
136 )
137
138 def get_build_targets_impl(self) -> set[str]:
139 change_info_file_path = os.environ.get('CHANGE_INFO')
140 if not change_info_file_path:
141 logging.info(
142 'No CHANGE_INFO env var found, general-tests optimization disabled.'
143 )
144 return {'general-tests'}
145
146 test_infos = self.build_context.test_infos
147 test_mapping_test_groups = set()
148 for test_info in test_infos:
149 is_test_mapping = test_info.is_test_mapping
150 current_test_mapping_test_groups = test_info.test_mapping_test_groups
151 uses_general_tests = test_info.build_target_used('general-tests')
152
153 if uses_general_tests and not is_test_mapping:
154 logging.info(
155 'Test uses general-tests.zip but is not test-mapping, general-tests'
156 ' optimization disabled.'
157 )
158 return {'general-tests'}
159
160 if is_test_mapping:
161 test_mapping_test_groups.update(current_test_mapping_test_groups)
162
163 change_info = ChangeInfo(change_info_file_path)
164 changed_files = change_info.find_changed_files()
165
166 test_mappings = test_mapping_module_retriever.GetTestMappings(
167 changed_files, set()
168 )
169
170 modules_to_build = set(self._REQUIRED_MODULES)
171
172 modules_to_build.update(
173 test_mapping_module_retriever.FindAffectedModules(
174 test_mappings, changed_files, test_mapping_test_groups
175 )
176 )
177
178 return modules_to_build
179
Luca Farsi70a53bd2024-08-07 17:29:16 -0700180 def get_enabled_flag(self):
Luca Farsib9c54642024-08-13 17:16:33 -0700181 return 'general_tests_optimized'
Luca Farsi70a53bd2024-08-07 17:29:16 -0700182
183 @classmethod
184 def get_optimized_targets(cls) -> dict[str, OptimizedBuildTarget]:
185 return {'general-tests': functools.partial(cls)}
Luca Farsi040fabe2024-05-22 17:21:47 -0700186
187
Luca Farsi70a53bd2024-08-07 17:29:16 -0700188OPTIMIZED_BUILD_TARGETS = {}
189OPTIMIZED_BUILD_TARGETS.update(GeneralTestsOptimizer.get_optimized_targets())