blob: 6f9b59c5c88c36a3fae181462457812b549ce339 [file] [log] [blame]
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +00001# 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
15import dataclasses
Zhuoyao Zhangdb666bc2024-04-30 00:28:34 +000016import glob
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +000017from importlib import resources
18import logging
19import os
20from pathlib import Path
21import re
Zhuoyao Zhangdb666bc2024-04-30 00:28:34 +000022import shutil
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +000023import signal
24import stat
25import subprocess
Zhuoyao Zhangdb666bc2024-04-30 00:28:34 +000026import sys
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +000027import tempfile
28import textwrap
29import time
30import unittest
31import zipfile
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +000032
33EXII_RETURN_CODE = 0
34INTERRUPTED_RETURN_CODE = 130
35
36
37class RunToolWithLoggingTest(unittest.TestCase):
38
39 @classmethod
40 def setUpClass(cls):
41 super().setUpClass()
42 # Configure to print logging to stdout.
43 logging.basicConfig(filename=None, level=logging.DEBUG)
44 console = logging.StreamHandler(sys.stdout)
Zhuoyao Zhangdb666bc2024-04-30 00:28:34 +000045 logging.getLogger("").addHandler(console)
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +000046
47 def setUp(self):
48 super().setUp()
49 self.working_dir = tempfile.TemporaryDirectory()
50 # Run all the tests from working_dir which is our temp Android build top.
51 os.chdir(self.working_dir.name)
52 # Extract envsetup.zip which contains the envsetup.sh and other dependent
53 # scripts required to set up the build environments.
Zhuoyao Zhangdb666bc2024-04-30 00:28:34 +000054 with resources.files("testdata").joinpath("envsetup.zip").open("rb") as p:
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +000055 with zipfile.ZipFile(p, "r") as zip_f:
56 zip_f.extractall()
57
58 def tearDown(self):
59 self.working_dir.cleanup()
60 super().tearDown()
61
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +000062 def test_does_not_log_when_logger_var_empty(self):
63 test_tool = TestScript.create(self.working_dir)
64
65 self._run_script_and_wait(f"""
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +000066 ANDROID_TOOL_LOGGER=""
67 run_tool_with_logging "FAKE_TOOL" {test_tool.executable} arg1 arg2
68 """)
69
70 test_tool.assert_called_once_with_args("arg1 arg2")
71
72 def test_does_not_log_with_logger_unset(self):
73 test_tool = TestScript.create(self.working_dir)
74
75 self._run_script_and_wait(f"""
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +000076 unset ANDROID_TOOL_LOGGER
77 run_tool_with_logging "FAKE_TOOL" {test_tool.executable} arg1 arg2
78 """)
79
80 test_tool.assert_called_once_with_args("arg1 arg2")
81
82 def test_log_success_with_logger_enabled(self):
83 test_tool = TestScript.create(self.working_dir)
84 test_logger = TestScript.create(self.working_dir)
85
86 self._run_script_and_wait(f"""
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +000087 ANDROID_TOOL_LOGGER="{test_logger.executable}"
88 run_tool_with_logging "FAKE_TOOL" {test_tool.executable} arg1 arg2
89 """)
90
91 test_tool.assert_called_once_with_args("arg1 arg2")
92 expected_logger_args = (
Zhuoyao Zhangdfdf19f2024-05-20 18:31:12 +000093 "--tool_tag=FAKE_TOOL --start_timestamp=\d+\.\d+ --end_timestamp="
94 "\d+\.\d+ --tool_args=arg1 arg2 --exit_code=0"
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +000095 )
96 test_logger.assert_called_once_with_args(expected_logger_args)
97
98 def test_run_tool_output_is_same_with_and_without_logging(self):
99 test_tool = TestScript.create(self.working_dir, "echo 'tool called'")
100 test_logger = TestScript.create(self.working_dir)
101
102 run_tool_with_logging_stdout, run_tool_with_logging_stderr = (
103 self._run_script_and_wait(f"""
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +0000104 ANDROID_TOOL_LOGGER="{test_logger.executable}"
105 run_tool_with_logging "FAKE_TOOL" {test_tool.executable} arg1 arg2
106 """)
107 )
108
109 run_tool_without_logging_stdout, run_tool_without_logging_stderr = (
110 self._run_script_and_wait(f"""
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +0000111 ANDROID_TOOL_LOGGER="{test_logger.executable}"
112 {test_tool.executable} arg1 arg2
113 """)
114 )
115
116 self.assertEqual(
117 run_tool_with_logging_stdout, run_tool_without_logging_stdout
118 )
119 self.assertEqual(
120 run_tool_with_logging_stderr, run_tool_without_logging_stderr
121 )
122
123 def test_logger_output_is_suppressed(self):
124 test_tool = TestScript.create(self.working_dir)
125 test_logger = TestScript.create(self.working_dir, "echo 'logger called'")
126
127 run_tool_with_logging_output, _ = self._run_script_and_wait(f"""
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +0000128 ANDROID_TOOL_LOGGER="{test_logger.executable}"
129 run_tool_with_logging "FAKE_TOOL" {test_tool.executable} arg1 arg2
130 """)
131
132 self.assertNotIn("logger called", run_tool_with_logging_output)
133
134 def test_logger_error_is_suppressed(self):
135 test_tool = TestScript.create(self.working_dir)
136 test_logger = TestScript.create(
137 self.working_dir, "echo 'logger failed' > /dev/stderr; exit 1"
138 )
139
140 _, err = self._run_script_and_wait(f"""
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +0000141 ANDROID_TOOL_LOGGER="{test_logger.executable}"
142 run_tool_with_logging "FAKE_TOOL" {test_tool.executable} arg1 arg2
143 """)
144
145 self.assertNotIn("logger failed", err)
146
147 def test_log_success_when_tool_interrupted(self):
148 test_tool = TestScript.create(self.working_dir, script_body="sleep 100")
149 test_logger = TestScript.create(self.working_dir)
150
151 process = self._run_script_in_build_env(f"""
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +0000152 ANDROID_TOOL_LOGGER="{test_logger.executable}"
153 run_tool_with_logging "FAKE_TOOL" {test_tool.executable} arg1 arg2
154 """)
155
156 pgid = os.getpgid(process.pid)
157 # Give sometime for the subprocess to start.
158 time.sleep(1)
159 # Kill the subprocess and any processes created in the same group.
160 os.killpg(pgid, signal.SIGINT)
161
162 returncode, _, _ = self._wait_for_process(process)
163 self.assertEqual(returncode, INTERRUPTED_RETURN_CODE)
164
165 expected_logger_args = (
Zhuoyao Zhangdfdf19f2024-05-20 18:31:12 +0000166 "--tool_tag=FAKE_TOOL --start_timestamp=\d+\.\d+ --end_timestamp="
167 "\d+\.\d+ --tool_args=arg1 arg2 --exit_code=130"
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +0000168 )
169 test_logger.assert_called_once_with_args(expected_logger_args)
170
171 def test_logger_can_be_toggled_on(self):
172 test_tool = TestScript.create(self.working_dir)
173 test_logger = TestScript.create(self.working_dir)
174
175 self._run_script_and_wait(f"""
Zhuoyao Zhangef1c03f2024-05-07 22:18:37 +0000176 ANDROID_TOOL_LOGGER=""
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +0000177 ANDROID_TOOL_LOGGER="{test_logger.executable}"
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +0000178 run_tool_with_logging "FAKE_TOOL" {test_tool.executable} arg1 arg2
179 """)
180
181 test_logger.assert_called_with_times(1)
182
183 def test_logger_can_be_toggled_off(self):
184 test_tool = TestScript.create(self.working_dir)
185 test_logger = TestScript.create(self.working_dir)
186
187 self._run_script_and_wait(f"""
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +0000188 ANDROID_TOOL_LOGGER="{test_logger.executable}"
Zhuoyao Zhangef1c03f2024-05-07 22:18:37 +0000189 ANDROID_TOOL_LOGGER=""
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +0000190 run_tool_with_logging "FAKE_TOOL" {test_tool.executable} arg1 arg2
191 """)
192
193 test_logger.assert_not_called()
194
Zhuoyao Zhangdb666bc2024-04-30 00:28:34 +0000195 def test_integration_tool_event_logger_dry_run(self):
196 test_tool = TestScript.create(self.working_dir)
197 logger_path = self._import_logger()
198
199 self._run_script_and_wait(f"""
200 TMPDIR="{self.working_dir.name}"
Zhuoyao Zhangdb666bc2024-04-30 00:28:34 +0000201 ANDROID_TOOL_LOGGER="{logger_path}"
202 ANDROID_TOOL_LOGGER_EXTRA_ARGS="--dry_run"
203 run_tool_with_logging "FAKE_TOOL" {test_tool.executable} arg1 arg2
204 """)
205
206 self._assert_logger_dry_run()
207
Zhuoyao Zhangdfdf19f2024-05-20 18:31:12 +0000208 def test_tool_args_do_not_fail_logger(self):
209 test_tool = TestScript.create(self.working_dir)
210 logger_path = self._import_logger()
211
212 self._run_script_and_wait(f"""
213 TMPDIR="{self.working_dir.name}"
214 ANDROID_TOOL_LOGGER="{logger_path}"
215 ANDROID_TOOL_LOGGER_EXTRA_ARGS="--dry_run"
216 run_tool_with_logging "FAKE_TOOL" {test_tool.executable} --tool-arg1
217 """)
218
219 self._assert_logger_dry_run()
220
Zhuoyao Zhangdb666bc2024-04-30 00:28:34 +0000221 def _import_logger(self) -> Path:
222 logger = "tool_event_logger"
223 logger_path = Path(self.working_dir.name).joinpath(logger)
224 with resources.as_file(resources.files("testdata").joinpath(logger)) as p:
225 shutil.copy(p, logger_path)
226 Path.chmod(logger_path, 0o755)
227 return logger_path
228
229 def _assert_logger_dry_run(self):
230 log_files = glob.glob(self.working_dir.name + "/tool_event_logger_*/*.log")
231 self.assertEqual(len(log_files), 1)
232
233 with open(log_files[0], "r") as f:
234 lines = f.readlines()
235 self.assertEqual(len(lines), 1)
236 self.assertIn("dry run", lines[0])
237
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +0000238 def _create_build_env_script(self) -> str:
239 return f"""
240 source {Path(self.working_dir.name).joinpath("build/make/envsetup.sh")}
241 """
242
243 def _run_script_and_wait(self, test_script: str) -> tuple[str, str]:
244 process = self._run_script_in_build_env(test_script)
245 returncode, out, err = self._wait_for_process(process)
246 logging.debug("script stdout: %s", out)
247 logging.debug("script stderr: %s", err)
248 self.assertEqual(returncode, EXII_RETURN_CODE)
249 return out, err
250
251 def _run_script_in_build_env(self, test_script: str) -> subprocess.Popen:
252 setup_build_env_script = self._create_build_env_script()
253 return subprocess.Popen(
254 setup_build_env_script + test_script,
255 shell=True,
256 stdout=subprocess.PIPE,
257 stderr=subprocess.PIPE,
258 text=True,
259 start_new_session=True,
Zhuoyao Zhangdb666bc2024-04-30 00:28:34 +0000260 executable="/bin/bash",
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +0000261 )
262
263 def _wait_for_process(
264 self, process: subprocess.Popen
265 ) -> tuple[int, str, str]:
266 pgid = os.getpgid(process.pid)
267 out, err = process.communicate()
268 # Wait for all process in the same group to complete since the logger runs
269 # as a separate detached process.
270 self._wait_for_process_group(pgid)
271 return (process.returncode, out, err)
272
273 def _wait_for_process_group(self, pgid: int, timeout: int = 5):
274 """Waits for all subprocesses within the process group to complete."""
275 start_time = time.time()
276 while True:
277 if time.time() - start_time > timeout:
278 raise TimeoutError(
279 f"Process group did not complete after {timeout} seconds"
280 )
281 for pid in os.listdir("/proc"):
282 if pid.isdigit():
283 try:
284 if os.getpgid(int(pid)) == pgid:
285 time.sleep(0.1)
286 break
287 except (FileNotFoundError, PermissionError, ProcessLookupError):
288 pass
289 else:
290 # All processes have completed.
291 break
292
293
294@dataclasses.dataclass
295class TestScript:
296 executable: Path
297 output_file: Path
298
299 def create(temp_dir: Path, script_body: str = ""):
300 with tempfile.NamedTemporaryFile(dir=temp_dir.name, delete=False) as f:
301 output_file = f.name
302
303 with tempfile.NamedTemporaryFile(dir=temp_dir.name, delete=False) as f:
304 executable = f.name
305 executable_contents = textwrap.dedent(f"""
306 #!/bin/bash
307
308 echo "${{@}}" >> {output_file}
309 {script_body}
310 """)
311 f.write(executable_contents.encode("utf-8"))
312
Zhuoyao Zhangdb666bc2024-04-30 00:28:34 +0000313 Path.chmod(f.name, os.stat(f.name).st_mode | stat.S_IEXEC)
Zhuoyao Zhangcc44d2e2024-03-26 22:50:12 +0000314
315 return TestScript(executable, output_file)
316
317 def assert_called_with_times(self, expected_call_times: int):
318 lines = self._read_contents_from_output_file()
319 assert len(lines) == expected_call_times, (
320 f"Expect to call {expected_call_times} times, but actually called"
321 f" {len(lines)} times."
322 )
323
324 def assert_called_with_args(self, expected_args: str):
325 lines = self._read_contents_from_output_file()
326 assert len(lines) > 0
327 assert re.search(expected_args, lines[0]), (
328 f"Expect to call with args {expected_args}, but actually called with"
329 f" args {lines[0]}."
330 )
331
332 def assert_not_called(self):
333 self.assert_called_with_times(0)
334
335 def assert_called_once_with_args(self, expected_args: str):
336 self.assert_called_with_times(1)
337 self.assert_called_with_args(expected_args)
338
339 def _read_contents_from_output_file(self) -> list[str]:
340 with open(self.output_file, "r") as f:
341 return f.readlines()
342
343
344if __name__ == "__main__":
345 unittest.main()