Viewing files at commit 1a441edafc71Commit detailsView latest

Browse files
tests/test_fixture_registry.py 187 lines · 7.4 KB · python
1
from __future__ import annotations
2
3
import json
4
import subprocess
5
import sys
6
import tempfile
7
import unittest
8
from pathlib import Path
9
10
ROOT = Path(__file__).resolve().parents[1]
11
sys.path.insert(0, str(ROOT / "scripts"))
12
13
from oakbench.fixture_registry import load_fixture_registry, tree_manifest_sha256, verify_fixture
14
15
16
def run_fixture_ensure(args: list[str]) -> subprocess.CompletedProcess:
17
    return subprocess.run(
18
        [sys.executable, "scripts/fixture_ensure.py", *args],
19
        cwd=ROOT,
20
        text=True,
21
        stdout=subprocess.PIPE,
22
        stderr=subprocess.PIPE,
23
        check=False,
24
    )
25
26
27
def write_registry(path: Path, manifest: str | None) -> Path:
28
    path.write_text(
29
        json.dumps(
30
            {
31
                "schema_version": 1,
32
                "fixtures": {
33
                    "tiny-v1": {
34
                        "generator": "scripts/none.py",
35
                        "version": "v1",
36
                        "seed": "tiny-seed",
37
                        "description": "tiny test fixture",
38
                        "manifest": manifest,
39
                        "license": "synthetic",
40
                        "hosting": ["local"],
41
                    }
42
                },
43
            }
44
        )
45
    )
46
    return path
47
48
49
class FixtureRegistryTest(unittest.TestCase):
50
    def test_loads_committed_registry(self) -> None:
51
        registry = load_fixture_registry()
52
        self.assertIn("large-mixed-v1", registry)
53
        self.assertEqual(registry["large-mixed-v1"].row_fields()["fixture_seed"], "oakbench-large-v1")
54
55
    def test_tree_manifest_is_deterministic_and_path_sensitive(self) -> None:
56
        with tempfile.TemporaryDirectory() as tmp:
57
            root = Path(tmp)
58
            (root / "a").write_text("one")
59
            (root / "dir").mkdir()
60
            (root / "dir" / "b").write_text("two")
61
            first = tree_manifest_sha256(root)
62
            second = tree_manifest_sha256(root)
63
            self.assertEqual(first, second)
64
            (root / "dir" / "b").write_text("three")
65
            self.assertNotEqual(first, tree_manifest_sha256(root))
66
67
    def test_verify_fixture_without_recorded_manifest_is_unverifiable(self) -> None:
68
        # Null manifest means unmeasured: an existing tree must not claim
69
        # verified, only report the actual hash as unverifiable.
70
        spec = load_fixture_registry()["large-mixed-v1"]
71
        with tempfile.TemporaryDirectory() as tmp:
72
            root = Path(tmp)
73
            (root / "a").write_text("one")
74
            status, actual = verify_fixture(spec, root)
75
            self.assertEqual(status, "unverifiable:manifest_null")
76
            self.assertIsNotNone(actual)
77
78
    def test_verify_fixture_missing_root_fails_without_hash(self) -> None:
79
        spec = load_fixture_registry()["large-mixed-v1"]
80
        with tempfile.TemporaryDirectory() as tmp:
81
            status, actual = verify_fixture(spec, Path(tmp) / "missing")
82
            self.assertEqual(status, "failed:path_missing")
83
            self.assertIsNone(actual)
84
85
    def test_verify_fixture_file_root_fails_without_hash(self) -> None:
86
        spec = load_fixture_registry()["large-mixed-v1"]
87
        with tempfile.TemporaryDirectory() as tmp:
88
            file_path = Path(tmp) / "file"
89
            file_path.write_text("not a directory")
90
            status, actual = verify_fixture(spec, file_path)
91
            self.assertEqual(status, "failed:not_a_directory")
92
            self.assertIsNone(actual)
93
94
    def test_verify_fixture_empty_root_fails_without_hash(self) -> None:
95
        spec = load_fixture_registry()["large-mixed-v1"]
96
        with tempfile.TemporaryDirectory() as tmp:
97
            status, actual = verify_fixture(spec, Path(tmp))
98
            self.assertEqual(status, "failed:tree_empty")
99
            self.assertIsNone(actual)
100
101
    def test_verify_fixture_pinned_manifest_matches_and_mismatches(self) -> None:
102
        with tempfile.TemporaryDirectory() as tmp:
103
            root = Path(tmp)
104
            (root / "a").write_text("one")
105
            manifest = tree_manifest_sha256(root)
106
            registry_path = write_registry(root.parent / f"{root.name}-registry.json", manifest)
107
            try:
108
                spec = load_fixture_registry(registry_path)["tiny-v1"]
109
                self.assertEqual(verify_fixture(spec, root), ("verified", manifest))
110
                (root / "a").write_text("two")
111
                status, actual = verify_fixture(spec, root)
112
                self.assertEqual(status, "failed:manifest_mismatch")
113
                self.assertNotEqual(actual, manifest)
114
            finally:
115
                registry_path.unlink()
116
117
118
class FixtureEnsureTest(unittest.TestCase):
119
    def test_missing_path_is_nonzero_with_reason(self) -> None:
120
        with tempfile.TemporaryDirectory() as tmp:
121
            proc = run_fixture_ensure(
122
                [
123
                    "--fixture-id",
124
                    "large-mixed-v1",
125
                    "--path",
126
                    str(Path(tmp) / "missing"),
127
                    "--json",
128
                ]
129
            )
130
            self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
131
            row = json.loads(proc.stdout)[0]
132
            self.assertIs(row["verified"], False)
133
            self.assertEqual(row["reason"], "path_missing")
134
            self.assertIsNone(row["actual_manifest"])
135
136
    def test_empty_dir_is_nonzero_with_reason(self) -> None:
137
        with tempfile.TemporaryDirectory() as tmp:
138
            proc = run_fixture_ensure(
139
                ["--fixture-id", "large-mixed-v1", "--path", tmp, "--json"]
140
            )
141
            self.assertEqual(proc.returncode, 1, proc.stdout + proc.stderr)
142
            row = json.loads(proc.stdout)[0]
143
            self.assertIs(row["verified"], False)
144
            self.assertEqual(row["reason"], "tree_empty")
145
146
    def test_manifest_null_existing_tree_is_unverifiable_not_verified(self) -> None:
147
        with tempfile.TemporaryDirectory() as tmp:
148
            root = Path(tmp)
149
            (root / "a").write_text("one")
150
            proc = run_fixture_ensure(
151
                ["--fixture-id", "large-mixed-v1", "--path", str(root), "--json"]
152
            )
153
            self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
154
            row = json.loads(proc.stdout)[0]
155
            self.assertIsNone(row["verified"])
156
            self.assertEqual(row["reason"], "manifest_null")
157
            self.assertIsNotNone(row["actual_manifest"])
158
159
    def test_pinned_manifest_matching_tree_is_verified_true(self) -> None:
160
        with tempfile.TemporaryDirectory() as tmp:
161
            root = Path(tmp) / "fixture"
162
            root.mkdir()
163
            (root / "a").write_text("one")
164
            (root / "dir").mkdir()
165
            (root / "dir" / "b").write_text("two")
166
            manifest = tree_manifest_sha256(root)
167
            registry_path = write_registry(Path(tmp) / "registry.json", manifest)
168
            proc = run_fixture_ensure(
169
                [
170
                    "--registry",
171
                    str(registry_path),
172
                    "--fixture-id",
173
                    "tiny-v1",
174
                    "--path",
175
                    str(root),
176
                    "--json",
177
                ]
178
            )
179
            self.assertEqual(proc.returncode, 0, proc.stdout + proc.stderr)
180
            row = json.loads(proc.stdout)[0]
181
            self.assertIs(row["verified"], True)
182
            self.assertEqual(row["actual_manifest"], manifest)
183
            self.assertNotIn("reason", row)
184
185
186
if __name__ == "__main__":
187
    unittest.main()