#!/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.
"""Compiles profiles and generates coverage reports for local test binaries."""

import argparse
import os
import pathlib
import subprocess
import sys


def find_llvm_tool(tool_name: str) -> pathlib.Path:
  """Locates pinned LLVM tool inside third_party.

  Args:
      tool_name: Executable name (e.g., 'llvm-profdata').

  Returns:
      Absolute path to the pinned tool executable.
  """
  base = pathlib.Path(__file__).resolve().parents[4]
  tool_path = base / 'third_party' / 'llvm-build' / 'Release+Asserts' / 'bin'
  exec_path = tool_path / tool_name
  if not exec_path.exists():
    print(
      f'Downloading missing LLVM coverage tool ({tool_name})...',
      file=sys.stderr,
    )
    update_script = base / 'tools' / 'clang' / 'scripts' / 'update.py'
    try:
      subprocess.check_call(
        [
          sys.executable,
          str(update_script),
          '--package',
          'coverage_tools',
        ]
      )
    except subprocess.CalledProcessError as e:
      print(f'Failed to download LLVM coverage tools: {e}', file=sys.stderr)
      sys.exit(1)

    if not exec_path.exists():
      print(f'LLVM tool still not found at {exec_path}', file=sys.stderr)
      sys.exit(1)
  return exec_path


def run_test_binary(
  binary_path: pathlib.Path,
  profraw_dir: pathlib.Path,
  extra_args: list[str],
) -> int:
  """Runs local test binary with LLVM_PROFILE_FILE environment variable.

  Args:
      binary_path: Path to executable test binary.
      profraw_dir: Scratch directory to store raw profiles.
      extra_args: Additional arguments passed to the binary.

  Returns:
      Subprocess return code integer.
  """
  env = os.environ.copy()
  prof_template = profraw_dir / f'{binary_path.name}.%4m%c.profraw'
  env['LLVM_PROFILE_FILE'] = str(prof_template)
  cmd = [str(binary_path)] + extra_args
  res = subprocess.run(cmd, env=env, check=False)
  return res.returncode


def merge_profiles(
  profraw_dir: pathlib.Path,
  output_profdata: pathlib.Path,
) -> bool:
  """Merges profraw files into indexed profdata artifact.

  Args:
      profraw_dir: Directory containing .profraw files.
      output_profdata: Output .profdata file path.

  Returns:
      True if successful, False otherwise.
  """
  profdata = find_llvm_tool('llvm-profdata')
  raw_files = list(profraw_dir.glob('*.profraw'))
  if not raw_files:
    print(f'No profraw files found in {profraw_dir}', file=sys.stderr)
    return False
  cmd = [str(profdata), 'merge', '-o', str(output_profdata), '--sparse']
  cmd += [str(f) for f in raw_files]
  res = subprocess.run(cmd, check=False)
  return res.returncode == 0


def export_coverage(
  binary_path: pathlib.Path,
  profdata_path: pathlib.Path,
  source_file: str | None,
) -> str | None:
  """Runs llvm-cov export to return text report or summary.

  Args:
      binary_path: Path to instrumented binary.
      profdata_path: Path to merged .profdata artifact.
      source_file: Optional source file filter.

  Returns:
      Exported coverage text string or None on error.
  """
  cov_tool = find_llvm_tool('llvm-cov')
  cmd = [
    str(cov_tool),
    'show',
    str(binary_path),
    f'-instr-profile={profdata_path}',
  ]
  if source_file:
    cmd.append(source_file)
  res = subprocess.run(cmd, capture_output=True, text=True, check=False)
  if res.returncode != 0:
    print(f'llvm-cov failed: {res.stderr}', file=sys.stderr)
    return None
  return res.stdout


def compile_binary(binary_path: pathlib.Path) -> bool:
  """Compiles target test binary locally using autoninja.

  Args:
      binary_path: Path to executable test binary.

  Returns:
      True if compilation succeeded, False otherwise.
  """
  build_dir = binary_path.parent
  target_name = binary_path.name
  print(f'Compiling target {target_name} in {build_dir}...', file=sys.stderr)
  cmd = ['autoninja', '-C', str(build_dir), target_name]
  res = subprocess.run(cmd, check=False)
  return res.returncode == 0


def main() -> None:
  """Parses CLI args and executes rapid local coverage generation."""
  parser = argparse.ArgumentParser(description=__doc__)
  parser.add_argument(
    '--binary', required=True, type=pathlib.Path, help='Path to test binary'
  )
  parser.add_argument(
    '--scratch',
    type=pathlib.Path,
    default=pathlib.Path('scratch/local_prof'),
    help='Scratch directory for profiles',
  )
  parser.add_argument('--source', help='Optional source file filter')
  parser.add_argument(
    '--no-compile',
    action='store_true',
    help='Skip autoninja target compilation step if binary is pre-built',
  )
  parser.add_argument(
    'test_args',
    nargs=argparse.REMAINDER,
    help='Arguments passed to test binary',
  )
  args = parser.parse_args()

  if not args.no_compile:
    if not compile_binary(args.binary):
      print(f'Failed to compile target {args.binary.name}', file=sys.stderr)
      sys.exit(1)

  args.scratch.mkdir(parents=True, exist_ok=True)
  code = run_test_binary(args.binary, args.scratch, args.test_args)
  if code != 0:
    print(f'Warning: test binary exited with {code}', file=sys.stderr)

  profdata = args.scratch / f'{args.binary.name}.profdata'
  if not merge_profiles(args.scratch, profdata):
    sys.exit(1)

  report = export_coverage(args.binary, profdata, args.source)
  if report:
    print(report)


if __name__ == '__main__':
  main()
