| #!/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. | |
| """Tests for measure_fuzzilli_coverage.py.""" | |
| import os | |
| import subprocess | |
| import unittest | |
| from unittest import mock | |
| import measure_fuzzilli_coverage | |
| from pathlib import Path | |
| COVERAGE_SCRIPT_PATH = ( | |
| f"{Path(__file__).resolve().parent.parents[1]}" | |
| "/tools/code_coverage/coverage.py" | |
| ) | |
| TEST_FUZZILLI_DIR = '/fake/fuzzilli' | |
| TEST_BUILD_OUT_DIR = '/fake/out/Fuzzilli' | |
| TEST_REPORT_OUT_DIR = '/fake/out/report' | |
| TEST_MINUTES = 2 | |
| TEST_SECONDS = 120 | |
| TEST_PROFILE = "fakeProfile" | |
| TEST_PROFRAW_DIR = '/tmp/fake_profraw_dir' | |
| class MeasureFuzzilliCoverageTest(unittest.TestCase): | |
| @mock.patch.dict(os.environ, {}, clear=True) | |
| @mock.patch('subprocess.Popen') | |
| @mock.patch('subprocess.run') | |
| def test_RunFuzzilli(self, mock_run, mock_popen): | |
| mock_process = mock.Mock() | |
| mock_process.wait.side_effect = subprocess.TimeoutExpired( | |
| cmd='FuzzilliCli', timeout=TEST_SECONDS | |
| ) | |
| mock_popen.return_value = mock_process | |
| mock_pg_handler = mock.Mock() | |
| success = measure_fuzzilli_coverage._RunFuzzilli( | |
| mock_pg_handler, | |
| TEST_FUZZILLI_DIR, | |
| TEST_BUILD_OUT_DIR, | |
| TEST_MINUTES, | |
| TEST_PROFILE, | |
| TEST_PROFRAW_DIR, | |
| ) | |
| self.assertTrue(success) | |
| self.assertEqual( | |
| os.environ.get("LLVM_PROFILE_FILE"), | |
| os.path.join(TEST_PROFRAW_DIR, "fuzzilli.%4m%c.profraw"), | |
| ) | |
| mock_run.assert_called_with( | |
| ['swift', 'build', '-c', 'release'], check=True, cwd=TEST_FUZZILLI_DIR | |
| ) | |
| expected_run_command_prefix = [ | |
| os.path.join(TEST_FUZZILLI_DIR, '.build/release/FuzzilliCli'), | |
| '--storagePath=/tmp/fuzzilli_storage', | |
| '--overwrite', | |
| '--engine=hybrid', | |
| '--profile=fakeProfile', | |
| ] | |
| self.assertTrue(mock_popen.called) | |
| actual_run_command = mock_popen.call_args[0][0] | |
| self.assertEqual(actual_run_command[:-1], expected_run_command_prefix) | |
| self.assertTrue(actual_run_command[-1].endswith('.sh')) | |
| self.assertEqual( | |
| mock_popen.call_args[1], | |
| {'cwd': TEST_FUZZILLI_DIR, 'start_new_session': True}, | |
| ) | |
| mock_process.wait.assert_called_with(timeout=TEST_SECONDS) | |
| mock_pg_handler.SetProcessGroup.assert_called_with(mock_process) | |
| mock_pg_handler.KillProcessGroup.assert_called_once() | |
| @mock.patch.dict(os.environ, {}, clear=True) | |
| @mock.patch('subprocess.Popen') | |
| @mock.patch('subprocess.run') | |
| def test_RunFuzzilliBuildFails(self, mock_run, mock_popen): | |
| mock_run.side_effect = subprocess.CalledProcessError( | |
| returncode=1, cmd='swift build' | |
| ) | |
| mock_pg_handler = mock.Mock() | |
| success = measure_fuzzilli_coverage._RunFuzzilli( | |
| mock_pg_handler, | |
| TEST_FUZZILLI_DIR, | |
| TEST_BUILD_OUT_DIR, | |
| TEST_MINUTES, | |
| TEST_PROFILE, | |
| TEST_PROFRAW_DIR, | |
| ) | |
| self.assertFalse(success) | |
| mock_run.assert_called_with( | |
| ['swift', 'build', '-c', 'release'], check=True, cwd=TEST_FUZZILLI_DIR | |
| ) | |
| mock_popen.assert_not_called() | |
| mock_pg_handler.SetProcessGroup.assert_not_called() | |
| mock_pg_handler.KillProcessGroup.assert_not_called() | |
| @mock.patch.dict(os.environ, {}, clear=True) | |
| @mock.patch('subprocess.Popen') | |
| @mock.patch('subprocess.run') | |
| def test_RunFuzzilliEarlyExit(self, mock_run, mock_popen): | |
| mock_process = mock.Mock() | |
| # As if crash before timeout reached | |
| mock_process.returncode = 1 | |
| mock_popen.return_value = mock_process | |
| mock_pg_handler = mock.Mock() | |
| success = measure_fuzzilli_coverage._RunFuzzilli( | |
| mock_pg_handler, | |
| TEST_FUZZILLI_DIR, | |
| TEST_BUILD_OUT_DIR, | |
| TEST_MINUTES, | |
| TEST_PROFILE, | |
| TEST_PROFRAW_DIR, | |
| ) | |
| self.assertFalse(success) | |
| mock_process.wait.assert_called_with(timeout=TEST_SECONDS) | |
| mock_pg_handler.SetProcessGroup.assert_called_with(mock_process) | |
| mock_pg_handler.KillProcessGroup.assert_not_called() | |
| def _assert_generate_coverage_report( | |
| self, mock_run, mock_glob, filters=None, ignore_regex=None | |
| ): | |
| if not filters: | |
| filters = [] | |
| mock_glob.return_value = [os.path.join(TEST_PROFRAW_DIR, '1.profraw')] | |
| measure_fuzzilli_coverage._GenerateCoverageReport( | |
| TEST_BUILD_OUT_DIR, | |
| TEST_REPORT_OUT_DIR, | |
| ignore_filename_regex=ignore_regex, | |
| filters=filters, | |
| profraw_dir=TEST_PROFRAW_DIR, | |
| ) | |
| expected_gen_profile_command = [ | |
| 'llvm-profdata', | |
| 'merge', | |
| '-o', | |
| os.path.join(TEST_PROFRAW_DIR, 'coverage.profdata'), | |
| os.path.join(TEST_PROFRAW_DIR, '1.profraw'), | |
| ] | |
| expected_coverage_command = [ | |
| COVERAGE_SCRIPT_PATH, | |
| 'js_in_process_fuzzer', | |
| '-b', | |
| TEST_BUILD_OUT_DIR, | |
| '-o', | |
| TEST_REPORT_OUT_DIR, | |
| '-p', | |
| os.path.join(TEST_PROFRAW_DIR, 'coverage.profdata'), | |
| '--no-component-view', | |
| ] | |
| for f in filters: | |
| expected_coverage_command.extend(['-f', f]) | |
| if ignore_regex: | |
| expected_coverage_command.extend(['-i', ignore_regex]) | |
| mock_run.assert_has_calls( | |
| [ | |
| mock.call(expected_gen_profile_command, check=True), | |
| mock.call(expected_coverage_command, check=True), | |
| ] | |
| ) | |
| @mock.patch('glob.glob') | |
| @mock.patch('subprocess.run') | |
| def test_GenerateCoverageReport(self, mock_run, mock_glob): | |
| self._assert_generate_coverage_report(mock_run, mock_glob) | |
| @mock.patch('glob.glob') | |
| @mock.patch('subprocess.run') | |
| def test_GenerateCoverageReportWithFilters(self, mock_run, mock_glob): | |
| self._assert_generate_coverage_report( | |
| mock_run, mock_glob, filters=['/fake/directory'] | |
| ) | |
| @mock.patch('glob.glob') | |
| @mock.patch('subprocess.run') | |
| def test_GenerateCoverageReportWithIgnoreRegex(self, mock_run, mock_glob): | |
| self._assert_generate_coverage_report( | |
| mock_run, mock_glob, ignore_regex='.*out.*' | |
| ) | |
| @mock.patch('glob.glob') | |
| @mock.patch('subprocess.run') | |
| def test_GenerateCoverageReportWithFiltersAndIgnoreRegex( | |
| self, mock_run, mock_glob | |
| ): | |
| self._assert_generate_coverage_report( | |
| mock_run, mock_glob, filters=['/fake/directory'], ignore_regex='.*out.*' | |
| ) | |
| @mock.patch('glob.glob') | |
| @mock.patch('subprocess.run') | |
| def test_GenerateCoverageReportFails(self, mock_run, mock_glob): | |
| mock_glob.return_value = [os.path.join(TEST_PROFRAW_DIR, '1.profraw')] | |
| mock_run.side_effect = subprocess.CalledProcessError( | |
| returncode=1, cmd='llvm-profdata' | |
| ) | |
| success = measure_fuzzilli_coverage._GenerateCoverageReport( | |
| TEST_BUILD_OUT_DIR, | |
| TEST_REPORT_OUT_DIR, | |
| ignore_filename_regex=None, | |
| filters=None, | |
| profraw_dir=TEST_PROFRAW_DIR, | |
| ) | |
| self.assertFalse(success) | |
| @mock.patch('glob.glob') | |
| @mock.patch('subprocess.run') | |
| def test_GenerateCoverageReportFailsFileNotFoundError( | |
| self, mock_run, mock_glob | |
| ): | |
| mock_glob.return_value = [os.path.join(TEST_PROFRAW_DIR, '1.profraw')] | |
| mock_run.side_effect = FileNotFoundError() | |
| success = measure_fuzzilli_coverage._GenerateCoverageReport( | |
| TEST_BUILD_OUT_DIR, | |
| TEST_REPORT_OUT_DIR, | |
| ignore_filename_regex=None, | |
| filters=None, | |
| profraw_dir=TEST_PROFRAW_DIR, | |
| ) | |
| self.assertFalse(success) | |
| if __name__ == '__main__': | |
| unittest.main() |