#!/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()
