Files
build/ci/optimized_targets_test.py
Luca Farsi 8ea6742d05 Fix packaging outputs commands
There were a few issues with the output packaging process that were
found during testing of the general-tests optimization.

First and foremost is that the packaging commands were trying to be
created before the build ran, when the outputs don't exist. I've changed
the logic to just collect the methods themselves which will then be run
during build plan execution after the build has completed.

A few other smaller issues include fixing the path to the soong_zip
binary, incorrect execution of the soong dumpvars command, and not
building the shared libs zip.

Test: atest build_test_suites_test; atest optimized_targets_test
Bug: 358215235
Change-Id: I8a3f54738f8bb5d871aadf7423844076c38b54a6
2024-09-19 11:01:06 -07:00

351 lines
12 KiB
Python

# Copyright 2024, The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for optimized_targets.py"""
import json
import logging
import os
import pathlib
import re
import subprocess
import textwrap
import unittest
from unittest import mock
from build_context import BuildContext
import optimized_targets
from pyfakefs import fake_filesystem_unittest
class GeneralTestsOptimizerTest(fake_filesystem_unittest.TestCase):
def setUp(self):
self.setUpPyfakefs()
os_environ_patcher = mock.patch.dict('os.environ', {})
self.addCleanup(os_environ_patcher.stop)
self.mock_os_environ = os_environ_patcher.start()
self._setup_working_build_env()
self._write_change_info_file()
test_mapping_dir = pathlib.Path('/project/path/file/path')
test_mapping_dir.mkdir(parents=True)
self._write_test_mapping_file()
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()
build_targets = optimizer.get_build_targets()
expected_build_targets = set(
optimized_targets.GeneralTestsOptimizer._REQUIRED_MODULES
)
expected_build_targets.add('test_mapping_module')
self.assertSetEqual(build_targets, expected_build_targets)
def test_no_change_info_no_optimization(self):
del os.environ['CHANGE_INFO']
optimizer = self._create_general_tests_optimizer()
build_targets = optimizer.get_build_targets()
self.assertSetEqual(build_targets, {'general-tests'})
def test_mapping_groups_unused_module_not_built(self):
test_context = self._create_test_context()
test_context['testInfos'][0]['extraOptions'] = [
{
'key': 'additional-files-filter',
'values': ['general-tests.zip'],
},
{
'key': 'test-mapping-test-group',
'values': ['unused-test-mapping-group'],
},
]
optimizer = self._create_general_tests_optimizer(
build_context=self._create_build_context(test_context=test_context)
)
build_targets = optimizer.get_build_targets()
expected_build_targets = set(
optimized_targets.GeneralTestsOptimizer._REQUIRED_MODULES
)
self.assertSetEqual(build_targets, expected_build_targets)
def test_general_tests_used_by_non_test_mapping_test_no_optimization(self):
test_context = self._create_test_context()
test_context['testInfos'][0]['extraOptions'] = [{
'key': 'additional-files-filter',
'values': ['general-tests.zip'],
}]
optimizer = self._create_general_tests_optimizer(
build_context=self._create_build_context(test_context=test_context)
)
build_targets = optimizer.get_build_targets()
self.assertSetEqual(build_targets, {'general-tests'})
def test_malformed_change_info_raises(self):
with open(self.change_info_file, 'w') as f:
f.write('not change info')
optimizer = self._create_general_tests_optimizer()
with self.assertRaises(json.decoder.JSONDecodeError):
build_targets = optimizer.get_build_targets()
def test_malformed_test_mapping_raises(self):
with open('/project/path/file/path/TEST_MAPPING', 'w') as f:
f.write('not test mapping')
optimizer = self._create_general_tests_optimizer()
with self.assertRaises(json.decoder.JSONDecodeError):
build_targets = optimizer.get_build_targets()
@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'])
targets = optimizer.get_build_targets()
package_commands = optimizer.get_package_outputs_commands()
self._verify_soong_zip_commands(package_commands, ['test_mapping_module'])
@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'])
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()
def _create_general_tests_optimizer(self, build_context: BuildContext = None):
if not build_context:
build_context = self._create_build_context()
return optimized_targets.GeneralTestsOptimizer(
'general-tests', build_context, None
)
def _create_build_context(
self,
general_tests_optimized: bool = True,
test_context: dict[str, any] = None,
) -> BuildContext:
if not test_context:
test_context = self._create_test_context()
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['testContext'] = test_context
return BuildContext(build_context_dict)
def _create_test_context(self):
return {
'testInfos': [
{
'name': 'atp_test',
'target': 'test_target',
'branch': 'branch',
'extraOptions': [
{
'key': 'additional-files-filter',
'values': ['general-tests.zip'],
},
{
'key': 'test-mapping-test-group',
'values': ['test-mapping-group'],
},
],
'command': '/tf/command',
'extraBuildTargets': [
'extra_build_target',
],
},
],
}
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/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.
logging.disable(logging.ERROR)
unittest.main()