#!/usr/bin/env python3 """Run unit tests and enforce a maximum execution-time budget. Usage: python3 scripts/check_test_speed.py [--max-seconds N] """ from __future__ import annotations import re import subprocess # nosec B404 import click from gitea_runner_manager.i18n import _ DEFAULT_MAX_SECONDS = 2.0 TEST_COMMAND = ["make", "test-unit"] _TIMING_RE = re.compile(r"(\d+) passed.* in ([0-9.]+)s") def run_tests() -> tuple[str, str]: """Execute the unit-test suite and return (stdout, stderr).""" result = subprocess.run( # nosec B603 TEST_COMMAND, capture_output=True, text=True, check=False, ) return result.stdout, result.stderr def parse_duration(output: str) -> float: """Extract elapsed seconds from pytest summary line. Raises: click.ClickException: when the timing line cannot be found. """ for line in output.splitlines(): match = _TIMING_RE.search(line) if match: return float(match.group(2)) raise click.ClickException(_("Could not parse test execution time from output.")) def check_speed(duration: float, max_seconds: float) -> None: """Validate duration is within budget; raise on violation.""" if duration > max_seconds: raise click.ClickException( _( "Unit tests too slow: {duration:.2f}s (max allowed: {max}s).\n" " Fix: run 'make pytest-cov' to profile, then optimise slow tests.\n" " Hint: avoid unnecessary imports, use lighter mocks, or cache fixtures.", duration=duration, max=max_seconds, ) ) def main(max_seconds: float) -> None: """Run tests, parse timing, and enforce the budget.""" stdout, stderr = run_tests() combined = stdout + "\n" + stderr click.echo(combined, err=False) duration = parse_duration(combined) check_speed(duration, max_seconds) click.echo( _( "Unit tests passed in {duration:.2f}s (under {max}s limit).", duration=duration, max=max_seconds, ) ) @click.command() @click.option( "--max-seconds", type=float, default=DEFAULT_MAX_SECONDS, show_default=True, help="Maximum allowed execution time in seconds.", ) def cli(max_seconds: float) -> None: main(max_seconds) if __name__ == "__main__": # pragma: no cover cli() # pragma: no cover