xref: /aosp_15_r20/build/make/ci/build_test_suites_test.py (revision 9e94795a3d4ef5c1d47486f9a02bb378756cea8a)
1*9e94795aSAndroid Build Coastguard Worker# Copyright 2024, The Android Open Source Project
2*9e94795aSAndroid Build Coastguard Worker#
3*9e94795aSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
4*9e94795aSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
5*9e94795aSAndroid Build Coastguard Worker# You may obtain a copy of the License at
6*9e94795aSAndroid Build Coastguard Worker#
7*9e94795aSAndroid Build Coastguard Worker#     http://www.apache.org/licenses/LICENSE-2.0
8*9e94795aSAndroid Build Coastguard Worker#
9*9e94795aSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
10*9e94795aSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
11*9e94795aSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*9e94795aSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
13*9e94795aSAndroid Build Coastguard Worker# limitations under the License.
14*9e94795aSAndroid Build Coastguard Worker
15*9e94795aSAndroid Build Coastguard Worker"""Tests for build_test_suites.py"""
16*9e94795aSAndroid Build Coastguard Worker
17*9e94795aSAndroid Build Coastguard Workerimport argparse
18*9e94795aSAndroid Build Coastguard Workerimport functools
19*9e94795aSAndroid Build Coastguard Workerfrom importlib import resources
20*9e94795aSAndroid Build Coastguard Workerimport json
21*9e94795aSAndroid Build Coastguard Workerimport multiprocessing
22*9e94795aSAndroid Build Coastguard Workerimport os
23*9e94795aSAndroid Build Coastguard Workerimport pathlib
24*9e94795aSAndroid Build Coastguard Workerimport shutil
25*9e94795aSAndroid Build Coastguard Workerimport signal
26*9e94795aSAndroid Build Coastguard Workerimport stat
27*9e94795aSAndroid Build Coastguard Workerimport subprocess
28*9e94795aSAndroid Build Coastguard Workerimport sys
29*9e94795aSAndroid Build Coastguard Workerimport tempfile
30*9e94795aSAndroid Build Coastguard Workerimport textwrap
31*9e94795aSAndroid Build Coastguard Workerimport time
32*9e94795aSAndroid Build Coastguard Workerfrom typing import Callable
33*9e94795aSAndroid Build Coastguard Workerimport unittest
34*9e94795aSAndroid Build Coastguard Workerfrom unittest import mock
35*9e94795aSAndroid Build Coastguard Workerfrom build_context import BuildContext
36*9e94795aSAndroid Build Coastguard Workerimport build_test_suites
37*9e94795aSAndroid Build Coastguard Workerimport ci_test_lib
38*9e94795aSAndroid Build Coastguard Workerimport optimized_targets
39*9e94795aSAndroid Build Coastguard Workerfrom pyfakefs import fake_filesystem_unittest
40*9e94795aSAndroid Build Coastguard Workerimport metrics_agent
41*9e94795aSAndroid Build Coastguard Workerimport test_discovery_agent
42*9e94795aSAndroid Build Coastguard Worker
43*9e94795aSAndroid Build Coastguard Worker
44*9e94795aSAndroid Build Coastguard Workerclass BuildTestSuitesTest(fake_filesystem_unittest.TestCase):
45*9e94795aSAndroid Build Coastguard Worker
46*9e94795aSAndroid Build Coastguard Worker  def setUp(self):
47*9e94795aSAndroid Build Coastguard Worker    self.setUpPyfakefs()
48*9e94795aSAndroid Build Coastguard Worker
49*9e94795aSAndroid Build Coastguard Worker    os_environ_patcher = mock.patch.dict('os.environ', {})
50*9e94795aSAndroid Build Coastguard Worker    self.addCleanup(os_environ_patcher.stop)
51*9e94795aSAndroid Build Coastguard Worker    self.mock_os_environ = os_environ_patcher.start()
52*9e94795aSAndroid Build Coastguard Worker
53*9e94795aSAndroid Build Coastguard Worker    subprocess_run_patcher = mock.patch('subprocess.run')
54*9e94795aSAndroid Build Coastguard Worker    self.addCleanup(subprocess_run_patcher.stop)
55*9e94795aSAndroid Build Coastguard Worker    self.mock_subprocess_run = subprocess_run_patcher.start()
56*9e94795aSAndroid Build Coastguard Worker
57*9e94795aSAndroid Build Coastguard Worker    metrics_agent_finalize_patcher = mock.patch('metrics_agent.MetricsAgent.end_reporting')
58*9e94795aSAndroid Build Coastguard Worker    self.addCleanup(metrics_agent_finalize_patcher.stop)
59*9e94795aSAndroid Build Coastguard Worker    self.mock_metrics_agent_end = metrics_agent_finalize_patcher.start()
60*9e94795aSAndroid Build Coastguard Worker
61*9e94795aSAndroid Build Coastguard Worker    self._setup_working_build_env()
62*9e94795aSAndroid Build Coastguard Worker
63*9e94795aSAndroid Build Coastguard Worker  def test_missing_target_release_env_var_raises(self):
64*9e94795aSAndroid Build Coastguard Worker    del os.environ['TARGET_RELEASE']
65*9e94795aSAndroid Build Coastguard Worker
66*9e94795aSAndroid Build Coastguard Worker    with self.assert_raises_word(build_test_suites.Error, 'TARGET_RELEASE'):
67*9e94795aSAndroid Build Coastguard Worker      build_test_suites.main([])
68*9e94795aSAndroid Build Coastguard Worker
69*9e94795aSAndroid Build Coastguard Worker  def test_missing_target_product_env_var_raises(self):
70*9e94795aSAndroid Build Coastguard Worker    del os.environ['TARGET_PRODUCT']
71*9e94795aSAndroid Build Coastguard Worker
72*9e94795aSAndroid Build Coastguard Worker    with self.assert_raises_word(build_test_suites.Error, 'TARGET_PRODUCT'):
73*9e94795aSAndroid Build Coastguard Worker      build_test_suites.main([])
74*9e94795aSAndroid Build Coastguard Worker
75*9e94795aSAndroid Build Coastguard Worker  def test_missing_top_env_var_raises(self):
76*9e94795aSAndroid Build Coastguard Worker    del os.environ['TOP']
77*9e94795aSAndroid Build Coastguard Worker
78*9e94795aSAndroid Build Coastguard Worker    with self.assert_raises_word(build_test_suites.Error, 'TOP'):
79*9e94795aSAndroid Build Coastguard Worker      build_test_suites.main([])
80*9e94795aSAndroid Build Coastguard Worker
81*9e94795aSAndroid Build Coastguard Worker  def test_missing_dist_dir_env_var_raises(self):
82*9e94795aSAndroid Build Coastguard Worker    del os.environ['DIST_DIR']
83*9e94795aSAndroid Build Coastguard Worker
84*9e94795aSAndroid Build Coastguard Worker    with self.assert_raises_word(build_test_suites.Error, 'DIST_DIR'):
85*9e94795aSAndroid Build Coastguard Worker      build_test_suites.main([])
86*9e94795aSAndroid Build Coastguard Worker
87*9e94795aSAndroid Build Coastguard Worker  def test_invalid_arg_raises(self):
88*9e94795aSAndroid Build Coastguard Worker    invalid_args = ['--invalid_arg']
89*9e94795aSAndroid Build Coastguard Worker
90*9e94795aSAndroid Build Coastguard Worker    with self.assertRaisesRegex(SystemExit, '2'):
91*9e94795aSAndroid Build Coastguard Worker      build_test_suites.main(invalid_args)
92*9e94795aSAndroid Build Coastguard Worker
93*9e94795aSAndroid Build Coastguard Worker  def test_build_failure_returns(self):
94*9e94795aSAndroid Build Coastguard Worker    self.mock_subprocess_run.side_effect = subprocess.CalledProcessError(
95*9e94795aSAndroid Build Coastguard Worker        42, None
96*9e94795aSAndroid Build Coastguard Worker    )
97*9e94795aSAndroid Build Coastguard Worker
98*9e94795aSAndroid Build Coastguard Worker    with self.assertRaisesRegex(SystemExit, '42'):
99*9e94795aSAndroid Build Coastguard Worker      build_test_suites.main([])
100*9e94795aSAndroid Build Coastguard Worker
101*9e94795aSAndroid Build Coastguard Worker  def test_incorrectly_formatted_build_context_raises(self):
102*9e94795aSAndroid Build Coastguard Worker    build_context = self.fake_top.joinpath('build_context')
103*9e94795aSAndroid Build Coastguard Worker    build_context.touch()
104*9e94795aSAndroid Build Coastguard Worker    os.environ['BUILD_CONTEXT'] = str(build_context)
105*9e94795aSAndroid Build Coastguard Worker
106*9e94795aSAndroid Build Coastguard Worker    with self.assert_raises_word(build_test_suites.Error, 'JSON'):
107*9e94795aSAndroid Build Coastguard Worker      build_test_suites.main([])
108*9e94795aSAndroid Build Coastguard Worker
109*9e94795aSAndroid Build Coastguard Worker  def test_build_success_returns(self):
110*9e94795aSAndroid Build Coastguard Worker    with self.assertRaisesRegex(SystemExit, '0'):
111*9e94795aSAndroid Build Coastguard Worker      build_test_suites.main([])
112*9e94795aSAndroid Build Coastguard Worker
113*9e94795aSAndroid Build Coastguard Worker  def assert_raises_word(self, cls, word):
114*9e94795aSAndroid Build Coastguard Worker    return self.assertRaisesRegex(cls, rf'\b{word}\b')
115*9e94795aSAndroid Build Coastguard Worker
116*9e94795aSAndroid Build Coastguard Worker  def _setup_working_build_env(self):
117*9e94795aSAndroid Build Coastguard Worker    self.fake_top = pathlib.Path('/fake/top')
118*9e94795aSAndroid Build Coastguard Worker    self.fake_top.mkdir(parents=True)
119*9e94795aSAndroid Build Coastguard Worker
120*9e94795aSAndroid Build Coastguard Worker    self.soong_ui_dir = self.fake_top.joinpath('build/soong')
121*9e94795aSAndroid Build Coastguard Worker    self.soong_ui_dir.mkdir(parents=True, exist_ok=True)
122*9e94795aSAndroid Build Coastguard Worker
123*9e94795aSAndroid Build Coastguard Worker    self.logs_dir = self.fake_top.joinpath('dist/logs')
124*9e94795aSAndroid Build Coastguard Worker    self.logs_dir.mkdir(parents=True, exist_ok=True)
125*9e94795aSAndroid Build Coastguard Worker
126*9e94795aSAndroid Build Coastguard Worker    self.soong_ui = self.soong_ui_dir.joinpath('soong_ui.bash')
127*9e94795aSAndroid Build Coastguard Worker    self.soong_ui.touch()
128*9e94795aSAndroid Build Coastguard Worker
129*9e94795aSAndroid Build Coastguard Worker    self.mock_os_environ.update({
130*9e94795aSAndroid Build Coastguard Worker        'TARGET_RELEASE': 'release',
131*9e94795aSAndroid Build Coastguard Worker        'TARGET_PRODUCT': 'product',
132*9e94795aSAndroid Build Coastguard Worker        'TOP': str(self.fake_top),
133*9e94795aSAndroid Build Coastguard Worker        'DIST_DIR': str(self.fake_top.joinpath('dist')),
134*9e94795aSAndroid Build Coastguard Worker    })
135*9e94795aSAndroid Build Coastguard Worker
136*9e94795aSAndroid Build Coastguard Worker    self.mock_subprocess_run.return_value = 0
137*9e94795aSAndroid Build Coastguard Worker
138*9e94795aSAndroid Build Coastguard Worker
139*9e94795aSAndroid Build Coastguard Workerclass RunCommandIntegrationTest(ci_test_lib.TestCase):
140*9e94795aSAndroid Build Coastguard Worker
141*9e94795aSAndroid Build Coastguard Worker  def setUp(self):
142*9e94795aSAndroid Build Coastguard Worker    self.temp_dir = ci_test_lib.TestTemporaryDirectory.create(self)
143*9e94795aSAndroid Build Coastguard Worker
144*9e94795aSAndroid Build Coastguard Worker    # Copy the Python executable from 'non-code' resources and make it
145*9e94795aSAndroid Build Coastguard Worker    # executable for use by tests that launch a subprocess. Note that we don't
146*9e94795aSAndroid Build Coastguard Worker    # use Python's native `sys.executable` property since that is not set when
147*9e94795aSAndroid Build Coastguard Worker    # running via the embedded launcher.
148*9e94795aSAndroid Build Coastguard Worker    base_name = 'py3-cmd'
149*9e94795aSAndroid Build Coastguard Worker    dest_file = self.temp_dir.joinpath(base_name)
150*9e94795aSAndroid Build Coastguard Worker    with resources.as_file(
151*9e94795aSAndroid Build Coastguard Worker        resources.files('testdata').joinpath(base_name)
152*9e94795aSAndroid Build Coastguard Worker    ) as p:
153*9e94795aSAndroid Build Coastguard Worker      shutil.copy(p, dest_file)
154*9e94795aSAndroid Build Coastguard Worker    dest_file.chmod(dest_file.stat().st_mode | stat.S_IEXEC)
155*9e94795aSAndroid Build Coastguard Worker    self.python_executable = dest_file
156*9e94795aSAndroid Build Coastguard Worker
157*9e94795aSAndroid Build Coastguard Worker    self._managed_processes = []
158*9e94795aSAndroid Build Coastguard Worker
159*9e94795aSAndroid Build Coastguard Worker  def tearDown(self):
160*9e94795aSAndroid Build Coastguard Worker    self._terminate_managed_processes()
161*9e94795aSAndroid Build Coastguard Worker
162*9e94795aSAndroid Build Coastguard Worker  def test_raises_on_nonzero_exit(self):
163*9e94795aSAndroid Build Coastguard Worker    with self.assertRaises(Exception):
164*9e94795aSAndroid Build Coastguard Worker      build_test_suites.run_command([
165*9e94795aSAndroid Build Coastguard Worker          self.python_executable,
166*9e94795aSAndroid Build Coastguard Worker          '-c',
167*9e94795aSAndroid Build Coastguard Worker          textwrap.dedent(f"""\
168*9e94795aSAndroid Build Coastguard Worker              import sys
169*9e94795aSAndroid Build Coastguard Worker              sys.exit(1)
170*9e94795aSAndroid Build Coastguard Worker              """),
171*9e94795aSAndroid Build Coastguard Worker      ])
172*9e94795aSAndroid Build Coastguard Worker
173*9e94795aSAndroid Build Coastguard Worker  def test_streams_stdout(self):
174*9e94795aSAndroid Build Coastguard Worker
175*9e94795aSAndroid Build Coastguard Worker    def run_slow_command(stdout_file, marker):
176*9e94795aSAndroid Build Coastguard Worker      with open(stdout_file, 'w') as f:
177*9e94795aSAndroid Build Coastguard Worker        build_test_suites.run_command(
178*9e94795aSAndroid Build Coastguard Worker            [
179*9e94795aSAndroid Build Coastguard Worker                self.python_executable,
180*9e94795aSAndroid Build Coastguard Worker                '-c',
181*9e94795aSAndroid Build Coastguard Worker                textwrap.dedent(f"""\
182*9e94795aSAndroid Build Coastguard Worker                  import time
183*9e94795aSAndroid Build Coastguard Worker
184*9e94795aSAndroid Build Coastguard Worker                  print('{marker}', end='', flush=True)
185*9e94795aSAndroid Build Coastguard Worker
186*9e94795aSAndroid Build Coastguard Worker                  # Keep process alive until we check stdout.
187*9e94795aSAndroid Build Coastguard Worker                  time.sleep(10)
188*9e94795aSAndroid Build Coastguard Worker                  """),
189*9e94795aSAndroid Build Coastguard Worker            ],
190*9e94795aSAndroid Build Coastguard Worker            stdout=f,
191*9e94795aSAndroid Build Coastguard Worker        )
192*9e94795aSAndroid Build Coastguard Worker
193*9e94795aSAndroid Build Coastguard Worker    marker = 'Spinach'
194*9e94795aSAndroid Build Coastguard Worker    stdout_file = self.temp_dir.joinpath('stdout.txt')
195*9e94795aSAndroid Build Coastguard Worker
196*9e94795aSAndroid Build Coastguard Worker    p = self.start_process(target=run_slow_command, args=[stdout_file, marker])
197*9e94795aSAndroid Build Coastguard Worker
198*9e94795aSAndroid Build Coastguard Worker    self.assert_file_eventually_contains(stdout_file, marker)
199*9e94795aSAndroid Build Coastguard Worker
200*9e94795aSAndroid Build Coastguard Worker  def test_propagates_interruptions(self):
201*9e94795aSAndroid Build Coastguard Worker
202*9e94795aSAndroid Build Coastguard Worker    def run(pid_file):
203*9e94795aSAndroid Build Coastguard Worker      build_test_suites.run_command([
204*9e94795aSAndroid Build Coastguard Worker          self.python_executable,
205*9e94795aSAndroid Build Coastguard Worker          '-c',
206*9e94795aSAndroid Build Coastguard Worker          textwrap.dedent(f"""\
207*9e94795aSAndroid Build Coastguard Worker              import os
208*9e94795aSAndroid Build Coastguard Worker              import pathlib
209*9e94795aSAndroid Build Coastguard Worker              import time
210*9e94795aSAndroid Build Coastguard Worker
211*9e94795aSAndroid Build Coastguard Worker              pathlib.Path('{pid_file}').write_text(str(os.getpid()))
212*9e94795aSAndroid Build Coastguard Worker
213*9e94795aSAndroid Build Coastguard Worker              # Keep the process alive for us to explicitly interrupt it.
214*9e94795aSAndroid Build Coastguard Worker              time.sleep(10)
215*9e94795aSAndroid Build Coastguard Worker              """),
216*9e94795aSAndroid Build Coastguard Worker      ])
217*9e94795aSAndroid Build Coastguard Worker
218*9e94795aSAndroid Build Coastguard Worker    pid_file = self.temp_dir.joinpath('pid.txt')
219*9e94795aSAndroid Build Coastguard Worker    p = self.start_process(target=run, args=[pid_file])
220*9e94795aSAndroid Build Coastguard Worker    subprocess_pid = int(read_eventual_file_contents(pid_file))
221*9e94795aSAndroid Build Coastguard Worker
222*9e94795aSAndroid Build Coastguard Worker    os.kill(p.pid, signal.SIGINT)
223*9e94795aSAndroid Build Coastguard Worker    p.join()
224*9e94795aSAndroid Build Coastguard Worker
225*9e94795aSAndroid Build Coastguard Worker    self.assert_process_eventually_dies(p.pid)
226*9e94795aSAndroid Build Coastguard Worker    self.assert_process_eventually_dies(subprocess_pid)
227*9e94795aSAndroid Build Coastguard Worker
228*9e94795aSAndroid Build Coastguard Worker  def start_process(self, *args, **kwargs) -> multiprocessing.Process:
229*9e94795aSAndroid Build Coastguard Worker    p = multiprocessing.Process(*args, **kwargs)
230*9e94795aSAndroid Build Coastguard Worker    self._managed_processes.append(p)
231*9e94795aSAndroid Build Coastguard Worker    p.start()
232*9e94795aSAndroid Build Coastguard Worker    return p
233*9e94795aSAndroid Build Coastguard Worker
234*9e94795aSAndroid Build Coastguard Worker  def assert_process_eventually_dies(self, pid: int):
235*9e94795aSAndroid Build Coastguard Worker    try:
236*9e94795aSAndroid Build Coastguard Worker      wait_until(lambda: not ci_test_lib.process_alive(pid))
237*9e94795aSAndroid Build Coastguard Worker    except TimeoutError as e:
238*9e94795aSAndroid Build Coastguard Worker      self.fail(f'Process {pid} did not die after a while: {e}')
239*9e94795aSAndroid Build Coastguard Worker
240*9e94795aSAndroid Build Coastguard Worker  def assert_file_eventually_contains(self, file: pathlib.Path, substring: str):
241*9e94795aSAndroid Build Coastguard Worker    wait_until(lambda: file.is_file() and file.stat().st_size > 0)
242*9e94795aSAndroid Build Coastguard Worker    self.assertIn(substring, read_file_contents(file))
243*9e94795aSAndroid Build Coastguard Worker
244*9e94795aSAndroid Build Coastguard Worker  def _terminate_managed_processes(self):
245*9e94795aSAndroid Build Coastguard Worker    for p in self._managed_processes:
246*9e94795aSAndroid Build Coastguard Worker      if not p.is_alive():
247*9e94795aSAndroid Build Coastguard Worker        continue
248*9e94795aSAndroid Build Coastguard Worker
249*9e94795aSAndroid Build Coastguard Worker      # We terminate the process with `SIGINT` since using `terminate` or
250*9e94795aSAndroid Build Coastguard Worker      # `SIGKILL` doesn't kill any grandchild processes and we don't have
251*9e94795aSAndroid Build Coastguard Worker      # `psutil` available to easily query all children.
252*9e94795aSAndroid Build Coastguard Worker      os.kill(p.pid, signal.SIGINT)
253*9e94795aSAndroid Build Coastguard Worker
254*9e94795aSAndroid Build Coastguard Worker
255*9e94795aSAndroid Build Coastguard Workerclass BuildPlannerTest(unittest.TestCase):
256*9e94795aSAndroid Build Coastguard Worker
257*9e94795aSAndroid Build Coastguard Worker  class TestOptimizedBuildTarget(optimized_targets.OptimizedBuildTarget):
258*9e94795aSAndroid Build Coastguard Worker
259*9e94795aSAndroid Build Coastguard Worker    def __init__(
260*9e94795aSAndroid Build Coastguard Worker        self, target, build_context, args, output_targets, packaging_commands
261*9e94795aSAndroid Build Coastguard Worker    ):
262*9e94795aSAndroid Build Coastguard Worker      super().__init__(target, build_context, args)
263*9e94795aSAndroid Build Coastguard Worker      self.output_targets = output_targets
264*9e94795aSAndroid Build Coastguard Worker      self.packaging_commands = packaging_commands
265*9e94795aSAndroid Build Coastguard Worker
266*9e94795aSAndroid Build Coastguard Worker    def get_build_targets_impl(self):
267*9e94795aSAndroid Build Coastguard Worker      return self.output_targets
268*9e94795aSAndroid Build Coastguard Worker
269*9e94795aSAndroid Build Coastguard Worker    def get_package_outputs_commands_impl(self):
270*9e94795aSAndroid Build Coastguard Worker      return self.packaging_commands
271*9e94795aSAndroid Build Coastguard Worker
272*9e94795aSAndroid Build Coastguard Worker    def get_enabled_flag(self):
273*9e94795aSAndroid Build Coastguard Worker      return f'{self.target}_enabled'
274*9e94795aSAndroid Build Coastguard Worker
275*9e94795aSAndroid Build Coastguard Worker  def setUp(self):
276*9e94795aSAndroid Build Coastguard Worker    test_discovery_agent_patcher = mock.patch('test_discovery_agent.TestDiscoveryAgent.discover_test_zip_regexes')
277*9e94795aSAndroid Build Coastguard Worker    self.addCleanup(test_discovery_agent_patcher.stop)
278*9e94795aSAndroid Build Coastguard Worker    self.mock_test_discovery_agent_end = test_discovery_agent_patcher.start()
279*9e94795aSAndroid Build Coastguard Worker
280*9e94795aSAndroid Build Coastguard Worker
281*9e94795aSAndroid Build Coastguard Worker  def test_build_optimization_off_builds_everything(self):
282*9e94795aSAndroid Build Coastguard Worker    build_targets = {'target_1', 'target_2'}
283*9e94795aSAndroid Build Coastguard Worker    build_planner = self.create_build_planner(
284*9e94795aSAndroid Build Coastguard Worker        build_context=self.create_build_context(optimized_build_enabled=False),
285*9e94795aSAndroid Build Coastguard Worker        build_targets=build_targets,
286*9e94795aSAndroid Build Coastguard Worker    )
287*9e94795aSAndroid Build Coastguard Worker
288*9e94795aSAndroid Build Coastguard Worker    build_plan = build_planner.create_build_plan()
289*9e94795aSAndroid Build Coastguard Worker
290*9e94795aSAndroid Build Coastguard Worker    self.assertSetEqual(build_targets, build_plan.build_targets)
291*9e94795aSAndroid Build Coastguard Worker
292*9e94795aSAndroid Build Coastguard Worker  def test_build_optimization_off_doesnt_package(self):
293*9e94795aSAndroid Build Coastguard Worker    build_targets = {'target_1', 'target_2'}
294*9e94795aSAndroid Build Coastguard Worker    build_planner = self.create_build_planner(
295*9e94795aSAndroid Build Coastguard Worker        build_context=self.create_build_context(optimized_build_enabled=False),
296*9e94795aSAndroid Build Coastguard Worker        build_targets=build_targets,
297*9e94795aSAndroid Build Coastguard Worker    )
298*9e94795aSAndroid Build Coastguard Worker
299*9e94795aSAndroid Build Coastguard Worker    build_plan = build_planner.create_build_plan()
300*9e94795aSAndroid Build Coastguard Worker
301*9e94795aSAndroid Build Coastguard Worker    for packaging_command in self.run_packaging_commands(build_plan):
302*9e94795aSAndroid Build Coastguard Worker      self.assertEqual(len(packaging_command), 0)
303*9e94795aSAndroid Build Coastguard Worker
304*9e94795aSAndroid Build Coastguard Worker  def test_build_optimization_on_optimizes_target(self):
305*9e94795aSAndroid Build Coastguard Worker    build_targets = {'target_1', 'target_2'}
306*9e94795aSAndroid Build Coastguard Worker    build_planner = self.create_build_planner(
307*9e94795aSAndroid Build Coastguard Worker        build_targets=build_targets,
308*9e94795aSAndroid Build Coastguard Worker        build_context=self.create_build_context(
309*9e94795aSAndroid Build Coastguard Worker            enabled_build_features=[{'name': self.get_target_flag('target_1')}]
310*9e94795aSAndroid Build Coastguard Worker        ),
311*9e94795aSAndroid Build Coastguard Worker    )
312*9e94795aSAndroid Build Coastguard Worker
313*9e94795aSAndroid Build Coastguard Worker    build_plan = build_planner.create_build_plan()
314*9e94795aSAndroid Build Coastguard Worker
315*9e94795aSAndroid Build Coastguard Worker    expected_targets = {self.get_optimized_target_name('target_1'), 'target_2'}
316*9e94795aSAndroid Build Coastguard Worker    self.assertSetEqual(expected_targets, build_plan.build_targets)
317*9e94795aSAndroid Build Coastguard Worker
318*9e94795aSAndroid Build Coastguard Worker  def test_build_optimization_on_packages_target(self):
319*9e94795aSAndroid Build Coastguard Worker    build_targets = {'target_1', 'target_2'}
320*9e94795aSAndroid Build Coastguard Worker    optimized_target_name = self.get_optimized_target_name('target_1')
321*9e94795aSAndroid Build Coastguard Worker    packaging_commands = [[f'packaging {optimized_target_name}']]
322*9e94795aSAndroid Build Coastguard Worker    build_planner = self.create_build_planner(
323*9e94795aSAndroid Build Coastguard Worker        build_targets=build_targets,
324*9e94795aSAndroid Build Coastguard Worker        build_context=self.create_build_context(
325*9e94795aSAndroid Build Coastguard Worker            enabled_build_features=[{'name': self.get_target_flag('target_1')}]
326*9e94795aSAndroid Build Coastguard Worker        ),
327*9e94795aSAndroid Build Coastguard Worker        packaging_commands=packaging_commands,
328*9e94795aSAndroid Build Coastguard Worker    )
329*9e94795aSAndroid Build Coastguard Worker
330*9e94795aSAndroid Build Coastguard Worker    build_plan = build_planner.create_build_plan()
331*9e94795aSAndroid Build Coastguard Worker
332*9e94795aSAndroid Build Coastguard Worker    self.assertIn(packaging_commands, self.run_packaging_commands(build_plan))
333*9e94795aSAndroid Build Coastguard Worker
334*9e94795aSAndroid Build Coastguard Worker  def test_individual_build_optimization_off_doesnt_optimize(self):
335*9e94795aSAndroid Build Coastguard Worker    build_targets = {'target_1', 'target_2'}
336*9e94795aSAndroid Build Coastguard Worker    build_planner = self.create_build_planner(
337*9e94795aSAndroid Build Coastguard Worker        build_targets=build_targets,
338*9e94795aSAndroid Build Coastguard Worker    )
339*9e94795aSAndroid Build Coastguard Worker
340*9e94795aSAndroid Build Coastguard Worker    build_plan = build_planner.create_build_plan()
341*9e94795aSAndroid Build Coastguard Worker
342*9e94795aSAndroid Build Coastguard Worker    self.assertSetEqual(build_targets, build_plan.build_targets)
343*9e94795aSAndroid Build Coastguard Worker
344*9e94795aSAndroid Build Coastguard Worker  def test_individual_build_optimization_off_doesnt_package(self):
345*9e94795aSAndroid Build Coastguard Worker    build_targets = {'target_1', 'target_2'}
346*9e94795aSAndroid Build Coastguard Worker    packaging_commands = [['packaging command']]
347*9e94795aSAndroid Build Coastguard Worker    build_planner = self.create_build_planner(
348*9e94795aSAndroid Build Coastguard Worker        build_targets=build_targets,
349*9e94795aSAndroid Build Coastguard Worker        packaging_commands=packaging_commands,
350*9e94795aSAndroid Build Coastguard Worker    )
351*9e94795aSAndroid Build Coastguard Worker
352*9e94795aSAndroid Build Coastguard Worker    build_plan = build_planner.create_build_plan()
353*9e94795aSAndroid Build Coastguard Worker
354*9e94795aSAndroid Build Coastguard Worker    for packaging_command in self.run_packaging_commands(build_plan):
355*9e94795aSAndroid Build Coastguard Worker      self.assertEqual(len(packaging_command), 0)
356*9e94795aSAndroid Build Coastguard Worker
357*9e94795aSAndroid Build Coastguard Worker  def test_target_output_used_target_built(self):
358*9e94795aSAndroid Build Coastguard Worker    build_target = 'test_target'
359*9e94795aSAndroid Build Coastguard Worker    build_planner = self.create_build_planner(
360*9e94795aSAndroid Build Coastguard Worker        build_targets={build_target},
361*9e94795aSAndroid Build Coastguard Worker        build_context=self.create_build_context(
362*9e94795aSAndroid Build Coastguard Worker            test_context=self.get_test_context(build_target),
363*9e94795aSAndroid Build Coastguard Worker            enabled_build_features=[{'name': 'test_target_unused_exclusion'}],
364*9e94795aSAndroid Build Coastguard Worker        ),
365*9e94795aSAndroid Build Coastguard Worker    )
366*9e94795aSAndroid Build Coastguard Worker
367*9e94795aSAndroid Build Coastguard Worker    build_plan = build_planner.create_build_plan()
368*9e94795aSAndroid Build Coastguard Worker
369*9e94795aSAndroid Build Coastguard Worker    self.assertSetEqual(build_plan.build_targets, {build_target})
370*9e94795aSAndroid Build Coastguard Worker
371*9e94795aSAndroid Build Coastguard Worker  def test_target_regex_used_target_built(self):
372*9e94795aSAndroid Build Coastguard Worker    build_target = 'test_target'
373*9e94795aSAndroid Build Coastguard Worker    test_context = self.get_test_context(build_target)
374*9e94795aSAndroid Build Coastguard Worker    test_context['testInfos'][0]['extraOptions'] = [{
375*9e94795aSAndroid Build Coastguard Worker        'key': 'additional-files-filter',
376*9e94795aSAndroid Build Coastguard Worker        'values': [f'.*{build_target}.*\.zip'],
377*9e94795aSAndroid Build Coastguard Worker    }]
378*9e94795aSAndroid Build Coastguard Worker    build_planner = self.create_build_planner(
379*9e94795aSAndroid Build Coastguard Worker        build_targets={build_target},
380*9e94795aSAndroid Build Coastguard Worker        build_context=self.create_build_context(
381*9e94795aSAndroid Build Coastguard Worker            test_context=test_context,
382*9e94795aSAndroid Build Coastguard Worker            enabled_build_features=[{'name': 'test_target_unused_exclusion'}],
383*9e94795aSAndroid Build Coastguard Worker        ),
384*9e94795aSAndroid Build Coastguard Worker    )
385*9e94795aSAndroid Build Coastguard Worker
386*9e94795aSAndroid Build Coastguard Worker    build_plan = build_planner.create_build_plan()
387*9e94795aSAndroid Build Coastguard Worker
388*9e94795aSAndroid Build Coastguard Worker    self.assertSetEqual(build_plan.build_targets, {build_target})
389*9e94795aSAndroid Build Coastguard Worker
390*9e94795aSAndroid Build Coastguard Worker  def test_target_output_not_used_target_not_built(self):
391*9e94795aSAndroid Build Coastguard Worker    build_target = 'test_target'
392*9e94795aSAndroid Build Coastguard Worker    test_context = self.get_test_context(build_target)
393*9e94795aSAndroid Build Coastguard Worker    test_context['testInfos'][0]['extraOptions'] = []
394*9e94795aSAndroid Build Coastguard Worker    build_planner = self.create_build_planner(
395*9e94795aSAndroid Build Coastguard Worker        build_targets={build_target},
396*9e94795aSAndroid Build Coastguard Worker        build_context=self.create_build_context(
397*9e94795aSAndroid Build Coastguard Worker            test_context=test_context,
398*9e94795aSAndroid Build Coastguard Worker            enabled_build_features=[{'name': 'test_target_unused_exclusion'}],
399*9e94795aSAndroid Build Coastguard Worker        ),
400*9e94795aSAndroid Build Coastguard Worker    )
401*9e94795aSAndroid Build Coastguard Worker
402*9e94795aSAndroid Build Coastguard Worker    build_plan = build_planner.create_build_plan()
403*9e94795aSAndroid Build Coastguard Worker
404*9e94795aSAndroid Build Coastguard Worker    self.assertSetEqual(build_plan.build_targets, set())
405*9e94795aSAndroid Build Coastguard Worker
406*9e94795aSAndroid Build Coastguard Worker  def test_target_regex_matching_not_too_broad(self):
407*9e94795aSAndroid Build Coastguard Worker    build_target = 'test_target'
408*9e94795aSAndroid Build Coastguard Worker    test_context = self.get_test_context(build_target)
409*9e94795aSAndroid Build Coastguard Worker    test_context['testInfos'][0]['extraOptions'] = [{
410*9e94795aSAndroid Build Coastguard Worker        'key': 'additional-files-filter',
411*9e94795aSAndroid Build Coastguard Worker        'values': [f'.*a{build_target}.*\.zip'],
412*9e94795aSAndroid Build Coastguard Worker    }]
413*9e94795aSAndroid Build Coastguard Worker    build_planner = self.create_build_planner(
414*9e94795aSAndroid Build Coastguard Worker        build_targets={build_target},
415*9e94795aSAndroid Build Coastguard Worker        build_context=self.create_build_context(
416*9e94795aSAndroid Build Coastguard Worker            test_context=test_context,
417*9e94795aSAndroid Build Coastguard Worker            enabled_build_features=[{'name': 'test_target_unused_exclusion'}],
418*9e94795aSAndroid Build Coastguard Worker        ),
419*9e94795aSAndroid Build Coastguard Worker    )
420*9e94795aSAndroid Build Coastguard Worker
421*9e94795aSAndroid Build Coastguard Worker    build_plan = build_planner.create_build_plan()
422*9e94795aSAndroid Build Coastguard Worker
423*9e94795aSAndroid Build Coastguard Worker    self.assertSetEqual(build_plan.build_targets, set())
424*9e94795aSAndroid Build Coastguard Worker
425*9e94795aSAndroid Build Coastguard Worker  def create_build_planner(
426*9e94795aSAndroid Build Coastguard Worker      self,
427*9e94795aSAndroid Build Coastguard Worker      build_targets: set[str],
428*9e94795aSAndroid Build Coastguard Worker      build_context: BuildContext = None,
429*9e94795aSAndroid Build Coastguard Worker      args: argparse.Namespace = None,
430*9e94795aSAndroid Build Coastguard Worker      target_optimizations: dict[
431*9e94795aSAndroid Build Coastguard Worker          str, optimized_targets.OptimizedBuildTarget
432*9e94795aSAndroid Build Coastguard Worker      ] = None,
433*9e94795aSAndroid Build Coastguard Worker      packaging_commands: list[list[str]] = [],
434*9e94795aSAndroid Build Coastguard Worker  ) -> build_test_suites.BuildPlanner:
435*9e94795aSAndroid Build Coastguard Worker    if not build_context:
436*9e94795aSAndroid Build Coastguard Worker      build_context = self.create_build_context()
437*9e94795aSAndroid Build Coastguard Worker    if not args:
438*9e94795aSAndroid Build Coastguard Worker      args = self.create_args(extra_build_targets=build_targets)
439*9e94795aSAndroid Build Coastguard Worker    if not target_optimizations:
440*9e94795aSAndroid Build Coastguard Worker      target_optimizations = self.create_target_optimizations(
441*9e94795aSAndroid Build Coastguard Worker          build_context,
442*9e94795aSAndroid Build Coastguard Worker          build_targets,
443*9e94795aSAndroid Build Coastguard Worker          packaging_commands,
444*9e94795aSAndroid Build Coastguard Worker      )
445*9e94795aSAndroid Build Coastguard Worker    return build_test_suites.BuildPlanner(
446*9e94795aSAndroid Build Coastguard Worker        build_context, args, target_optimizations
447*9e94795aSAndroid Build Coastguard Worker    )
448*9e94795aSAndroid Build Coastguard Worker
449*9e94795aSAndroid Build Coastguard Worker  def create_build_context(
450*9e94795aSAndroid Build Coastguard Worker      self,
451*9e94795aSAndroid Build Coastguard Worker      optimized_build_enabled: bool = True,
452*9e94795aSAndroid Build Coastguard Worker      enabled_build_features: list[dict[str, str]] = [],
453*9e94795aSAndroid Build Coastguard Worker      test_context: dict[str, any] = {},
454*9e94795aSAndroid Build Coastguard Worker  ) -> BuildContext:
455*9e94795aSAndroid Build Coastguard Worker    build_context_dict = {}
456*9e94795aSAndroid Build Coastguard Worker    build_context_dict['enabledBuildFeatures'] = enabled_build_features
457*9e94795aSAndroid Build Coastguard Worker    if optimized_build_enabled:
458*9e94795aSAndroid Build Coastguard Worker      build_context_dict['enabledBuildFeatures'].append(
459*9e94795aSAndroid Build Coastguard Worker          {'name': 'optimized_build'}
460*9e94795aSAndroid Build Coastguard Worker      )
461*9e94795aSAndroid Build Coastguard Worker    build_context_dict['testContext'] = test_context
462*9e94795aSAndroid Build Coastguard Worker    return BuildContext(build_context_dict)
463*9e94795aSAndroid Build Coastguard Worker
464*9e94795aSAndroid Build Coastguard Worker  def create_args(
465*9e94795aSAndroid Build Coastguard Worker      self, extra_build_targets: set[str] = set()
466*9e94795aSAndroid Build Coastguard Worker  ) -> argparse.Namespace:
467*9e94795aSAndroid Build Coastguard Worker    parser = argparse.ArgumentParser()
468*9e94795aSAndroid Build Coastguard Worker    parser.add_argument('extra_targets', nargs='*')
469*9e94795aSAndroid Build Coastguard Worker    return parser.parse_args(extra_build_targets)
470*9e94795aSAndroid Build Coastguard Worker
471*9e94795aSAndroid Build Coastguard Worker  def create_target_optimizations(
472*9e94795aSAndroid Build Coastguard Worker      self,
473*9e94795aSAndroid Build Coastguard Worker      build_context: BuildContext,
474*9e94795aSAndroid Build Coastguard Worker      build_targets: set[str],
475*9e94795aSAndroid Build Coastguard Worker      packaging_commands: list[list[str]] = [],
476*9e94795aSAndroid Build Coastguard Worker  ):
477*9e94795aSAndroid Build Coastguard Worker    target_optimizations = dict()
478*9e94795aSAndroid Build Coastguard Worker    for target in build_targets:
479*9e94795aSAndroid Build Coastguard Worker      target_optimizations[target] = functools.partial(
480*9e94795aSAndroid Build Coastguard Worker          self.TestOptimizedBuildTarget,
481*9e94795aSAndroid Build Coastguard Worker          output_targets={self.get_optimized_target_name(target)},
482*9e94795aSAndroid Build Coastguard Worker          packaging_commands=packaging_commands,
483*9e94795aSAndroid Build Coastguard Worker      )
484*9e94795aSAndroid Build Coastguard Worker
485*9e94795aSAndroid Build Coastguard Worker    return target_optimizations
486*9e94795aSAndroid Build Coastguard Worker
487*9e94795aSAndroid Build Coastguard Worker  def get_target_flag(self, target: str):
488*9e94795aSAndroid Build Coastguard Worker    return f'{target}_enabled'
489*9e94795aSAndroid Build Coastguard Worker
490*9e94795aSAndroid Build Coastguard Worker  def get_optimized_target_name(self, target: str):
491*9e94795aSAndroid Build Coastguard Worker    return f'{target}_optimized'
492*9e94795aSAndroid Build Coastguard Worker
493*9e94795aSAndroid Build Coastguard Worker  def get_test_context(self, target: str):
494*9e94795aSAndroid Build Coastguard Worker    return {
495*9e94795aSAndroid Build Coastguard Worker        'testInfos': [
496*9e94795aSAndroid Build Coastguard Worker            {
497*9e94795aSAndroid Build Coastguard Worker                'name': 'atp_test',
498*9e94795aSAndroid Build Coastguard Worker                'target': 'test_target',
499*9e94795aSAndroid Build Coastguard Worker                'branch': 'branch',
500*9e94795aSAndroid Build Coastguard Worker                'extraOptions': [{
501*9e94795aSAndroid Build Coastguard Worker                    'key': 'additional-files-filter',
502*9e94795aSAndroid Build Coastguard Worker                    'values': [f'{target}.zip'],
503*9e94795aSAndroid Build Coastguard Worker                }],
504*9e94795aSAndroid Build Coastguard Worker                'command': '/tf/command',
505*9e94795aSAndroid Build Coastguard Worker                'extraBuildTargets': [
506*9e94795aSAndroid Build Coastguard Worker                    'extra_build_target',
507*9e94795aSAndroid Build Coastguard Worker                ],
508*9e94795aSAndroid Build Coastguard Worker            },
509*9e94795aSAndroid Build Coastguard Worker        ],
510*9e94795aSAndroid Build Coastguard Worker    }
511*9e94795aSAndroid Build Coastguard Worker
512*9e94795aSAndroid Build Coastguard Worker  def run_packaging_commands(self, build_plan: build_test_suites.BuildPlan):
513*9e94795aSAndroid Build Coastguard Worker    return [
514*9e94795aSAndroid Build Coastguard Worker        packaging_command_getter()
515*9e94795aSAndroid Build Coastguard Worker        for packaging_command_getter in build_plan.packaging_commands_getters
516*9e94795aSAndroid Build Coastguard Worker    ]
517*9e94795aSAndroid Build Coastguard Worker
518*9e94795aSAndroid Build Coastguard Worker
519*9e94795aSAndroid Build Coastguard Workerdef wait_until(
520*9e94795aSAndroid Build Coastguard Worker    condition_function: Callable[[], bool],
521*9e94795aSAndroid Build Coastguard Worker    timeout_secs: float = 3.0,
522*9e94795aSAndroid Build Coastguard Worker    polling_interval_secs: float = 0.1,
523*9e94795aSAndroid Build Coastguard Worker):
524*9e94795aSAndroid Build Coastguard Worker  """Waits until a condition function returns True."""
525*9e94795aSAndroid Build Coastguard Worker
526*9e94795aSAndroid Build Coastguard Worker  start_time_secs = time.time()
527*9e94795aSAndroid Build Coastguard Worker
528*9e94795aSAndroid Build Coastguard Worker  while not condition_function():
529*9e94795aSAndroid Build Coastguard Worker    if time.time() - start_time_secs > timeout_secs:
530*9e94795aSAndroid Build Coastguard Worker      raise TimeoutError(
531*9e94795aSAndroid Build Coastguard Worker          f'Condition not met within timeout: {timeout_secs} seconds'
532*9e94795aSAndroid Build Coastguard Worker      )
533*9e94795aSAndroid Build Coastguard Worker
534*9e94795aSAndroid Build Coastguard Worker    time.sleep(polling_interval_secs)
535*9e94795aSAndroid Build Coastguard Worker
536*9e94795aSAndroid Build Coastguard Worker
537*9e94795aSAndroid Build Coastguard Workerdef read_file_contents(file: pathlib.Path) -> str:
538*9e94795aSAndroid Build Coastguard Worker  with open(file, 'r') as f:
539*9e94795aSAndroid Build Coastguard Worker    return f.read()
540*9e94795aSAndroid Build Coastguard Worker
541*9e94795aSAndroid Build Coastguard Worker
542*9e94795aSAndroid Build Coastguard Workerdef read_eventual_file_contents(file: pathlib.Path) -> str:
543*9e94795aSAndroid Build Coastguard Worker  wait_until(lambda: file.is_file() and file.stat().st_size > 0)
544*9e94795aSAndroid Build Coastguard Worker  return read_file_contents(file)
545*9e94795aSAndroid Build Coastguard Worker
546*9e94795aSAndroid Build Coastguard Worker
547*9e94795aSAndroid Build Coastguard Workerif __name__ == '__main__':
548*9e94795aSAndroid Build Coastguard Worker  ci_test_lib.main()
549