Viewing files at commit f2acb751aa96Commit detailsView latest

Browse files
tests/test_stats.py 76 lines · 2.9 KB · python
1
from __future__ import annotations
2
3
import contextlib
4
import io
5
import json
6
import sys
7
import tempfile
8
import unittest
9
from pathlib import Path
10
from unittest.mock import patch
11
12
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
13
14
import benchmark_stats
15
from oakbench.stats import (
16
    benjamini_hochberg,
17
    bootstrap_ci,
18
    named_statistic,
19
    percentile_nearest,
20
    ratio_ci,
21
    required_n_for_relative_delta,
22
)
23
24
25
class StatsTest(unittest.TestCase):
26
    def test_percentile_nearest(self) -> None:
27
        self.assertEqual(percentile_nearest([1, 2, 3, 4], 50), 2)
28
        self.assertEqual(percentile_nearest([1, 2, 3, 4], 95), 4)
29
        self.assertIsNone(percentile_nearest([], 50))
30
31
    def test_bootstrap_ci_singleton(self) -> None:
32
        self.assertEqual(bootstrap_ci([7.0]), (7.0, 7.0))
33
34
    def test_zero_percentile_is_observed_zero_not_nan(self) -> None:
35
        self.assertEqual(named_statistic("p90")([0.0, 0.0, 0.0]), 0.0)
36
        self.assertEqual(bootstrap_ci([0.0] * 10, statistic="p90", samples=20), (0.0, 0.0))
37
38
    def test_ratio_ci_orders_bounds(self) -> None:
39
        low, high = ratio_ci([1, 2, 3], [2, 4, 6], samples=100, seed=1)
40
        self.assertLessEqual(low, high)
41
        self.assertGreater(low, 0)
42
43
    def test_bh_fdr(self) -> None:
44
        self.assertEqual(benjamini_hochberg([0.001, 0.02, 0.2], alpha=0.05), [True, True, False])
45
46
    def test_required_n_scales_with_square_of_noise_ratio(self) -> None:
47
        self.assertEqual(required_n_for_relative_delta(10, 5, 4), 16)
48
        self.assertEqual(required_n_for_relative_delta(3, 5, 4), 4)
49
        self.assertIsNone(required_n_for_relative_delta(3, 0, 4))
50
51
52
class BenchmarkStatsCliTest(unittest.TestCase):
53
    def test_null_returncode_row_is_excluded_not_a_crash(self) -> None:
54
        rows = [
55
            {"subject": "git", "scenario": "tiny", "operation": "status.clean", "returncode": 0, "elapsed_ms": 10.0},
56
            {"subject": "git", "scenario": "tiny", "operation": "status.clean", "returncode": 0, "elapsed_ms": 12.0},
57
            # Emitters may record returncode: null (unmeasured); it must be
58
            # treated as a failure and excluded, never raise TypeError.
59
            {"subject": "git", "scenario": "tiny", "operation": "status.clean", "returncode": None, "elapsed_ms": 999.0},
60
        ]
61
        with tempfile.TemporaryDirectory() as tmp:
62
            path = Path(tmp) / "rows.jsonl"
63
            path.write_text("\n".join(json.dumps(row) for row in rows) + "\n")
64
            stdout = io.StringIO()
65
            argv = ["benchmark_stats.py", str(path), "--bootstrap-samples", "10"]
66
            with patch.object(sys, "argv", argv):
67
                with contextlib.redirect_stdout(stdout):
68
                    code = benchmark_stats.main()
69
        self.assertEqual(code, 0)
70
        output = stdout.getvalue()
71
        self.assertIn("| `git` | `tiny/status.clean` | 2 |", output)
72
        self.assertNotIn("999", output)
73
74
75
if __name__ == "__main__":
76
    unittest.main()