| #!/usr/bin/env vpython3 |
| # Copyright 2026 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Unit tests for run_local_coverage.py.""" |
| |
| import contextlib |
| import io |
| import pathlib |
| import sys |
| import unittest |
| from unittest import mock |
| import run_local_coverage as rlc |
| |
| |
| class RunLocalCoverageTest(unittest.TestCase): |
| """Tests LLVM tool resolution, subprocess execution, profile merging, and main CLI workflow.""" |
| |
| def setUp(self) -> None: |
| """Sets up subprocess and LLVM tool path mocks.""" |
| self.run_patcher = mock.patch('subprocess.run') |
| self.mock_run = self.run_patcher.start() |
| self.addCleanup(self.run_patcher.stop) |
| |
| self.tool_patcher = mock.patch('run_local_coverage.find_llvm_tool') |
| self.mock_tool = self.tool_patcher.start() |
| self.mock_tool.return_value = pathlib.Path('/fake/bin/llvm-tool') |
| self.addCleanup(self.tool_patcher.stop) |
| |
| self.check_patcher = mock.patch('subprocess.check_call') |
| self.mock_check = self.check_patcher.start() |
| self.addCleanup(self.check_patcher.stop) |
| |
| def test_find_llvm_tool_success(self) -> None: |
| """Verifies tool resolution when executable exists.""" |
| self.tool_patcher.stop() |
| with mock.patch.object(pathlib.Path, 'exists', return_value=True): |
| res = rlc.find_llvm_tool('llvm-profdata') |
| self.assertEqual(res.name, 'llvm-profdata') |
| |
| def test_find_llvm_tool_downloads_missing(self) -> None: |
| """Verifies missing LLVM tool is downloaded automatically.""" |
| self.tool_patcher.stop() |
| with mock.patch.object(pathlib.Path, 'exists', side_effect=[False, True]): |
| res = rlc.find_llvm_tool('llvm-profdata') |
| self.assertEqual(res.name, 'llvm-profdata') |
| self.assertTrue(self.mock_check.called) |
| |
| def test_find_llvm_tool_download_failure(self) -> None: |
| """Verifies sys.exit(1) when downloading LLVM tools fails.""" |
| self.tool_patcher.stop() |
| self.mock_check.side_effect = rlc.subprocess.CalledProcessError( |
| 1, 'update.py') |
| with mock.patch.object(pathlib.Path, 'exists', return_value=False): |
| err_buf = io.StringIO() |
| with contextlib.redirect_stderr(err_buf): |
| with self.assertRaises(SystemExit) as cm: |
| rlc.find_llvm_tool('missing-tool') |
| self.assertEqual(cm.exception.code, 1) |
| self.assertIn('Failed to download', err_buf.getvalue()) |
| |
| def test_find_llvm_tool_not_found_after_update(self) -> None: |
| """Verifies sys.exit(1) when LLVM executable is still missing after update.""" |
| self.tool_patcher.stop() |
| with mock.patch.object(pathlib.Path, 'exists', return_value=False): |
| err_buf = io.StringIO() |
| with contextlib.redirect_stderr(err_buf): |
| with self.assertRaises(SystemExit) as cm: |
| rlc.find_llvm_tool('missing-tool') |
| self.assertEqual(cm.exception.code, 1) |
| self.assertIn('still not found', err_buf.getvalue()) |
| |
| def test_run_test_binary_injects_env(self) -> None: |
| """Verifies LLVM_PROFILE_FILE is injected into subprocess env.""" |
| self.mock_run.return_value = mock.MagicMock(returncode=0) |
| bin_path = pathlib.Path('out/Default/net_unittests') |
| prof_dir = pathlib.Path('scratch/prof') |
| res = rlc.run_test_binary(bin_path, prof_dir, ['--gtest_filter=Foo']) |
| self.assertEqual(res, 0) |
| _, kwargs = self.mock_run.call_args |
| self.assertIn('LLVM_PROFILE_FILE', kwargs['env']) |
| |
| def test_merge_profiles_no_files(self) -> None: |
| """Verifies merge returns False when profraw files are missing.""" |
| with mock.patch.object(pathlib.Path, 'glob', return_value=[]): |
| err_buf = io.StringIO() |
| with contextlib.redirect_stderr(err_buf): |
| res = rlc.merge_profiles(pathlib.Path('dir'), |
| pathlib.Path('out.profdata')) |
| self.assertFalse(res) |
| self.assertIn('No profraw files found', err_buf.getvalue()) |
| |
| def test_merge_profiles_subprocess_failure(self) -> None: |
| """Verifies merge returns False when llvm-profdata fails.""" |
| fake_files = [pathlib.Path('a.profraw')] |
| self.mock_run.return_value = mock.MagicMock(returncode=1) |
| with mock.patch.object(pathlib.Path, 'glob', return_value=fake_files): |
| res = rlc.merge_profiles(pathlib.Path('dir'), |
| pathlib.Path('out.profdata')) |
| self.assertFalse(res) |
| |
| def test_merge_profiles_success(self) -> None: |
| """Verifies merge returns True when llvm-profdata succeeds.""" |
| fake_files = [pathlib.Path('a.profraw')] |
| self.mock_run.return_value = mock.MagicMock(returncode=0) |
| with mock.patch.object(pathlib.Path, 'glob', return_value=fake_files): |
| res = rlc.merge_profiles(pathlib.Path('dir'), |
| pathlib.Path('out.profdata')) |
| self.assertTrue(res) |
| |
| def test_export_coverage_failure(self) -> None: |
| """Verifies llvm-cov failure returns None and prints stderr.""" |
| mock_proc = mock.MagicMock(returncode=1, stderr='bad profile') |
| self.mock_run.return_value = mock_proc |
| err_buf = io.StringIO() |
| with contextlib.redirect_stderr(err_buf): |
| out = rlc.export_coverage(pathlib.Path('bin'), pathlib.Path('prof.data'), |
| None) |
| self.assertIsNone(out) |
| self.assertIn('llvm-cov failed', err_buf.getvalue()) |
| |
| def test_export_coverage_success(self) -> None: |
| """Verifies stdout is returned when llvm-cov succeeds.""" |
| mock_proc = mock.MagicMock(returncode=0, stdout='Coverage: 100%') |
| self.mock_run.return_value = mock_proc |
| out = rlc.export_coverage(pathlib.Path('bin'), pathlib.Path('prof.data'), |
| 'foo.cc') |
| self.assertEqual(out, 'Coverage: 100%') |
| args, _ = self.mock_run.call_args |
| self.assertIn('foo.cc', args[0]) |
| |
| def test_main_success_flow(self) -> None: |
| """Verifies full CLI workflow printing report to stdout.""" |
| with mock.patch.object(pathlib.Path, 'mkdir'): |
| with mock.patch('run_local_coverage.run_test_binary', return_value=1): |
| with mock.patch('run_local_coverage.merge_profiles', return_value=True): |
| with mock.patch('run_local_coverage.export_coverage', |
| return_value='REPORT 100%'): |
| test_args = ['rlc.py', '--binary', 'net_unittests'] |
| out_buf = io.StringIO() |
| err_buf = io.StringIO() |
| with mock.patch.object(sys, 'argv', test_args): |
| with contextlib.redirect_stdout(out_buf): |
| with contextlib.redirect_stderr(err_buf): |
| rlc.main() |
| self.assertIn('REPORT 100%', out_buf.getvalue()) |
| self.assertIn('exited with 1', err_buf.getvalue()) |
| |
| def test_main_merge_failure(self) -> None: |
| """Verifies sys.exit(1) when profile merge step fails in main.""" |
| with mock.patch.object(pathlib.Path, 'mkdir'): |
| with mock.patch('run_local_coverage.run_test_binary', return_value=0): |
| with mock.patch('run_local_coverage.merge_profiles', |
| return_value=False): |
| test_args = ['rlc.py', '--binary', 'net_unittests'] |
| with mock.patch.object(sys, 'argv', test_args): |
| with self.assertRaises(SystemExit) as cm: |
| rlc.main() |
| self.assertEqual(cm.exception.code, 1) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |