blob: 2d49088b8ee524634bba3b04fe857651d0392332 [file]
#!/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()