blob: fddde176ec7544b3893d8aabfa0db74a24e7aeb7 [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
55 def package_outputs(self):
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:
58 return self.package_outputs_impl()
59
60 def package_outputs_impl(self):
61 raise NotImplementedError(
62 f'package_outputs_impl not implemented in {type(self).__name__}'
63 )
64
65 def get_enabled_flag(self):
66 raise NotImplementedError(
67 f'get_enabled_flag not implemented in {type(self).__name__}'
68 )
69
70 def get_build_targets_impl(self) -> set[str]:
71 raise NotImplementedError(
72 f'get_build_targets_impl not implemented in {type(self).__name__}'
73 )
Luca Farsi040fabe2024-05-22 17:21:47 -070074
75
76class NullOptimizer(OptimizedBuildTarget):
77 """No-op target optimizer.
78
79 This will simply build the same target it was given and do nothing for the
80 packaging step.
81 """
82
83 def __init__(self, target):
84 self.target = target
85
86 def get_build_targets(self):
87 return {self.target}
88
89 def package_outputs(self):
90 pass
91
92
Luca Farsib9c54642024-08-13 17:16:33 -070093class ChangeInfo:
94
95 def __init__(self, change_info_file_path):
96 try:
97 with open(change_info_file_path) as change_info_file:
98 change_info_contents = json.load(change_info_file)
99 except json.decoder.JSONDecodeError:
100 logging.error(f'Failed to load CHANGE_INFO: {change_info_file_path}')
101 raise
102
103 self._change_info_contents = change_info_contents
104
105 def find_changed_files(self) -> set[str]:
106 changed_files = set()
107
108 for change in self._change_info_contents['changes']:
109 project_path = change.get('projectPath') + '/'
110
111 for revision in change.get('revisions'):
112 for file_info in revision.get('fileInfos'):
113 changed_files.add(project_path + file_info.get('path'))
114
115 return changed_files
116
Luca Farsi70a53bd2024-08-07 17:29:16 -0700117class GeneralTestsOptimizer(OptimizedBuildTarget):
118 """general-tests optimizer
Luca Farsi040fabe2024-05-22 17:21:47 -0700119
Luca Farsi70a53bd2024-08-07 17:29:16 -0700120 TODO(b/358215235): Implement
121
122 This optimizer reads in the list of changed files from the file located in
123 env[CHANGE_INFO] and uses this list alongside the normal TEST MAPPING logic to
124 determine what test mapping modules will run for the given changes. It then
125 builds those modules and packages them in the same way general-tests.zip is
126 normally built.
127 """
128
Luca Farsib9c54642024-08-13 17:16:33 -0700129 # List of modules that are always required to be in general-tests.zip.
130 _REQUIRED_MODULES = frozenset(
131 ['cts-tradefed', 'vts-tradefed', 'compatibility-host-util']
132 )
133
134 def get_build_targets_impl(self) -> set[str]:
135 change_info_file_path = os.environ.get('CHANGE_INFO')
136 if not change_info_file_path:
137 logging.info(
138 'No CHANGE_INFO env var found, general-tests optimization disabled.'
139 )
140 return {'general-tests'}
141
142 test_infos = self.build_context.test_infos
143 test_mapping_test_groups = set()
144 for test_info in test_infos:
145 is_test_mapping = test_info.is_test_mapping
146 current_test_mapping_test_groups = test_info.test_mapping_test_groups
147 uses_general_tests = test_info.build_target_used('general-tests')
148
149 if uses_general_tests and not is_test_mapping:
150 logging.info(
151 'Test uses general-tests.zip but is not test-mapping, general-tests'
152 ' optimization disabled.'
153 )
154 return {'general-tests'}
155
156 if is_test_mapping:
157 test_mapping_test_groups.update(current_test_mapping_test_groups)
158
159 change_info = ChangeInfo(change_info_file_path)
160 changed_files = change_info.find_changed_files()
161
162 test_mappings = test_mapping_module_retriever.GetTestMappings(
163 changed_files, set()
164 )
165
166 modules_to_build = set(self._REQUIRED_MODULES)
167
168 modules_to_build.update(
169 test_mapping_module_retriever.FindAffectedModules(
170 test_mappings, changed_files, test_mapping_test_groups
171 )
172 )
173
174 return modules_to_build
175
Luca Farsi70a53bd2024-08-07 17:29:16 -0700176 def get_enabled_flag(self):
Luca Farsib9c54642024-08-13 17:16:33 -0700177 return 'general_tests_optimized'
Luca Farsi70a53bd2024-08-07 17:29:16 -0700178
179 @classmethod
180 def get_optimized_targets(cls) -> dict[str, OptimizedBuildTarget]:
181 return {'general-tests': functools.partial(cls)}
Luca Farsi040fabe2024-05-22 17:21:47 -0700182
183
Luca Farsi70a53bd2024-08-07 17:29:16 -0700184OPTIMIZED_BUILD_TARGETS = {}
185OPTIMIZED_BUILD_TARGETS.update(GeneralTestsOptimizer.get_optimized_targets())