blob: 0e9723cad8398c423b6e5fbc1bfafda74d8411f5 [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
210 def find_changed_files(self) -> set[str]:
211 changed_files = set()
212
213 for change in self._change_info_contents['changes']:
214 project_path = change.get('projectPath') + '/'
215
216 for revision in change.get('revisions'):
217 for file_info in revision.get('fileInfos'):
218 changed_files.add(project_path + file_info.get('path'))
219
220 return changed_files
221
Luca Farsid4e4b642024-09-10 16:37:51 -0700222
Luca Farsi70a53bd2024-08-07 17:29:16 -0700223class GeneralTestsOptimizer(OptimizedBuildTarget):
224 """general-tests optimizer
Luca Farsi040fabe2024-05-22 17:21:47 -0700225
Luca Farsi6a7c8932025-03-14 23:32:22 +0000226 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 -0700227 """
228
Luca Farsi8ea67422024-09-17 15:48:11 -0700229 # List of modules that are built alongside general-tests as dependencies.
230 _REQUIRED_MODULES = frozenset([
231 'cts-tradefed',
232 'vts-tradefed',
233 'compatibility-host-util',
Luca Farsi8ea67422024-09-17 15:48:11 -0700234 ])
Luca Farsib9c54642024-08-13 17:16:33 -0700235
236 def get_build_targets_impl(self) -> set[str]:
Luca Farsi6a7c8932025-03-14 23:32:22 +0000237 self._general_tests_outputs = self._get_general_tests_outputs()
238 test_modules = self._get_test_discovery_modules()
Luca Farsib9c54642024-08-13 17:16:33 -0700239
240 modules_to_build = set(self._REQUIRED_MODULES)
Luca Farsi6a7c8932025-03-14 23:32:22 +0000241 self._build_outputs = []
242 for module in test_modules:
243 module_outputs = [output for output in self._general_tests_outputs if module in output]
244 if module_outputs:
245 modules_to_build.add(module)
246 self._build_outputs.extend(module_outputs)
Luca Farsib9c54642024-08-13 17:16:33 -0700247
248 return modules_to_build
249
Luca Farsi6a7c8932025-03-14 23:32:22 +0000250 def _get_general_tests_outputs(self) -> list[str]:
251 src_top = pathlib.Path(os.environ.get('TOP', os.getcwd()))
252 soong_vars = self._query_soong_vars(
253 src_top,
254 [
255 'PRODUCT_OUT',
256 ],
257 )
258 product_out = pathlib.Path(soong_vars.get('PRODUCT_OUT'))
259 with open(f'{product_out / "general-tests_files"}') as general_tests_list_file:
260 general_tests_list = general_tests_list_file.readlines()
261 with open(f'{product_out / "general-tests_host_files"}') as general_tests_list_file:
262 self._general_tests_host_outputs = general_tests_list_file.readlines()
263 with open(f'{product_out / "general-tests_target_files"}') as general_tests_list_file:
264 self._general_tests_target_outputs = general_tests_list_file.readlines()
265 return general_tests_list
266
267
268 def _get_test_discovery_modules(self) -> set[str]:
269 test_modules = set()
270 for test_info in self.test_infos:
271 tf_command = self._build_tf_command(test_info)
272 discovery_agent = test_discovery_agent.TestDiscoveryAgent(tradefed_args=tf_command, test_mapping_zip_path=os.environ.get('DIST_DIR')+'/test_mappings.zip')
273 modules, dependencies = discovery_agent.discover_test_mapping_test_modules()
274 for regex in modules:
275 test_modules.add(regex)
276 return test_modules
277
278
279 def _build_tf_command(self, test_info) -> list[str]:
280 command = [test_info.command]
281 for extra_option in test_info.extra_options:
282 if not extra_option.get('key'):
283 continue
284 arg_key = '--' + extra_option.get('key')
285 if arg_key == '--build-id':
286 command.append(arg_key)
287 command.append(os.environ.get('BUILD_NUMBER'))
288 continue
289 if extra_option.get('values'):
290 for value in extra_option.get('values'):
291 command.append(arg_key)
292 command.append(value)
293 else:
294 command.append(arg_key)
295
296 return command
297
Luca Farsi64598e82024-08-28 13:39:25 -0700298 def get_package_outputs_commands_impl(self):
299 src_top = pathlib.Path(os.environ.get('TOP', os.getcwd()))
300 dist_dir = pathlib.Path(os.environ.get('DIST_DIR'))
Luca Farsi6a7c8932025-03-14 23:32:22 +0000301 tmp_dir = pathlib.Path(os.environ.get('TMPDIR'))
302 print(f'modules: {self.modules_to_build}')
Luca Farsi64598e82024-08-28 13:39:25 -0700303
Luca Farsi6a7c8932025-03-14 23:32:22 +0000304 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)]
305 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)]
306 host_config_files = [file for file in host_outputs if file.endswith('.config\n')]
307 target_config_files = [file for file in target_outputs if file.endswith('.config\n')]
308 logging.info(host_outputs)
309 logging.info(target_outputs)
310 with open(f"{tmp_dir / 'host.list'}", 'w') as host_list_file:
311 for output in host_outputs:
312 host_list_file.write(output)
313 with open(f"{tmp_dir / 'target.list'}", 'w') as target_list_file:
314 for output in target_outputs:
315 target_list_file.write(output)
Luca Farsi64598e82024-08-28 13:39:25 -0700316 soong_vars = self._query_soong_vars(
317 src_top,
318 [
Luca Farsi64598e82024-08-28 13:39:25 -0700319 'PRODUCT_OUT',
320 'SOONG_HOST_OUT',
321 'HOST_OUT',
322 ],
323 )
Luca Farsi64598e82024-08-28 13:39:25 -0700324 product_out = pathlib.Path(soong_vars.get('PRODUCT_OUT'))
325 soong_host_out = pathlib.Path(soong_vars.get('SOONG_HOST_OUT'))
326 host_out = pathlib.Path(soong_vars.get('HOST_OUT'))
Luca Farsi64598e82024-08-28 13:39:25 -0700327 zip_commands = []
328
329 zip_commands.extend(
330 self._get_zip_test_configs_zips_commands(
Luca Farsi8ea67422024-09-17 15:48:11 -0700331 src_top,
Luca Farsi64598e82024-08-28 13:39:25 -0700332 dist_dir,
333 host_out,
334 product_out,
335 host_config_files,
336 target_config_files,
337 )
338 )
339
Luca Farsi8ea67422024-09-17 15:48:11 -0700340 zip_command = self._base_zip_command(src_top, dist_dir, 'general-tests.zip')
Luca Farsi64598e82024-08-28 13:39:25 -0700341 # Add host testcases.
Luca Farsi6a7c8932025-03-14 23:32:22 +0000342 if host_outputs:
Luca Farsi8ea67422024-09-17 15:48:11 -0700343 zip_command.extend(
344 self._generate_zip_options_for_items(
345 prefix='host',
Luca Farsi6a7c8932025-03-14 23:32:22 +0000346 relative_root=str(host_out),
347 list_files=[f"{tmp_dir / 'host.list'}"],
Luca Farsi8ea67422024-09-17 15:48:11 -0700348 )
349 )
Luca Farsi64598e82024-08-28 13:39:25 -0700350
351 # Add target testcases.
Luca Farsi6a7c8932025-03-14 23:32:22 +0000352 if target_outputs:
Luca Farsi8ea67422024-09-17 15:48:11 -0700353 zip_command.extend(
354 self._generate_zip_options_for_items(
355 prefix='target',
Luca Farsi6a7c8932025-03-14 23:32:22 +0000356 relative_root=str(product_out),
357 list_files=[f"{tmp_dir / 'target.list'}"],
Luca Farsi8ea67422024-09-17 15:48:11 -0700358 )
359 )
Luca Farsi64598e82024-08-28 13:39:25 -0700360
361 # TODO(lucafarsi): Push this logic into a general-tests-minimal build command
362 # Add necessary tools. These are also hardcoded in general-tests.mk.
363 framework_path = soong_host_out / 'framework'
364
365 zip_command.extend(
366 self._generate_zip_options_for_items(
367 prefix='host/tools',
368 relative_root=str(framework_path),
369 files=[
370 f"{framework_path / 'cts-tradefed.jar'}",
371 f"{framework_path / 'compatibility-host-util.jar'}",
372 f"{framework_path / 'vts-tradefed.jar'}",
373 ],
374 )
375 )
376
Luca Farsi6a7c8932025-03-14 23:32:22 +0000377 zip_command.append('-sha256')
378
Luca Farsi64598e82024-08-28 13:39:25 -0700379 zip_commands.append(zip_command)
380 return zip_commands
381
Luca Farsi64598e82024-08-28 13:39:25 -0700382 def _get_zip_test_configs_zips_commands(
383 self,
Luca Farsi8ea67422024-09-17 15:48:11 -0700384 src_top: pathlib.Path,
Luca Farsi64598e82024-08-28 13:39:25 -0700385 dist_dir: pathlib.Path,
386 host_out: pathlib.Path,
387 product_out: pathlib.Path,
388 host_config_files: list[str],
389 target_config_files: list[str],
390 ) -> tuple[list[str], list[str]]:
391 """Generate general-tests_configs.zip and general-tests_list.zip.
392
393 general-tests_configs.zip contains all of the .config files that were
394 built and general-tests_list.zip contains a text file which lists
395 all of the .config files that are in general-tests_configs.zip.
396
397 general-tests_configs.zip is organized as follows:
398 /
399 host/
400 testcases/
401 test_1.config
402 test_2.config
403 ...
404 target/
405 testcases/
406 test_1.config
407 test_2.config
408 ...
409
410 So the process is we write out the paths to all the host config files into
411 one
412 file and all the paths to the target config files in another. We also write
413 the paths to all the config files into a third file to use for
414 general-tests_list.zip.
415
416 Args:
417 dist_dir: dist directory.
418 host_out: host out directory.
419 product_out: product out directory.
420 host_config_files: list of all host config files.
421 target_config_files: list of all target config files.
422
423 Returns:
424 The commands to generate general-tests_configs.zip and
425 general-tests_list.zip
426 """
427 with open(
428 f"{host_out / 'host_general-tests_list'}", 'w'
429 ) as host_list_file, open(
430 f"{product_out / 'target_general-tests_list'}", 'w'
431 ) as target_list_file, open(
432 f"{host_out / 'general-tests_list'}", 'w'
433 ) as list_file:
434
435 for config_file in host_config_files:
436 host_list_file.write(f'{config_file}' + '\n')
437 list_file.write('host/' + os.path.relpath(config_file, host_out) + '\n')
438
439 for config_file in target_config_files:
440 target_list_file.write(f'{config_file}' + '\n')
441 list_file.write(
442 'target/' + os.path.relpath(config_file, product_out) + '\n'
443 )
444
445 zip_commands = []
446
447 tests_config_zip_command = self._base_zip_command(
Luca Farsi8ea67422024-09-17 15:48:11 -0700448 src_top, dist_dir, 'general-tests_configs.zip'
Luca Farsi64598e82024-08-28 13:39:25 -0700449 )
450 tests_config_zip_command.extend(
451 self._generate_zip_options_for_items(
452 prefix='host',
453 relative_root=str(host_out),
454 list_files=[f"{host_out / 'host_general-tests_list'}"],
455 )
456 )
457
458 tests_config_zip_command.extend(
459 self._generate_zip_options_for_items(
460 prefix='target',
461 relative_root=str(product_out),
Luca Farsi8ea67422024-09-17 15:48:11 -0700462 list_files=[f"{product_out / 'target_general-tests_list'}"],
Luca Farsi64598e82024-08-28 13:39:25 -0700463 ),
464 )
465
466 zip_commands.append(tests_config_zip_command)
467
468 tests_list_zip_command = self._base_zip_command(
Luca Farsi8ea67422024-09-17 15:48:11 -0700469 src_top, dist_dir, 'general-tests_list.zip'
Luca Farsi64598e82024-08-28 13:39:25 -0700470 )
471 tests_list_zip_command.extend(
472 self._generate_zip_options_for_items(
473 relative_root=str(host_out),
474 files=[f"{host_out / 'general-tests_list'}"],
475 )
476 )
477 zip_commands.append(tests_list_zip_command)
478
479 return zip_commands
480
Luca Farsi70a53bd2024-08-07 17:29:16 -0700481 def get_enabled_flag(self):
Luca Farsib9c54642024-08-13 17:16:33 -0700482 return 'general_tests_optimized'
Luca Farsi70a53bd2024-08-07 17:29:16 -0700483
484 @classmethod
485 def get_optimized_targets(cls) -> dict[str, OptimizedBuildTarget]:
486 return {'general-tests': functools.partial(cls)}
Luca Farsi040fabe2024-05-22 17:21:47 -0700487
488
Luca Farsi70a53bd2024-08-07 17:29:16 -0700489OPTIMIZED_BUILD_TARGETS = {}
490OPTIMIZED_BUILD_TARGETS.update(GeneralTestsOptimizer.get_optimized_targets())