Viewing files at commit c467b16393a4Commit detailsView latest

Browse files
tests/test_vcs_conformance.py 130 lines · 6.3 KB · python
1
"""Positive CLI checks and fault-injected negative oracle controls."""
2
import os
3
from pathlib import Path
4
import shutil
5
import subprocess
6
import sys
7
import tempfile
8
import unittest
9
from unittest.mock import patch
10
11
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
12
from oakbench.conformance import (Repository, SCENARIOS, assert_state,
13
                                 run_scenario, tree_manifest)
14
15
16
class ManifestTests(unittest.TestCase):
17
    def test_exact_oracle_rejects_each_loss_class(self):
18
        with tempfile.TemporaryDirectory() as tmp:
19
            root = Path(tmp)
20
            target = root / "payload"
21
            target.write_bytes(b"sentinel\x00\xff")
22
            expected = tree_manifest(root)
23
            target.write_bytes(b"sentinel\x00\xfe")
24
            with self.assertRaises(AssertionError):
25
                assert_state(expected, tree_manifest(root))
26
            target.write_bytes(b"sentinel\x00\xff")
27
            target.rename(root / "renamed")
28
            with self.assertRaises(AssertionError):
29
                assert_state(expected, tree_manifest(root))
30
            (root / "renamed").rename(target)
31
            (root / "extra").write_bytes(b"")
32
            with self.assertRaises(AssertionError):
33
                assert_state(expected, tree_manifest(root))
34
            (root / "extra").unlink()
35
            if os.name == "posix":
36
                target.chmod(0o755)
37
                with self.assertRaises(AssertionError):
38
                    assert_state(expected, tree_manifest(root))
39
40
    @unittest.skipUnless(os.name == "posix", "POSIX symlinks required")
41
    def test_symlink_target_type_and_no_traversal(self):
42
        with tempfile.TemporaryDirectory() as tmp:
43
            root = Path(tmp)
44
            (root / "link").symlink_to("missing")
45
            expected = tree_manifest(root)
46
            (root / "link").unlink()
47
            (root / "link").symlink_to("different")
48
            with self.assertRaises(AssertionError):
49
                assert_state(expected, tree_manifest(root))
50
            (root / "link").unlink()
51
            (root / "link").write_text("missing")
52
            with self.assertRaises(AssertionError):
53
                assert_state(expected, tree_manifest(root))
54
            (root / "cycle").symlink_to(".")
55
            self.assertEqual(tree_manifest(root)["cycle"], {"kind": "symlink", "target": "."})
56
57
    def test_missing_binary_is_explicit_unmeasured_skip(self):
58
        with tempfile.TemporaryDirectory() as tmp:
59
            row = run_scenario(Path(tmp), "oak", "/nonexistent/conformance-oak", SCENARIOS[0])
60
            self.assertEqual(row["returncode"], 77)
61
            self.assertIsNone(row["integrity_ok"])
62
            self.assertIsNone(row["wall_ms"])
63
            self.assertTrue(row["skip_reason"])
64
65
66
@unittest.skipUnless(shutil.which("git"), "Git binary required")
67
class RealGitControls(unittest.TestCase):
68
    @unittest.skipUnless(os.name == "posix", "POSIX symlinks required")
69
    def test_relative_binary_resolved_before_fixture_cwd_change(self):
70
        with tempfile.TemporaryDirectory() as tmp:
71
            root = Path(tmp)
72
            (root / "git-subject").symlink_to(shutil.which("git"))
73
            relative_binary = os.path.relpath(root / "git-subject")
74
            row = run_scenario(root / "work", "git", relative_binary, "byte_roundtrip_v1")
75
            self.assertEqual(row["returncode"], 0, row.get("failure"))
76
            self.assertTrue(Path(row["binary"]).is_absolute())
77
78
    def test_five_positive_scenarios(self):
79
        with tempfile.TemporaryDirectory() as tmp:
80
            for scenario in SCENARIOS:
81
                with self.subTest(scenario=scenario):
82
                    row = run_scenario(Path(tmp), "git", "git", scenario)
83
                    if scenario == "mode_symlink_roundtrip_v1" and row["returncode"] == 77:
84
                        self.assertTrue(row["skip_reason"])
85
                    else:
86
                        self.assertEqual(row["returncode"], 0, row.get("failure"))
87
                        self.assertTrue(row["integrity_ok"])
88
89
    def test_noop_restore_cannot_pass(self):
90
        with tempfile.TemporaryDirectory() as tmp, patch.object(Repository, "restore"):
91
            row = run_scenario(Path(tmp), "git", "git", "byte_roundtrip_v1")
92
            self.assertEqual(row["returncode"], 1)
93
            self.assertFalse(row["integrity_ok"])
94
95
    def test_restore_cannot_erase_unrelated_work(self):
96
        real_restore = Repository.restore
97
        def lossy_restore(repo, paths, invalid_source=False):
98
            result = real_restore(repo, paths, invalid_source)
99
            (repo.root / "untracked.txt").unlink()
100
            return result
101
        with tempfile.TemporaryDirectory() as tmp, patch.object(Repository, "restore", lossy_restore):
102
            row = run_scenario(Path(tmp), "git", "git", "scoped_restore_preservation_v1")
103
            self.assertEqual(row["returncode"], 1)
104
            self.assertIn("untracked.txt", row["failure"])
105
106
    def test_expected_failure_cannot_hide_partial_write_or_success(self):
107
        def partial_failure(repo, paths, invalid_source=False):
108
            (repo.root / "other.txt").write_bytes(b"corrupted")
109
            return subprocess.CompletedProcess([], 1)
110
        for implementation in (partial_failure,
111
                               lambda *a, **kw: subprocess.CompletedProcess([], 0),
112
                               lambda *a, **kw: subprocess.CompletedProcess([], -9)):
113
            with self.subTest(implementation=implementation):
114
                with tempfile.TemporaryDirectory() as tmp, patch.object(Repository, "restore", implementation):
115
                    row = run_scenario(Path(tmp), "git", "git", "failed_restore_preservation_v1")
116
                    self.assertEqual(row["returncode"], 1)
117
                    self.assertFalse(row["integrity_ok"])
118
119
    def test_expected_failure_cannot_move_head_without_changing_tree(self):
120
        def move_head(repo, paths, invalid_source=False):
121
            repo.run("commit", "--allow-empty", "-m", "unexpected state mutation")
122
            return subprocess.CompletedProcess([], 1)
123
        with tempfile.TemporaryDirectory() as tmp, patch.object(Repository, "restore", move_head):
124
            row = run_scenario(Path(tmp), "git", "git", "failed_restore_preservation_v1")
125
            self.assertEqual(row["returncode"], 1)
126
            self.assertIn("head", row["failure"])
127
128
129
if __name__ == "__main__":
130
    unittest.main()