94 lines
4.2 KiB
Python
94 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
RUNTIME = ROOT / "harness/runtime"
|
|
sys.path.insert(0, str(RUNTIME))
|
|
import contract_projection # noqa: E402
|
|
import fs_transaction # noqa: E402
|
|
import typed_contract_check # noqa: E402
|
|
|
|
|
|
class ContractProjectionTest(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.tempdir = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.tempdir.cleanup)
|
|
self.root = Path(self.tempdir.name)
|
|
(self.root / "harness/source").mkdir(parents=True)
|
|
shutil.copy2(ROOT / "harness/source/typed-contracts.json", self.root / "harness/source/typed-contracts.json")
|
|
(self.root / "raw/project-notes").mkdir(parents=True)
|
|
branches = self.root / "raw/branch-notes"
|
|
branches.mkdir(parents=True)
|
|
(self.root / "schemas").mkdir()
|
|
(self.root / "schemas/a.json").write_text("{}\n", encoding="utf-8")
|
|
(self.root / "raw/project-notes/demo.md").write_text(
|
|
"""---
|
|
title: demo
|
|
---
|
|
<!-- section-id: artifact-registry -->
|
|
## Artifact Registry
|
|
| Artifact ID | Revision | Name | Schema Owner | Producer | Consumers | Schema Ref | Status |
|
|
|---|---:|---|---|---|---|---|---|
|
|
| `ART-DEMO-A-001` | 1 | `a.json` | `feature-owner-contract` | `feature-owner-contract` | `feature-consumer-one`, `feature-consumer-two` | `schemas/a.json` | active |
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
(branches / "feature-owner-contract.md").write_text("---\ntitle: owner\n---\n# owner\n", encoding="utf-8")
|
|
for slug in ("feature-consumer-one", "feature-consumer-two"):
|
|
(branches / f"{slug}.md").write_text(
|
|
f"---\ntitle: {slug}\nimports: [ART-DEMO-A-001@1]\n---\n# {slug}\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
def test_all_consumer_projections_are_one_transaction_and_idempotent(self) -> None:
|
|
updates, result = contract_projection.build_updates(self.root)
|
|
self.assertEqual(result["status"], "DRIFT")
|
|
self.assertEqual(len(updates), 2)
|
|
with mock.patch.object(contract_projection, "replace_many") as replace:
|
|
code = contract_projection.main(["--root", str(self.root), "--write"])
|
|
self.assertEqual(code, 0)
|
|
replace.assert_called_once()
|
|
self.assertEqual(len(replace.call_args.args[0]), 2)
|
|
|
|
fs_transaction.replace_many({path: text.encode("utf-8") for path, text in updates.items()})
|
|
second, current = contract_projection.build_updates(self.root)
|
|
self.assertEqual((second, current["status"]), ({}, "CURRENT"))
|
|
self.assertEqual(typed_contract_check.check(self.root)["status"], "PASS")
|
|
|
|
def test_mid_commit_failure_restores_every_consumer(self) -> None:
|
|
updates, _result = contract_projection.build_updates(self.root)
|
|
before = {path: path.read_bytes() for path in updates}
|
|
original = fs_transaction.os.replace
|
|
calls = 0
|
|
|
|
def fail_second(source: object, target: object) -> None:
|
|
nonlocal calls
|
|
calls += 1
|
|
if calls == 2:
|
|
raise OSError("injected projection failure")
|
|
original(source, target)
|
|
|
|
with mock.patch.object(fs_transaction.os, "replace", side_effect=fail_second):
|
|
with self.assertRaises(fs_transaction.TransactionError):
|
|
fs_transaction.replace_many({path: text.encode("utf-8") for path, text in updates.items()})
|
|
self.assertEqual({path: path.read_bytes() for path in updates}, before)
|
|
|
|
def test_manual_projection_edit_reports_artifact_drift(self) -> None:
|
|
updates, _result = contract_projection.build_updates(self.root)
|
|
fs_transaction.replace_many({path: text.encode("utf-8") for path, text in updates.items()})
|
|
target = self.root / "raw/branch-notes/feature-consumer-one.md"
|
|
target.write_text(target.read_text(encoding="utf-8").replace("a.json", "other.json"), encoding="utf-8")
|
|
result = typed_contract_check.check(self.root)
|
|
self.assertIn("ARTIFACT_PROJECTION_DRIFT", {item["code"] for item in result["findings"]})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|