mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Allow selecting reviewed native hook fragments for stamping
Keep unselected contracts pending, preserve merge order within the selection, and reject unknown names before writing. Co-Authored-By: Codex GPT-6 <[email protected]>
This commit is contained in:
1 parent
696d021699
commit
82011f3c6f
3 files changed
+109
-6
No files matched your search
@@ -6,6 +6,7 @@ import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
SPEC = importlib.util.spec_from_file_location(
|
||||
"stamp_native_hooks", Path(__file__).resolve().parents[1] / "tools/stamp_native_hooks.py"
|
||||
@@ -88,6 +89,84 @@ class StampTests(unittest.TestCase):
|
||||
self.assertEqual(json.loads((self.root / stamp.CATALOG).read_text())["hooks"], [])
|
||||
self.assertEqual(json.loads((self.root / stamp.CATALOG).read_text())["helpers"], fragment["helpers"])
|
||||
|
||||
def test_selected_fragments_keep_merge_order_and_others_stay_pending(self):
|
||||
self.git("add", ".")
|
||||
self.git("commit", "-qm", "base")
|
||||
# An older, unfinished API must not stop two later, reviewed APIs being registered.
|
||||
for name in ("blocked", "z", "a"):
|
||||
self.fragment(name)
|
||||
self.git("add", ".")
|
||||
self.git("commit", "-qm", name)
|
||||
blocked = self.root / stamp.FRAGMENTS / "blocked.json"
|
||||
pending_comment = self.root / "pending.rs"
|
||||
pending_comment.write_text("/// Since API @API:blocked@.\n")
|
||||
fragment = json.loads(blocked.read_text())
|
||||
fragment["comments"] = ["pending.rs"]
|
||||
blocked.write_text(json.dumps(fragment))
|
||||
api = self.root / stamp.MOD_API
|
||||
api.write_text("/// APIs @API:blocked@, @API:z@ and @API:a@.\n")
|
||||
before = {path: path.read_bytes() for path in (blocked, pending_comment)}
|
||||
|
||||
# CLI order must not override merge order; duplicate selections must not consume numbers.
|
||||
self.assertFalse(stamp.stamp(False, ["a", "z", "a"]))
|
||||
source = (self.root / stamp.SOURCE).read_text()
|
||||
self.assertIn("API 8 adds z.", source)
|
||||
self.assertIn("API 9 adds a.", source)
|
||||
self.assertIn("NATIVE_HOOKS_API: u32 = 9;", source)
|
||||
self.assertEqual(api.read_text(), "/// APIs @API:blocked@, 8 and 9.\n")
|
||||
docs = (self.root / stamp.DOCS).read_text()
|
||||
self.assertIn("API 8 adds z; requires 8.", docs)
|
||||
self.assertIn("API 9 adds a; requires 9.", docs)
|
||||
self.assertEqual({path: path.read_bytes() for path in before}, before)
|
||||
self.assertEqual(sorted(path.stem for path in blocked.parent.glob("*.json")), ["blocked"])
|
||||
hooks = json.loads((self.root / stamp.CATALOG).read_text())["hooks"]
|
||||
self.assertEqual([h["name"] for h in hooks], ["a", "z"])
|
||||
|
||||
# Once ready, the skipped API gets the next free number, even though it merged first.
|
||||
self.assertTrue(stamp.stamp(True))
|
||||
self.assertFalse(stamp.stamp(False))
|
||||
self.assertIn("API 10 adds blocked.", (self.root / stamp.SOURCE).read_text())
|
||||
self.assertIn("API 10 adds blocked; requires 10.", (self.root / stamp.DOCS).read_text())
|
||||
self.assertEqual(api.read_text(), "/// APIs 10, 8 and 9.\n")
|
||||
self.assertEqual(pending_comment.read_text(), "/// Since API 10.\n")
|
||||
hooks = json.loads((self.root / stamp.CATALOG).read_text())["hooks"]
|
||||
self.assertEqual([h["name"] for h in hooks], ["a", "z", "blocked"])
|
||||
self.assertFalse(stamp.stamp(False))
|
||||
|
||||
def test_selection_rejects_unknown_names_before_writing_even_in_check_mode(self):
|
||||
path = self.fragment("ready")
|
||||
before = {p: p.read_bytes() for p in self.root.rglob("*") if p.is_file()}
|
||||
for check in (False, True):
|
||||
with self.subTest(check=check):
|
||||
with self.assertRaisesRegex(ValueError, "unknown pending fragment.*typo"):
|
||||
stamp.stamp(check, ["ready", "typo"])
|
||||
self.assertEqual({p: p.read_bytes() for p in before}, before)
|
||||
path.unlink()
|
||||
with self.assertRaisesRegex(ValueError, "unknown pending fragment.*ready"):
|
||||
stamp.stamp(False, ["ready"])
|
||||
|
||||
def test_selection_cli_check_is_read_only_and_can_stamp_helper_only(self):
|
||||
self.git("add", ".")
|
||||
self.git("commit", "-qm", "base")
|
||||
path = self.fragment("ready")
|
||||
fragment = json.loads(path.read_text())
|
||||
fragment["hooks"] = []
|
||||
fragment["helpers"] = [{"name": "helper", "target": "target::helper"}]
|
||||
path.write_text(json.dumps(fragment))
|
||||
blocked = self.fragment("blocked")
|
||||
before = {p: p.read_bytes() for p in self.root.rglob("*") if p.is_file()}
|
||||
with patch("sys.argv", ["stamp_native_hooks.py", "--check", "--only", "ready"]):
|
||||
self.assertEqual(stamp.main(), 1)
|
||||
self.assertEqual({p: p.read_bytes() for p in before}, before)
|
||||
with patch("sys.argv", ["stamp_native_hooks.py", "--only", "ready"]):
|
||||
self.assertEqual(stamp.main(), 0)
|
||||
catalog = json.loads((self.root / stamp.CATALOG).read_text())
|
||||
self.assertEqual(catalog["hooks"], [])
|
||||
self.assertEqual(catalog["helpers"], fragment["helpers"])
|
||||
self.assertIn("NATIVE_HOOKS_API: u32 = 8;", (self.root / stamp.SOURCE).read_text())
|
||||
self.assertFalse(path.exists())
|
||||
self.assertEqual(blocked.read_bytes(), before[blocked])
|
||||
|
||||
def test_catalog_facts_travel_with_the_hook(self):
|
||||
self.git("add", ".")
|
||||
self.git("commit", "-qm", "base")
|
||||
|
||||
@@ -26,7 +26,23 @@ not API yet; do not depend on it until it has a number.
|
||||
|
||||
After hook PRs merge, run `python3 tools/stamp_native_hooks.py` on current
|
||||
main and open a PR with the result. The tool assigns one API number per
|
||||
fragment in first-parent merge order. It consumes all pending fragments.
|
||||
`--check` exits nonzero when any are pending. Running the tool again does
|
||||
nothing. For local testing only, run it on a hook branch without committing
|
||||
fragment in first-parent merge order. By default it consumes all pending
|
||||
fragments; running it again does nothing. `--check` exits nonzero when any
|
||||
are pending.
|
||||
|
||||
To register reviewed fragments while unrelated contracts are still being
|
||||
settled, select their names without `.json`:
|
||||
|
||||
```sh
|
||||
python3 tools/stamp_native_hooks.py --only gui_render_post client_services
|
||||
```
|
||||
|
||||
Only the selected fragments get numbers, still in merge order regardless of
|
||||
argument order. All others and their markers remain pending; a later stamp
|
||||
continues from the current API number. Missing names (including already
|
||||
stamped ones) fail before writing anything. `--check --only ...` checks the
|
||||
selection without changing files. Selection does not check dependencies:
|
||||
include any pending contracts the selected APIs require.
|
||||
|
||||
For local testing only, run the tool on a hook branch without committing
|
||||
the result. If another stamp PR merges first, regenerate from current main.
|
||||
@@ -16,8 +16,14 @@ CATALOG = Path("native-hooks.json")
|
||||
MOD_API = Path("crates/horizon-client/src/mod_api.rs")
|
||||
|
||||
|
||||
def stamp(check):
|
||||
def stamp(check, only=None):
|
||||
paths = sorted((ROOT / FRAGMENTS).glob("*.json"))
|
||||
if only is not None:
|
||||
selected = set(only)
|
||||
missing = selected - {path.stem for path in paths}
|
||||
if missing:
|
||||
raise ValueError(f"unknown pending fragment(s): {', '.join(sorted(missing))}")
|
||||
paths = [path for path in paths if path.stem in selected]
|
||||
if not paths:
|
||||
return False
|
||||
if check:
|
||||
@@ -45,7 +51,7 @@ def stamp(check):
|
||||
names = {hook["name"] for hook in catalog["hooks"]}
|
||||
helper_names = {helper["name"] for helper in catalog.get("helpers", [])}
|
||||
extra = {}
|
||||
# Match the build-time generator's fragment order so stamping preserves the generated API.
|
||||
# Append the selected entries in filename order, as the build-time generator reads fragments.
|
||||
for path in sorted(ordered):
|
||||
fragment = json.loads(path.read_text())
|
||||
for hook in fragment["hooks"]:
|
||||
@@ -109,9 +115,11 @@ def stamp(check):
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--check", action="store_true", help="fail if fragments are pending")
|
||||
parser.add_argument("--only", nargs="+", metavar="FRAGMENT",
|
||||
help="stamp only these pending fragment names (without .json), in merge order")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
return int(stamp(args.check))
|
||||
return int(stamp(args.check, args.only))
|
||||
except (ValueError, KeyError, OSError, subprocess.CalledProcessError) as error:
|
||||
print(f"stamp_native_hooks: {error}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
Reference in new issue
Block a user