| 1 | from __future__ import annotations
|
| 2 |
|
| 3 | import contextlib
|
| 4 | import io
|
| 5 | import json
|
| 6 | import os
|
| 7 | import subprocess
|
| 8 | import sys
|
| 9 | import tempfile
|
| 10 | import unittest
|
| 11 | from pathlib import Path
|
| 12 |
|
| 13 | sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
| 14 |
|
| 15 | import crosscheck_hyperfine as crosscheck
|
| 16 |
|
| 17 |
|
| 18 | class HyperfineCrosscheckTest(unittest.TestCase):
|
| 19 | def test_skip_row_names_hyperfine_and_uses_skip_contract(self) -> None:
|
| 20 | row = crosscheck.skip_row(
|
| 21 | command=["oak", "status"],
|
| 22 | reason="hyperfine not found on PATH",
|
| 23 | bench_id="bench-1",
|
| 24 | scenario="s",
|
| 25 | operation="op",
|
| 26 | run=2,
|
| 27 | subject="oak",
|
| 28 | subject_kind="oak",
|
| 29 | )
|
| 30 |
|
| 31 | self.assertEqual(row["schema_version"], 1)
|
| 32 | self.assertEqual(row["returncode"], 77)
|
| 33 | self.assertTrue(row["skipped"])
|
| 34 | self.assertIn("hyperfine", row["skip_reason"])
|
| 35 | self.assertEqual(row["command"], ["oak", "status"])
|
| 36 | self.assertEqual(row["measurement_source"], "external_hyperfine")
|
| 37 |
|
| 38 | def test_measurement_row_converts_hyperfine_seconds_to_ms(self) -> None:
|
| 39 | result = {
|
| 40 | "command": "python -c pass",
|
| 41 | "mean": 0.012,
|
| 42 | "median": 0.01,
|
| 43 | "stddev": 0.002,
|
| 44 | "min": 0.009,
|
| 45 | "max": 0.02,
|
| 46 | "times": [0.009, 0.01, 0.02],
|
| 47 | }
|
| 48 |
|
| 49 | row = crosscheck.measurement_row(command=["python", "-c", "pass"], hyperfine_result=result)
|
| 50 |
|
| 51 | self.assertEqual(row["elapsed_ms"], 10.0)
|
| 52 | self.assertEqual(row["returncode"], 0)
|
| 53 | self.assertEqual(row["hyperfine_mean_ms"], 12.0)
|
| 54 | self.assertEqual(row["hyperfine_median_ms"], 10.0)
|
| 55 | self.assertEqual(row["hyperfine_stddev_ms"], 2.0)
|
| 56 | self.assertEqual(row["hyperfine_min_ms"], 9.0)
|
| 57 | self.assertEqual(row["hyperfine_max_ms"], 20.0)
|
| 58 | self.assertEqual(row["hyperfine_times_ms"], [9.0, 10.0, 20.0])
|
| 59 |
|
| 60 | def test_run_hyperfine_reads_exported_json_from_fake_runner(self) -> None:
|
| 61 | with tempfile.TemporaryDirectory(prefix="oakbench-hyperfine-test-") as tmp:
|
| 62 | fake = Path(tmp) / "hyperfine"
|
| 63 | fake.write_text("#!/bin/sh\nexit 0\n")
|
| 64 | fake.chmod(0o755)
|
| 65 |
|
| 66 | def runner(args, **kwargs): # type: ignore[no-untyped-def]
|
| 67 | export_path = Path(args[args.index("--export-json") + 1])
|
| 68 | export_path.write_text(json.dumps({"results": [{"command": "echo ok", "median": 0.003}]}))
|
| 69 | return subprocess.CompletedProcess(args, 0, stdout="", stderr="")
|
| 70 |
|
| 71 | result = crosscheck.run_hyperfine(
|
| 72 | ["echo", "ok"],
|
| 73 | cwd=Path(tmp),
|
| 74 | runs=2,
|
| 75 | warmup=0,
|
| 76 | executable=str(fake),
|
| 77 | env=os.environ.copy(),
|
| 78 | runner=runner,
|
| 79 | )
|
| 80 |
|
| 81 | self.assertEqual(result["command"], "echo ok")
|
| 82 | self.assertEqual(result["median"], 0.003)
|
| 83 |
|
| 84 | def test_main_missing_hyperfine_prints_clear_message(self) -> None:
|
| 85 | stderr = io.StringIO()
|
| 86 | with contextlib.redirect_stderr(stderr):
|
| 87 | code = crosscheck.main(["--hyperfine", "__oakbench_missing_hyperfine__", "--", "echo", "ok"])
|
| 88 |
|
| 89 | self.assertEqual(code, 77)
|
| 90 | self.assertIn("hyperfine not found on PATH", stderr.getvalue())
|
| 91 |
|
| 92 | def test_main_missing_hyperfine_can_emit_json_skip_row(self) -> None:
|
| 93 | stdout = io.StringIO()
|
| 94 | with contextlib.redirect_stdout(stdout):
|
| 95 | code = crosscheck.main(["--jsonl", "--hyperfine", "__oakbench_missing_hyperfine__", "--", "echo", "ok"])
|
| 96 |
|
| 97 | self.assertEqual(code, 77)
|
| 98 | row = json.loads(stdout.getvalue())
|
| 99 | self.assertEqual(row["returncode"], 77)
|
| 100 | self.assertTrue(row["skipped"])
|
| 101 | self.assertEqual(row["command"], ["echo", "ok"])
|
| 102 |
|
| 103 |
|
| 104 | if __name__ == "__main__":
|
| 105 | unittest.main()
|