"""Unit tests for scripts/check_test_speed.py.""" from unittest.mock import MagicMock, patch import click import pytest from click.testing import CliRunner from scripts.check_test_speed import ( DEFAULT_MAX_SECONDS, TEST_COMMAND, check_speed, cli, parse_duration, run_tests, ) class TestRunTests: @patch("scripts.check_test_speed.subprocess.run") def test_run_tests_returns_stdout_stderr(self, mock_run: MagicMock) -> None: mock_run.return_value = MagicMock(stdout="out", stderr="err", returncode=0) stdout, stderr = run_tests() assert stdout == "out" assert stderr == "err" mock_run.assert_called_once_with( TEST_COMMAND, capture_output=True, text=True, check=False, ) class TestParseDuration: def test_parses_valid_line(self) -> None: assert parse_duration("234 passed in 0.70s") == 0.70 def test_parses_with_warnings(self) -> None: assert parse_duration("293 passed, 1 warning in 0.45s") == 0.45 def test_parses_multiline_output(self) -> None: output = "some header\n234 passed in 1.23s\nfooter" assert parse_duration(output) == 1.23 def test_raises_when_no_timing_line(self) -> None: with pytest.raises(click.ClickException) as exc: parse_duration("no timing here") assert "Could not parse" in str(exc.value) class TestCheckSpeed: def test_under_budget_passes(self) -> None: check_speed(1.0, 2.0) # should not raise def test_exact_budget_passes(self) -> None: check_speed(2.0, 2.0) # should not raise def test_over_budget_raises(self) -> None: with pytest.raises(click.ClickException) as exc: check_speed(2.1, 2.0) msg = str(exc.value) assert "too slow" in msg.lower() assert "2.10s" in msg assert "max allowed: 2.0s" in msg def test_main_module_block() -> None: import scripts.check_test_speed as cts with patch.object(cts, "cli") as mock_cli: with patch.object(cts, "__name__", "__main__"): cts.cli([]) mock_cli.assert_called_once_with([]) class TestMain: @patch("scripts.check_test_speed.run_tests") @patch("scripts.check_test_speed.parse_duration") @patch("scripts.check_test_speed.check_speed") def test_successful_run( self, mock_check: MagicMock, mock_parse: MagicMock, mock_run: MagicMock, ) -> None: mock_run.return_value = ("stdout\n", "stderr\n") mock_parse.return_value = 1.5 runner = CliRunner() result = runner.invoke(cli, []) assert result.exit_code == 0 assert "1.50s" in result.output assert "under 2.0s limit" in result.output mock_run.assert_called_once() mock_parse.assert_called_once_with("stdout\n\nstderr\n") mock_check.assert_called_once_with(1.5, DEFAULT_MAX_SECONDS) @patch("scripts.check_test_speed.run_tests") @patch("scripts.check_test_speed.parse_duration") def test_slow_tests_exit( self, mock_parse: MagicMock, mock_run: MagicMock, ) -> None: mock_run.return_value = ("out\n", "err\n") mock_parse.return_value = 3.0 runner = CliRunner() result = runner.invoke(cli, []) assert result.exit_code == 1 assert "too slow" in result.output.lower() @patch("scripts.check_test_speed.run_tests") def test_parse_failure_exits( self, mock_run: MagicMock, ) -> None: mock_run.return_value = ("bad output\n", "") runner = CliRunner() result = runner.invoke(cli, []) assert result.exit_code == 1 assert "Could not parse" in result.output @patch("scripts.check_test_speed.run_tests") @patch("scripts.check_test_speed.parse_duration") @patch("scripts.check_test_speed.check_speed") def test_custom_max_seconds( self, mock_check: MagicMock, mock_parse: MagicMock, mock_run: MagicMock, ) -> None: mock_run.return_value = ("out\n", "err\n") mock_parse.return_value = 0.5 runner = CliRunner() result = runner.invoke(cli, ["--max-seconds", "1.5"]) assert result.exit_code == 0 mock_check.assert_called_once_with(0.5, 1.5)