blob: 548e34273da7bfef2bec8b31476741e5dcd89e26 [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 Farsib9c54642024-08-13 17:16:33 -070019import json
20import logging
21import os
Luca Farsi64598e82024-08-28 13:39:25 -070022import pathlib
23import subprocess
Luca Farsib9c54642024-08-13 17:16:33 -070024
Luca Farsi64598e82024-08-28 13:39:25 -070025from build_context import BuildContext
Luca Farsi6a7c8932025-03-14 23:32:22 +000026import metrics_agent
Luca Farsib9c54642024-08-13 17:16:33 -070027import test_mapping_module_retriever
Luca Farsi6a7c8932025-03-14 23:32:22 +000028import test_discovery_agent
Luca Farsi040fabe2024-05-22 17:21:47 -070029
30
31class OptimizedBuildTarget(ABC):
32 """A representation of an optimized build target.
33
34 This class will determine what targets to build given a given build_cotext and
35 will have a packaging function to generate any necessary output zips for the
36 build.
37 """
38
Luca Farsi64598e82024-08-28 13:39:25 -070039 _SOONG_UI_BASH_PATH = 'build/soong/soong_ui.bash'
40 _PREBUILT_SOONG_ZIP_PATH = 'prebuilts/build-tools/linux-x86/bin/soong_zip'
41
Luca Farsi70a53bd2024-08-07 17:29:16 -070042 def __init__(
43 self,
44 target: str,
Luca Farsib130e792024-08-22 12:04:41 -070045 build_context: BuildContext,
Luca Farsi70a53bd2024-08-07 17:29:16 -070046 args: argparse.Namespace,
Luca Farsi6a7c8932025-03-14 23:32:22 +000047 test_infos,
Luca Farsi70a53bd2024-08-07 17:29:16 -070048 ):
49 self.target = target
Luca Farsi040fabe2024-05-22 17:21:47 -070050 self.build_context = build_context
51 self.args = args
Luca Farsi6a346012025-02-11 00:57:53 +000052 self.test_infos = test_infos
Luca Farsi040fabe2024-05-22 17:21:47 -070053
Luca Farsi70a53bd2024-08-07 17:29:16 -070054 def get_build_targets(self) -> set[str]:
Luca Farsib130e792024-08-22 12:04:41 -070055 features = self.build_context.enabled_build_features
Luca Farsi70a53bd2024-08-07 17:29:16 -070056 if self.get_enabled_flag() in features:
Luca Farsib9c54642024-08-13 17:16:33 -070057 self.modules_to_build = self.get_build_targets_impl()
58 return self.modules_to_build
59
Luca Farsi6a7c8932025-03-14 23:32:22 +000060 if self.target == 'general-tests':
61 self._report_info_metrics_silently('general-tests.zip')
Luca Farsib9c54642024-08-13 17:16:33 -070062 self.modules_to_build = {self.target}
Luca Farsi70a53bd2024-08-07 17:29:16 -070063 return {self.target}
Luca Farsi040fabe2024-05-22 17:21:47 -070064
Luca Farsid4e4b642024-09-10 16:37:51 -070065 def get_package_outputs_commands(self) -> list[list[str]]:
Luca Farsib130e792024-08-22 12:04:41 -070066 features = self.build_context.enabled_build_features
Luca Farsi70a53bd2024-08-07 17:29:16 -070067 if self.get_enabled_flag() in features:
Luca Farsid4e4b642024-09-10 16:37:51 -070068 return self.get_package_outputs_commands_impl()
Luca Farsi70a53bd2024-08-07 17:29:16 -070069
Luca Farsid4e4b642024-09-10 16:37:51 -070070 return []
71
72 def get_package_outputs_commands_impl(self) -> list[list[str]]:
Luca Farsi70a53bd2024-08-07 17:29:16 -070073 raise NotImplementedError(
Luca Farsid4e4b642024-09-10 16:37:51 -070074 'get_package_outputs_commands_impl not implemented in'
75 f' {type(self).__name__}'
Luca Farsi70a53bd2024-08-07 17:29:16 -070076 )
77
78 def get_enabled_flag(self):
79 raise NotImplementedError(
80 f'get_enabled_flag not implemented in {type(self).__name__}'
81 )
82
83 def get_build_targets_impl(self) -> set[str]:
84 raise NotImplementedError(
85 f'get_build_targets_impl not implemented in {type(self).__name__}'
86 )
Luca Farsi040fabe2024-05-22 17:21:47 -070087
Luca Farsi64598e82024-08-28 13:39:25 -070088 def _generate_zip_options_for_items(
89 self,
90 prefix: str = '',
91 relative_root: str = '',
92 list_files: list[str] | None = None,
93 files: list[str] | None = None,
94 directories: list[str] | None = None,
95 ) -> list[str]:
96 if not list_files and not files and not directories:
97 raise RuntimeError(
98 f'No items specified to be added to zip! Prefix: {prefix}, Relative'
99 f' root: {relative_root}'
100 )
101 command_segment = []
102 # These are all soong_zip options so consult soong_zip --help for specifics.
103 if prefix:
104 command_segment.append('-P')
105 command_segment.append(prefix)
106 if relative_root:
107 command_segment.append('-C')
108 command_segment.append(relative_root)
109 if list_files:
110 for list_file in list_files:
111 command_segment.append('-l')
112 command_segment.append(list_file)
113 if files:
114 for file in files:
115 command_segment.append('-f')
116 command_segment.append(file)
117 if directories:
118 for directory in directories:
119 command_segment.append('-D')
120 command_segment.append(directory)
121
122 return command_segment
123
124 def _query_soong_vars(
125 self, src_top: pathlib.Path, soong_vars: list[str]
126 ) -> dict[str, str]:
127 process_result = subprocess.run(
128 args=[
129 f'{src_top / self._SOONG_UI_BASH_PATH}',
Luca Farsi8ea67422024-09-17 15:48:11 -0700130 '--dumpvars-mode',
131 f'--abs-vars={" ".join(soong_vars)}',
Luca Farsi64598e82024-08-28 13:39:25 -0700132 ],
133 env=os.environ,
134 check=False,
135 capture_output=True,
Luca Farsi8ea67422024-09-17 15:48:11 -0700136 text=True,
Luca Farsi64598e82024-08-28 13:39:25 -0700137 )
138 if not process_result.returncode == 0:
139 logging.error('soong dumpvars command failed! stderr:')
140 logging.error(process_result.stderr)
141 raise RuntimeError('Soong dumpvars failed! See log for stderr.')
142
143 if not process_result.stdout:
144 raise RuntimeError(
145 'Necessary soong variables ' + soong_vars + ' not found.'
146 )
147
148 try:
149 return {
150 line.split('=')[0]: line.split('=')[1].strip("'")
Luca Farsi8ea67422024-09-17 15:48:11 -0700151 for line in process_result.stdout.strip().split('\n')
Luca Farsi64598e82024-08-28 13:39:25 -0700152 }
153 except IndexError as e:
154 raise RuntimeError(
155 'Error parsing soong dumpvars output! See output here:'
156 f' {process_result.stdout}',
157 e,
158 )
159
160 def _base_zip_command(
161 self, src_top: pathlib.Path, dist_dir: pathlib.Path, name: str
162 ) -> list[str]:
163 return [
164 f'{src_top / self._PREBUILT_SOONG_ZIP_PATH }',
165 '-d',
166 '-o',
167 f'{dist_dir / name}',
168 ]
169
Luca Farsi6a7c8932025-03-14 23:32:22 +0000170 def _report_info_metrics_silently(self, artifact_name):
171 try:
172 metrics_agent_instance = metrics_agent.MetricsAgent.instance()
173 targets = self.get_build_targets_impl()
174 metrics_agent_instance.report_optimized_target(self.target)
175 metrics_agent_instance.add_target_artifact(self.target, artifact_name, 0, targets)
176 except Exception as e:
177 logging.error(f'error while silently reporting metrics: {e}')
178
179
Luca Farsi040fabe2024-05-22 17:21:47 -0700180
181class NullOptimizer(OptimizedBuildTarget):
182 """No-op target optimizer.
183
184 This will simply build the same target it was given and do nothing for the
185 packaging step.
186 """
187
188 def __init__(self, target):
189 self.target = target
190
191 def get_build_targets(self):
192 return {self.target}
193
Luca Farsid4e4b642024-09-10 16:37:51 -0700194 def get_package_outputs_commands(self):
195 return []
Luca Farsi040fabe2024-05-22 17:21:47 -0700196
197
Luca Farsib9c54642024-08-13 17:16:33 -0700198class ChangeInfo:
199
200 def __init__(self, change_info_file_path):
201 try:
202 with open(change_info_file_path) as change_info_file:
203 change_info_contents = json.load(change_info_file)
204 except json.decoder.JSONDecodeError:
205 logging.error(f'Failed to load CHANGE_INFO: {change_info_file_path}')
206 raise
207
208 self._change_info_contents = change_info_contents
209
Julien Desprezbe518142025-03-21 11:31:01 -0700210 def get_changed_paths(self) -> set[str]:
211 changed_paths = set()
212 for change in self._change_info_contents['changes']:
213 project_path = change.get('projectPath') + '/'
214
215 for revision in change.get('revisions'):
216 for file_info in revision.get('fileInfos'):
217 file_path = file_info.get('path')
218 dir_path = os.path.dirname(file_path)
219 changed_paths.add(project_path + dir_path)
220
221 return changed_paths
222
Luca Farsib9c54642024-08-13 17:16:33 -0700223 def find_changed_files(self) -> set[str]:
224 changed_files = set()
225
226 for change in self._change_info_contents['changes']:
227 project_path = change.get('projectPath') + '/'
228
229 for revision in change.get('revisions'):
230 for file_info in revision.get('fileInfos'):
231 changed_files.add(project_path + file_info.get('path'))
232
233 return changed_files
234
Luca Farsid4e4b642024-09-10 16:37:51 -0700235
Luca Farsi70a53bd2024-08-07 17:29:16 -0700236class GeneralTestsOptimizer(OptimizedBuildTarget):
237 """general-tests optimizer
Luca Farsi040fabe2024-05-22 17:21:47 -0700238
Luca Farsi6a7c8932025-03-14 23:32:22 +0000239 This optimizer uses test discovery to build a list of modules that are needed by all tests configured for the build. These modules are then build and packaged by the optimizer in the same way as they are in a normal build.
Luca Farsi70a53bd2024-08-07 17:29:16 -0700240 """
241
Luca Farsi8ea67422024-09-17 15:48:11 -0700242 # List of modules that are built alongside general-tests as dependencies.
243 _REQUIRED_MODULES = frozenset([
244 'cts-tradefed',
245 'vts-tradefed',
246 'compatibility-host-util',
Luca Farsi8ea67422024-09-17 15:48:11 -0700247 ])
Luca Farsib9c54642024-08-13 17:16:33 -0700248
249 def get_build_targets_impl(self) -> set[str]:
Luca Farsi6a7c8932025-03-14 23:32:22 +0000250 self._general_tests_outputs = self._get_general_tests_outputs()
251 test_modules = self._get_test_discovery_modules()
Luca Farsib9c54642024-08-13 17:16:33 -0700252
253 modules_to_build = set(self._REQUIRED_MODULES)
Luca Farsi6a7c8932025-03-14 23:32:22 +0000254 self._build_outputs = []
255 for module in test_modules:
256 module_outputs = [output for output in self._general_tests_outputs if module in output]
257 if module_outputs:
258 modules_to_build.add(module)
259 self._build_outputs.extend(module_outputs)
Luca Farsib9c54642024-08-13 17:16:33 -0700260
261 return modules_to_build
262
Luca Farsi6a7c8932025-03-14 23:32:22 +0000263 def _get_general_tests_outputs(self) -> list[str]:
264 src_top = pathlib.Path(os.environ.get('TOP', os.getcwd()))
265 soong_vars = self._query_soong_vars(
266 src_top,
267 [
268 'PRODUCT_OUT',
269 ],
270 )
271 product_out = pathlib.Path(soong_vars.get('PRODUCT_OUT'))
272 with open(f'{product_out / "general-tests_files"}') as general_tests_list_file:
273 general_tests_list = general_tests_list_file.readlines()
274 with open(f'{product_out / "general-tests_host_files"}') as general_tests_list_file:
275 self._general_tests_host_outputs = general_tests_list_file.readlines()
276 with open(f'{product_out / "general-tests_target_files"}') as general_tests_list_file:
277 self._general_tests_target_outputs = general_tests_list_file.readlines()
278 return general_tests_list
279
280
281 def _get_test_discovery_modules(self) -> set[str]:
Julien Desprezbe518142025-03-21 11:31:01 -0700282 change_info = ChangeInfo(os.environ.get('CHANGE_INFO'))
283 change_paths = change_info.get_changed_paths()
Luca Farsi6a7c8932025-03-14 23:32:22 +0000284 test_modules = set()
285 for test_info in self.test_infos:
Julien Desprezbe518142025-03-21 11:31:01 -0700286 tf_command = self._build_tf_command(test_info, change_paths)
Luca Farsi6a7c8932025-03-14 23:32:22 +0000287 discovery_agent = test_discovery_agent.TestDiscoveryAgent(tradefed_args=tf_command, test_mapping_zip_path=os.environ.get('DIST_DIR')+'/test_mappings.zip')
288 modules, dependencies = discovery_agent.discover_test_mapping_test_modules()
289 for regex in modules:
290 test_modules.add(regex)
291 return test_modules
292
293
Julien Desprezbe518142025-03-21 11:31:01 -0700294 def _build_tf_command(self, test_info, change_paths) -> list[str]:
Luca Farsi6a7c8932025-03-14 23:32:22 +0000295 command = [test_info.command]
296 for extra_option in test_info.extra_options:
297 if not extra_option.get('key'):
298 continue
299 arg_key = '--' + extra_option.get('key')
300 if arg_key == '--build-id':
301 command.append(arg_key)
302 command.append(os.environ.get('BUILD_NUMBER'))
303 continue
304 if extra_option.get('values'):
305 for value in extra_option.get('values'):
306 command.append(arg_key)
307 command.append(value)
308 else:
309 command.append(arg_key)
Julien Desprezbe518142025-03-21 11:31:01 -0700310 if test_info.is_test_mapping:
311 for change_path in change_paths:
312 command.append('--test-mapping-path')
313 command.append(change_path)
Luca Farsi6a7c8932025-03-14 23:32:22 +0000314
315 return command
316
Luca Farsi64598e82024-08-28 13:39:25 -0700317 def get_package_outputs_commands_impl(self):
318 src_top = pathlib.Path(os.environ.get('TOP', os.getcwd()))
319 dist_dir = pathlib.Path(os.environ.get('DIST_DIR'))
Luca Farsi6a7c8932025-03-14 23:32:22 +0000320 tmp_dir = pathlib.Path(os.environ.get('TMPDIR'))
321 print(f'modules: {self.modules_to_build}')
Luca Farsi64598e82024-08-28 13:39:25 -0700322
Luca Farsi6a7c8932025-03-14 23:32:22 +0000323 host_outputs = [str(src_top) + '/' + file for file in self._general_tests_host_outputs if any('/'+module+'/' in file for module in self.modules_to_build)]
324 target_outputs = [str(src_top) + '/' + file for file in self._general_tests_target_outputs if any('/'+module+'/' in file for module in self.modules_to_build)]
325 host_config_files = [file for file in host_outputs if file.endswith('.config\n')]
326 target_config_files = [file for file in target_outputs if file.endswith('.config\n')]
327 logging.info(host_outputs)
328 logging.info(target_outputs)
329 with open(f"{tmp_dir / 'host.list'}", 'w') as host_list_file:
330 for output in host_outputs:
331 host_list_file.write(output)
332 with open(f"{tmp_dir / 'target.list'}", 'w') as target_list_file:
333 for output in target_outputs:
334 target_list_file.write(output)
Luca Farsi64598e82024-08-28 13:39:25 -0700335 soong_vars = self._query_soong_vars(
336 src_top,
337 [
Luca Farsi64598e82024-08-28 13:39:25 -0700338 'PRODUCT_OUT',
339 'SOONG_HOST_OUT',
340 'HOST_OUT',
341 ],
342 )
Luca Farsi64598e82024-08-28 13:39:25 -0700343 product_out = pathlib.Path(soong_vars.get('PRODUCT_OUT'))
344 soong_host_out = pathlib.Path(soong_vars.get('SOONG_HOST_OUT'))
345 host_out = pathlib.Path(soong_vars.get('HOST_OUT'))
Luca Farsi64598e82024-08-28 13:39:25 -0700346 zip_commands = []
347
348 zip_commands.extend(
349 self._get_zip_test_configs_zips_commands(
Luca Farsi8ea67422024-09-17 15:48:11 -0700350 src_top,
Luca Farsi64598e82024-08-28 13:39:25 -0700351 dist_dir,
352 host_out,
353 product_out,
354 host_config_files,
355 target_config_files,
356 )
357 )
358
Luca Farsi8ea67422024-09-17 15:48:11 -0700359 zip_command = self._base_zip_command(src_top, dist_dir, 'general-tests.zip')
Luca Farsi64598e82024-08-28 13:39:25 -0700360 # Add host testcases.
Luca Farsi6a7c8932025-03-14 23:32:22 +0000361 if host_outputs:
Luca Farsi8ea67422024-09-17 15:48:11 -0700362 zip_command.extend(
363 self._generate_zip_options_for_items(
364 prefix='host',
Luca Farsi6a7c8932025-03-14 23:32:22 +0000365 relative_root=str(host_out),
366 list_files=[f"{tmp_dir / 'host.list'}"],
Luca Farsi8ea67422024-09-17 15:48:11 -0700367 )
368 )
Luca Farsi64598e82024-08-28 13:39:25 -0700369
370 # Add target testcases.
Luca Farsi6a7c8932025-03-14 23:32:22 +0000371 if target_outputs:
Luca Farsi8ea67422024-09-17 15:48:11 -0700372 zip_command.extend(
373 self._generate_zip_options_for_items(
374 prefix='target',
Luca Farsi6a7c8932025-03-14 23:32:22 +0000375 relative_root=str(product_out),
376 list_files=[f"{tmp_dir / 'target.list'}"],
Luca Farsi8ea67422024-09-17 15:48:11 -0700377 )
378 )
Luca Farsi64598e82024-08-28 13:39:25 -0700379
380 # TODO(lucafarsi): Push this logic into a general-tests-minimal build command
381 # Add necessary tools. These are also hardcoded in general-tests.mk.
382 framework_path = soong_host_out / 'framework'
383
384 zip_command.extend(
385 self._generate_zip_options_for_items(
386 prefix='host/tools',
387 relative_root=str(framework_path),
388 files=[
389 f"{framework_path / 'cts-tradefed.jar'}",
390 f"{framework_path / 'compatibility-host-util.jar'}",
391 f"{framework_path / 'vts-tradefed.jar'}",
392 ],
393 )
394 )
395
Luca Farsi6a7c8932025-03-14 23:32:22 +0000396 zip_command.append('-sha256')
397
Luca Farsi64598e82024-08-28 13:39:25 -0700398 zip_commands.append(zip_command)
399 return zip_commands
400
Luca Farsi64598e82024-08-28 13:39:25 -0700401 def _get_zip_test_configs_zips_commands(
402 self,
Luca Farsi8ea67422024-09-17 15:48:11 -0700403 src_top: pathlib.Path,
Luca Farsi64598e82024-08-28 13:39:25 -0700404 dist_dir: pathlib.Path,
405 host_out: pathlib.Path,
406 product_out: pathlib.Path,
407 host_config_files: list[str],
408 target_config_files: list[str],
409 ) -> tuple[list[str], list[str]]:
410 """Generate general-tests_configs.zip and general-tests_list.zip.
411
412 general-tests_configs.zip contains all of the .config files that were
413 built and general-tests_list.zip contains a text file which lists
414 all of the .config files that are in general-tests_configs.zip.
415
416 general-tests_configs.zip is organized as follows:
417 /
418 host/
419 testcases/
420 test_1.config
421 test_2.config
422 ...
423 target/
424 testcases/
425 test_1.config
426 test_2.config
427 ...
428
429 So the process is we write out the paths to all the host config files into
430 one
431 file and all the paths to the target config files in another. We also write
432 the paths to all the config files into a third file to use for
433 general-tests_list.zip.
434
435 Args:
436 dist_dir: dist directory.
437 host_out: host out directory.
438 product_out: product out directory.
439 host_config_files: list of all host config files.
440 target_config_files: list of all target config files.
441
442 Returns:
443 The commands to generate general-tests_configs.zip and
444 general-tests_list.zip
445 """
446 with open(
447 f"{host_out / 'host_general-tests_list'}", 'w'
448 ) as host_list_file, open(
449 f"{product_out / 'target_general-tests_list'}", 'w'
450 ) as target_list_file, open(
451 f"{host_out / 'general-tests_list'}", 'w'
452 ) as list_file:
453
454 for config_file in host_config_files:
455 host_list_file.write(f'{config_file}' + '\n')
456 list_file.write('host/' + os.path.relpath(config_file, host_out) + '\n')
457
458 for config_file in target_config_files:
459 target_list_file.write(f'{config_file}' + '\n')
460 list_file.write(
461 'target/' + os.path.relpath(config_file, product_out) + '\n'
462 )
463
464 zip_commands = []
465
466 tests_config_zip_command = self._base_zip_command(
Luca Farsi8ea67422024-09-17 15:48:11 -0700467 src_top, dist_dir, 'general-tests_configs.zip'
Luca Farsi64598e82024-08-28 13:39:25 -0700468 )
469 tests_config_zip_command.extend(
470 self._generate_zip_options_for_items(
471 prefix='host',
472 relative_root=str(host_out),
473 list_files=[f"{host_out / 'host_general-tests_list'}"],
474 )
475 )
476
477 tests_config_zip_command.extend(
478 self._generate_zip_options_for_items(
479 prefix='target',
480 relative_root=str(product_out),
Luca Farsi8ea67422024-09-17 15:48:11 -0700481 list_files=[f"{product_out / 'target_general-tests_list'}"],
Luca Farsi64598e82024-08-28 13:39:25 -0700482 ),
483 )
484
485 zip_commands.append(tests_config_zip_command)
486
487 tests_list_zip_command = self._base_zip_command(
Luca Farsi8ea67422024-09-17 15:48:11 -0700488 src_top, dist_dir, 'general-tests_list.zip'
Luca Farsi64598e82024-08-28 13:39:25 -0700489 )
490 tests_list_zip_command.extend(
491 self._generate_zip_options_for_items(
492 relative_root=str(host_out),
493 files=[f"{host_out / 'general-tests_list'}"],
494 )
495 )
496 zip_commands.append(tests_list_zip_command)
497
498 return zip_commands
499
Luca Farsi70a53bd2024-08-07 17:29:16 -0700500 def get_enabled_flag(self):
Luca Farsib9c54642024-08-13 17:16:33 -0700501 return 'general_tests_optimized'
Luca Farsi70a53bd2024-08-07 17:29:16 -0700502
503 @classmethod
504 def get_optimized_targets(cls) -> dict[str, OptimizedBuildTarget]:
505 return {'general-tests': functools.partial(cls)}
Luca Farsi040fabe2024-05-22 17:21:47 -0700506
507
Luca Farsi70a53bd2024-08-07 17:29:16 -0700508OPTIMIZED_BUILD_TARGETS = {}
509OPTIMIZED_BUILD_TARGETS.update(GeneralTestsOptimizer.get_optimized_targets())