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:
rison1337andCodex GPT-6 committed 2026-11-05 15:11:17 +05:00
1 parent 696d021699
commit 82011f3c6f
3 files changed
+109 -6

No files matched your search

+79
View File
@@ -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")
+19 -3
View File
@@ -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.
+11 -3
View File
@@ -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