blob: 8256acd1b40243a1cfcd54c8d5081dcbfe7f02ea [file] [log] [blame]
Luca Farsib9c54642024-08-13 17:16:33 -07001# Copyright 2024, The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Tests for optimized_targets.py"""
16
17import json
18import logging
19import os
20import pathlib
21import re
Luca Farsi64598e82024-08-28 13:39:25 -070022import subprocess
23import textwrap
Luca Farsib9c54642024-08-13 17:16:33 -070024import unittest
25from unittest import mock
Luca Farsib9c54642024-08-13 17:16:33 -070026from build_context import BuildContext
Luca Farsi64598e82024-08-28 13:39:25 -070027import optimized_targets
Luca Farsib9c54642024-08-13 17:16:33 -070028from pyfakefs import fake_filesystem_unittest
Luca Farsi6a7c8932025-03-14 23:32:22 +000029import test_discovery_agent
Luca Farsib9c54642024-08-13 17:16:33 -070030
31
32class GeneralTestsOptimizerTest(fake_filesystem_unittest.TestCase):
33
34 def setUp(self):
35 self.setUpPyfakefs()
36
37 os_environ_patcher = mock.patch.dict('os.environ', {})
38 self.addCleanup(os_environ_patcher.stop)
39 self.mock_os_environ = os_environ_patcher.start()
40
41 self._setup_working_build_env()
Luca Farsib9c54642024-08-13 17:16:33 -070042 test_mapping_dir = pathlib.Path('/project/path/file/path')
43 test_mapping_dir.mkdir(parents=True)
Luca Farsib9c54642024-08-13 17:16:33 -070044
45 def _setup_working_build_env(self):
Luca Farsi64598e82024-08-28 13:39:25 -070046 self._write_soong_ui_file()
47 self._host_out_testcases = pathlib.Path('/tmp/top/host_out_testcases')
48 self._host_out_testcases.mkdir(parents=True)
49 self._target_out_testcases = pathlib.Path('/tmp/top/target_out_testcases')
50 self._target_out_testcases.mkdir(parents=True)
51 self._product_out = pathlib.Path('/tmp/top/product_out')
52 self._product_out.mkdir(parents=True)
53 self._soong_host_out = pathlib.Path('/tmp/top/soong_host_out')
54 self._soong_host_out.mkdir(parents=True)
55 self._host_out = pathlib.Path('/tmp/top/host_out')
56 self._host_out.mkdir(parents=True)
Luca Farsi6a7c8932025-03-14 23:32:22 +000057 self._write_general_tests_files_outputs()
Luca Farsi64598e82024-08-28 13:39:25 -070058
59 self._dist_dir = pathlib.Path('/tmp/top/out/dist')
60 self._dist_dir.mkdir(parents=True)
Luca Farsib9c54642024-08-13 17:16:33 -070061
62 self.mock_os_environ.update({
Luca Farsi64598e82024-08-28 13:39:25 -070063 'TOP': '/tmp/top',
64 'DIST_DIR': '/tmp/top/out/dist',
Luca Farsi6a7c8932025-03-14 23:32:22 +000065 'TMPDIR': '/tmp/'
Luca Farsib9c54642024-08-13 17:16:33 -070066 })
67
Luca Farsi64598e82024-08-28 13:39:25 -070068 def _write_soong_ui_file(self):
69 soong_path = pathlib.Path('/tmp/top/build/soong')
70 soong_path.mkdir(parents=True)
71 with open(os.path.join(soong_path, 'soong_ui.bash'), 'w') as f:
72 f.write("""
73 #/bin/bash
Luca Farsi64598e82024-08-28 13:39:25 -070074 echo PRODUCT_OUT='/tmp/top/product_out'
75 echo SOONG_HOST_OUT='/tmp/top/soong_host_out'
76 echo HOST_OUT='/tmp/top/host_out'
77 """)
78 os.chmod(os.path.join(soong_path, 'soong_ui.bash'), 0o666)
79
Luca Farsi6a7c8932025-03-14 23:32:22 +000080 def _write_general_tests_files_outputs(self):
81 with open(os.path.join(self._product_out, 'general-tests_files'), 'w') as f:
82 f.write("""
83 path/to/module_1/general-tests-host-file
84 path/to/module_1/general-tests-host-file.config
85 path/to/module_1/general-tests-target-file
86 path/to/module_1/general-tests-target-file.config
87 path/to/module_2/general-tests-host-file
88 path/to/module_2/general-tests-host-file.config
89 path/to/module_2/general-tests-target-file
90 path/to/module_2/general-tests-target-file.config
91 path/to/module_1/general-tests-host-file
92 path/to/module_1/general-tests-host-file.config
93 path/to/module_1/general-tests-target-file
94 path/to/module_1/general-tests-target-file.config
95 """)
96 with open(os.path.join(self._product_out, 'general-tests_host_files'), 'w') as f:
97 f.write("""
98 path/to/module_1/general-tests-host-file
99 path/to/module_1/general-tests-host-file.config
100 path/to/module_2/general-tests-host-file
101 path/to/module_2/general-tests-host-file.config
102 path/to/module_1/general-tests-host-file
103 path/to/module_1/general-tests-host-file.config
104 """)
105 with open(os.path.join(self._product_out, 'general-tests_target_files'), 'w') as f:
106 f.write("""
107 path/to/module_1/general-tests-target-file
108 path/to/module_1/general-tests-target-file.config
109 path/to/module_2/general-tests-target-file
110 path/to/module_2/general-tests-target-file.config
111 path/to/module_1/general-tests-target-file
112 path/to/module_1/general-tests-target-file.config
113 """)
Luca Farsi64598e82024-08-28 13:39:25 -0700114
Luca Farsib9c54642024-08-13 17:16:33 -0700115
Luca Farsi64598e82024-08-28 13:39:25 -0700116 @mock.patch('subprocess.run')
Luca Farsi6a7c8932025-03-14 23:32:22 +0000117 @mock.patch.object(test_discovery_agent.TestDiscoveryAgent, 'discover_test_mapping_test_modules')
118 def test_general_tests_optimized(self, discover_modules, subprocess_run):
Luca Farsi64598e82024-08-28 13:39:25 -0700119 subprocess_run.return_value = self._get_soong_vars_output()
Luca Farsi6a7c8932025-03-14 23:32:22 +0000120 discover_modules.return_value = (['module_1'], ['dependency_1'])
121
122 optimizer = self._create_general_tests_optimizer()
123
124 build_targets = optimizer.get_build_targets()
125
126 expected_build_targets = set(
127 optimized_targets.GeneralTestsOptimizer._REQUIRED_MODULES
128 )
129 expected_build_targets.add('module_1')
130
131 self.assertSetEqual(build_targets, expected_build_targets)
132
133 @mock.patch('subprocess.run')
134 @mock.patch.object(test_discovery_agent.TestDiscoveryAgent, 'discover_test_mapping_test_modules')
135 def test_module_unused_module_not_built(self, discover_modules, subprocess_run):
136 subprocess_run.return_value = self._get_soong_vars_output()
137 discover_modules.return_value = (['no_module'], ['dependency_1'])
138
139 optimizer = self._create_general_tests_optimizer()
140
141 build_targets = optimizer.get_build_targets()
142
143 expected_build_targets = set(
144 optimized_targets.GeneralTestsOptimizer._REQUIRED_MODULES
145 )
146 self.assertSetEqual(build_targets, expected_build_targets)
147
148 @mock.patch('subprocess.run')
149 @mock.patch.object(test_discovery_agent.TestDiscoveryAgent, 'discover_test_mapping_test_modules')
150 def test_packaging_outputs_success(self, discover_modules, subprocess_run):
151 subprocess_run.return_value = self._get_soong_vars_output()
152 discover_modules.return_value = (['module_1'], ['dependency_1'])
Luca Farsi64598e82024-08-28 13:39:25 -0700153 optimizer = self._create_general_tests_optimizer()
154 self._set_up_build_outputs(['test_mapping_module'])
Luca Farsib9c54642024-08-13 17:16:33 -0700155
Luca Farsi64598e82024-08-28 13:39:25 -0700156 targets = optimizer.get_build_targets()
157 package_commands = optimizer.get_package_outputs_commands()
Luca Farsib9c54642024-08-13 17:16:33 -0700158
Luca Farsi6a7c8932025-03-14 23:32:22 +0000159 self._verify_soong_zip_commands(package_commands, ['module_1'])
Luca Farsib9c54642024-08-13 17:16:33 -0700160
Luca Farsi64598e82024-08-28 13:39:25 -0700161 @mock.patch('subprocess.run')
162 def test_get_soong_dumpvars_fails_raises(self, subprocess_run):
163 subprocess_run.return_value = self._get_soong_vars_output(return_code=-1)
164 optimizer = self._create_general_tests_optimizer()
165 self._set_up_build_outputs(['test_mapping_module'])
Luca Farsib9c54642024-08-13 17:16:33 -0700166
Luca Farsi64598e82024-08-28 13:39:25 -0700167 with self.assertRaisesRegex(RuntimeError, 'Soong dumpvars failed!'):
Luca Farsi6a7c8932025-03-14 23:32:22 +0000168 targets = optimizer.get_build_targets()
Luca Farsi64598e82024-08-28 13:39:25 -0700169
170 @mock.patch('subprocess.run')
171 def test_get_soong_dumpvars_bad_output_raises(self, subprocess_run):
172 subprocess_run.return_value = self._get_soong_vars_output(
173 stdout='This output is bad'
174 )
175 optimizer = self._create_general_tests_optimizer()
176 self._set_up_build_outputs(['test_mapping_module'])
177
Luca Farsi64598e82024-08-28 13:39:25 -0700178 with self.assertRaisesRegex(
179 RuntimeError, 'Error parsing soong dumpvars output'
180 ):
Luca Farsi6a7c8932025-03-14 23:32:22 +0000181 targets = optimizer.get_build_targets()
Luca Farsi64598e82024-08-28 13:39:25 -0700182
Luca Farsi64598e82024-08-28 13:39:25 -0700183 def _create_general_tests_optimizer(self, build_context: BuildContext = None):
Luca Farsib9c54642024-08-13 17:16:33 -0700184 if not build_context:
185 build_context = self._create_build_context()
186 return optimized_targets.GeneralTestsOptimizer(
Luca Farsi6a7c8932025-03-14 23:32:22 +0000187 'general-tests', build_context, None, build_context.test_infos
Luca Farsib9c54642024-08-13 17:16:33 -0700188 )
189
190 def _create_build_context(
191 self,
192 general_tests_optimized: bool = True,
193 test_context: dict[str, any] = None,
194 ) -> BuildContext:
195 if not test_context:
196 test_context = self._create_test_context()
197 build_context_dict = {}
198 build_context_dict['enabledBuildFeatures'] = [{'name': 'optimized_build'}]
199 if general_tests_optimized:
Luca Farsi64598e82024-08-28 13:39:25 -0700200 build_context_dict['enabledBuildFeatures'].append(
201 {'name': 'general_tests_optimized'}
202 )
Luca Farsib9c54642024-08-13 17:16:33 -0700203 build_context_dict['testContext'] = test_context
204 return BuildContext(build_context_dict)
205
206 def _create_test_context(self):
207 return {
208 'testInfos': [
209 {
210 'name': 'atp_test',
211 'target': 'test_target',
212 'branch': 'branch',
213 'extraOptions': [
214 {
215 'key': 'additional-files-filter',
216 'values': ['general-tests.zip'],
217 },
218 {
219 'key': 'test-mapping-test-group',
220 'values': ['test-mapping-group'],
221 },
222 ],
223 'command': '/tf/command',
224 'extraBuildTargets': [
225 'extra_build_target',
226 ],
227 },
228 ],
229 }
230
Luca Farsi64598e82024-08-28 13:39:25 -0700231 def _get_soong_vars_output(
232 self, return_code: int = 0, stdout: str = ''
233 ) -> subprocess.CompletedProcess:
234 return_value = subprocess.CompletedProcess(args=[], returncode=return_code)
235 if not stdout:
236 stdout = textwrap.dedent(f"""\
Luca Farsi64598e82024-08-28 13:39:25 -0700237 PRODUCT_OUT='{self._product_out}'
238 SOONG_HOST_OUT='{self._soong_host_out}'
Luca Farsi6a7c8932025-03-14 23:32:22 +0000239 HOST_OUT='{self._host_out}'
240 """)
Luca Farsi64598e82024-08-28 13:39:25 -0700241
242 return_value.stdout = stdout
243 return return_value
244
245 def _set_up_build_outputs(self, targets: list[str]):
246 for target in targets:
247 host_dir = self._host_out_testcases / target
248 host_dir.mkdir()
249 (host_dir / f'{target}.config').touch()
250 (host_dir / f'test_file').touch()
251
252 target_dir = self._target_out_testcases / target
253 target_dir.mkdir()
254 (target_dir / f'{target}.config').touch()
255 (target_dir / f'test_file').touch()
256
257 def _verify_soong_zip_commands(self, commands: list[str], targets: list[str]):
258 """Verify the structure of the zip commands.
259
260 Zip commands have to start with the soong_zip binary path, then are followed
261 by a couple of options and the name of the file being zipped. Depending on
262 which zip we are creating look for a few essential items being added in
263 those zips.
264
265 Args:
266 commands: list of command lists
267 targets: list of targets expected to be in general-tests.zip
268 """
269 for command in commands:
270 self.assertEqual(
Luca Farsi8ea67422024-09-17 15:48:11 -0700271 '/tmp/top/prebuilts/build-tools/linux-x86/bin/soong_zip',
Luca Farsi64598e82024-08-28 13:39:25 -0700272 command[0],
273 )
274 self.assertEqual('-d', command[1])
275 self.assertEqual('-o', command[2])
276 match (command[3]):
277 case '/tmp/top/out/dist/general-tests_configs.zip':
278 self.assertIn(f'{self._host_out}/host_general-tests_list', command)
279 self.assertIn(
280 f'{self._product_out}/target_general-tests_list', command
281 )
282 return
283 case '/tmp/top/out/dist/general-tests_list.zip':
284 self.assertIn('-f', command)
285 self.assertIn(f'{self._host_out}/general-tests_list', command)
286 return
287 case '/tmp/top/out/dist/general-tests.zip':
288 for target in targets:
289 self.assertIn(f'{self._host_out_testcases}/{target}', command)
290 self.assertIn(f'{self._target_out_testcases}/{target}', command)
291 self.assertIn(
292 f'{self._soong_host_out}/framework/cts-tradefed.jar', command
293 )
294 self.assertIn(
295 f'{self._soong_host_out}/framework/compatibility-host-util.jar',
296 command,
297 )
298 self.assertIn(
299 f'{self._soong_host_out}/framework/vts-tradefed.jar', command
300 )
301 return
302 case _:
303 self.fail(f'malformed command: {command}')
304
Luca Farsib9c54642024-08-13 17:16:33 -0700305
306if __name__ == '__main__':
307 # Setup logging to be silent so unit tests can pass through TF.
308 logging.disable(logging.ERROR)
309 unittest.main()