Implement package_outputs in GeneralTestsOptimizer
Implement the output packaging step in the GeneralTestsOptimizer. This
step takes all the outputs built by the general-tests target (in the
case where it was optimized) and packages them into the necessary zips
generated by the target normally.
Test: atest build_test_suites_test; atest optimized_targets_test
Bug: 358215235
Change-Id: I5d27eef4e37137cc9b6e235f52f3856ba0b30460
diff --git a/ci/optimized_targets_test.py b/ci/optimized_targets_test.py
index 919c193..762b62e 100644
--- a/ci/optimized_targets_test.py
+++ b/ci/optimized_targets_test.py
@@ -19,10 +19,12 @@
import os
import pathlib
import re
+import subprocess
+import textwrap
import unittest
from unittest import mock
-import optimized_targets
from build_context import BuildContext
+import optimized_targets
from pyfakefs import fake_filesystem_unittest
@@ -43,11 +45,68 @@
def _setup_working_build_env(self):
self.change_info_file = pathlib.Path('/tmp/change_info')
+ self._write_soong_ui_file()
+ self._host_out_testcases = pathlib.Path('/tmp/top/host_out_testcases')
+ self._host_out_testcases.mkdir(parents=True)
+ self._target_out_testcases = pathlib.Path('/tmp/top/target_out_testcases')
+ self._target_out_testcases.mkdir(parents=True)
+ self._product_out = pathlib.Path('/tmp/top/product_out')
+ self._product_out.mkdir(parents=True)
+ self._soong_host_out = pathlib.Path('/tmp/top/soong_host_out')
+ self._soong_host_out.mkdir(parents=True)
+ self._host_out = pathlib.Path('/tmp/top/host_out')
+ self._host_out.mkdir(parents=True)
+
+ self._dist_dir = pathlib.Path('/tmp/top/out/dist')
+ self._dist_dir.mkdir(parents=True)
self.mock_os_environ.update({
'CHANGE_INFO': str(self.change_info_file),
+ 'TOP': '/tmp/top',
+ 'DIST_DIR': '/tmp/top/out/dist',
})
+ def _write_soong_ui_file(self):
+ soong_path = pathlib.Path('/tmp/top/build/soong')
+ soong_path.mkdir(parents=True)
+ with open(os.path.join(soong_path, 'soong_ui.bash'), 'w') as f:
+ f.write("""
+ #/bin/bash
+ echo HOST_OUT_TESTCASES='/tmp/top/host_out_testcases'
+ echo TARGET_OUT_TESTCASES='/tmp/top/target_out_testcases'
+ echo PRODUCT_OUT='/tmp/top/product_out'
+ echo SOONG_HOST_OUT='/tmp/top/soong_host_out'
+ echo HOST_OUT='/tmp/top/host_out'
+ """)
+ os.chmod(os.path.join(soong_path, 'soong_ui.bash'), 0o666)
+
+ def _write_change_info_file(self):
+ change_info_contents = {
+ 'changes': [{
+ 'projectPath': '/project/path',
+ 'revisions': [{
+ 'fileInfos': [{
+ 'path': 'file/path/file_name',
+ }],
+ }],
+ }]
+ }
+
+ with open(self.change_info_file, 'w') as f:
+ json.dump(change_info_contents, f)
+
+ def _write_test_mapping_file(self):
+ test_mapping_contents = {
+ 'test-mapping-group': [
+ {
+ 'name': 'test_mapping_module',
+ },
+ ],
+ }
+
+ with open('/project/path/file/path/TEST_MAPPING', 'w') as f:
+ json.dump(test_mapping_contents, f)
+
def test_general_tests_optimized(self):
optimizer = self._create_general_tests_optimizer()
@@ -124,36 +183,56 @@
with self.assertRaises(json.decoder.JSONDecodeError):
build_targets = optimizer.get_build_targets()
- def _write_change_info_file(self):
- change_info_contents = {
- 'changes': [{
- 'projectPath': '/project/path',
- 'revisions': [{
- 'fileInfos': [{
- 'path': 'file/path/file_name',
- }],
- }],
- }]
- }
+ @mock.patch('subprocess.run')
+ def test_packaging_outputs_success(self, subprocess_run):
+ subprocess_run.return_value = self._get_soong_vars_output()
+ optimizer = self._create_general_tests_optimizer()
+ self._set_up_build_outputs(['test_mapping_module'])
- with open(self.change_info_file, 'w') as f:
- json.dump(change_info_contents, f)
+ targets = optimizer.get_build_targets()
+ package_commands = optimizer.get_package_outputs_commands()
- def _write_test_mapping_file(self):
- test_mapping_contents = {
- 'test-mapping-group': [
- {
- 'name': 'test_mapping_module',
- },
- ],
- }
+ self._verify_soong_zip_commands(package_commands, ['test_mapping_module'])
- with open('/project/path/file/path/TEST_MAPPING', 'w') as f:
- json.dump(test_mapping_contents, f)
+ @mock.patch('subprocess.run')
+ def test_get_soong_dumpvars_fails_raises(self, subprocess_run):
+ subprocess_run.return_value = self._get_soong_vars_output(return_code=-1)
+ optimizer = self._create_general_tests_optimizer()
+ self._set_up_build_outputs(['test_mapping_module'])
- def _create_general_tests_optimizer(
- self, build_context: BuildContext = None
- ):
+ targets = optimizer.get_build_targets()
+
+ with self.assertRaisesRegex(RuntimeError, 'Soong dumpvars failed!'):
+ package_commands = optimizer.get_package_outputs_commands()
+
+ @mock.patch('subprocess.run')
+ def test_get_soong_dumpvars_bad_output_raises(self, subprocess_run):
+ subprocess_run.return_value = self._get_soong_vars_output(
+ stdout='This output is bad'
+ )
+ optimizer = self._create_general_tests_optimizer()
+ self._set_up_build_outputs(['test_mapping_module'])
+
+ targets = optimizer.get_build_targets()
+
+ with self.assertRaisesRegex(
+ RuntimeError, 'Error parsing soong dumpvars output'
+ ):
+ package_commands = optimizer.get_package_outputs_commands()
+
+ @mock.patch('subprocess.run')
+ def test_no_build_outputs_packaging_fails(self, subprocess_run):
+ subprocess_run.return_value = self._get_soong_vars_output()
+ optimizer = self._create_general_tests_optimizer()
+
+ targets = optimizer.get_build_targets()
+
+ with self.assertRaisesRegex(
+ RuntimeError, 'No items specified to be added to zip'
+ ):
+ package_commands = optimizer.get_package_outputs_commands()
+
+ def _create_general_tests_optimizer(self, build_context: BuildContext = None):
if not build_context:
build_context = self._create_build_context()
return optimized_targets.GeneralTestsOptimizer(
@@ -170,7 +249,9 @@
build_context_dict = {}
build_context_dict['enabledBuildFeatures'] = [{'name': 'optimized_build'}]
if general_tests_optimized:
- build_context_dict['enabledBuildFeatures'].append({'name': 'general_tests_optimized'})
+ build_context_dict['enabledBuildFeatures'].append(
+ {'name': 'general_tests_optimized'}
+ )
build_context_dict['testContext'] = test_context
return BuildContext(build_context_dict)
@@ -199,6 +280,81 @@
],
}
+ def _get_soong_vars_output(
+ self, return_code: int = 0, stdout: str = ''
+ ) -> subprocess.CompletedProcess:
+ return_value = subprocess.CompletedProcess(args=[], returncode=return_code)
+ if not stdout:
+ stdout = textwrap.dedent(f"""\
+ HOST_OUT_TESTCASES='{self._host_out_testcases}'
+ TARGET_OUT_TESTCASES='{self._target_out_testcases}'
+ PRODUCT_OUT='{self._product_out}'
+ SOONG_HOST_OUT='{self._soong_host_out}'
+ HOST_OUT='{self._host_out}'""")
+
+ return_value.stdout = stdout
+ return return_value
+
+ def _set_up_build_outputs(self, targets: list[str]):
+ for target in targets:
+ host_dir = self._host_out_testcases / target
+ host_dir.mkdir()
+ (host_dir / f'{target}.config').touch()
+ (host_dir / f'test_file').touch()
+
+ target_dir = self._target_out_testcases / target
+ target_dir.mkdir()
+ (target_dir / f'{target}.config').touch()
+ (target_dir / f'test_file').touch()
+
+ def _verify_soong_zip_commands(self, commands: list[str], targets: list[str]):
+ """Verify the structure of the zip commands.
+
+ Zip commands have to start with the soong_zip binary path, then are followed
+ by a couple of options and the name of the file being zipped. Depending on
+ which zip we are creating look for a few essential items being added in
+ those zips.
+
+ Args:
+ commands: list of command lists
+ targets: list of targets expected to be in general-tests.zip
+ """
+ for command in commands:
+ self.assertEqual(
+ '/tmp/top/host_out/prebuilts/build-tools/linux-x86/bin/soong_zip',
+ command[0],
+ )
+ self.assertEqual('-d', command[1])
+ self.assertEqual('-o', command[2])
+ match (command[3]):
+ case '/tmp/top/out/dist/general-tests_configs.zip':
+ self.assertIn(f'{self._host_out}/host_general-tests_list', command)
+ self.assertIn(
+ f'{self._product_out}/target_general-tests_list', command
+ )
+ return
+ case '/tmp/top/out/dist/general-tests_list.zip':
+ self.assertIn('-f', command)
+ self.assertIn(f'{self._host_out}/general-tests_list', command)
+ return
+ case '/tmp/top/out/dist/general-tests.zip':
+ for target in targets:
+ self.assertIn(f'{self._host_out_testcases}/{target}', command)
+ self.assertIn(f'{self._target_out_testcases}/{target}', command)
+ self.assertIn(
+ f'{self._soong_host_out}/framework/cts-tradefed.jar', command
+ )
+ self.assertIn(
+ f'{self._soong_host_out}/framework/compatibility-host-util.jar',
+ command,
+ )
+ self.assertIn(
+ f'{self._soong_host_out}/framework/vts-tradefed.jar', command
+ )
+ return
+ case _:
+ self.fail(f'malformed command: {command}')
+
if __name__ == '__main__':
# Setup logging to be silent so unit tests can pass through TF.