Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions plans/cmux-next/threading-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ How it is enforced:
and queues" does not list; a label that is a variable is not judged). A `.wait(` with `await` before it in its
statement is an async wait and is not refused. Escape: `// blocking-allow: <why>` (to be counted by the arch
ratchet's `blocking_allow_escapes` row: to come, PL-arch).
- **The count (the debt left to pay down).** `scripts/cmux-next/guardrails.py --sites main-blocking` judges the
whole tree (base debt included) and prints every main-blocking site and their count: 75 at 01facc1b131
(feat-cmux-next). Any rule id works.

## Threads and queues

Expand Down
34 changes: 34 additions & 0 deletions scripts/cmux-next/guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -2894,13 +2894,47 @@ def run_push(repo: Path, ranges: list, ratchet: Ratchet | None = None) -> bool:
return refused or refuse


def tree_sites(repo: Path, head: str, rule_id: str) -> list[str]:
"""Every site RULE_ID finds in HEAD's whole tree (base debt included), as `path:line: text`, sorted.

The rule judges what HEAD adds over the empty tree, so each count is the debt a slice of work pays down
(threading item 1, #25852: `--sites main-blocking` counts the main-thread blocking sites). A warning rule's
sites (maybe-main-blocking) count the same way."""
# The empty tree (git knows it without storing it), not a written empty commit: no loose objects.
empty = subprocess.run(["git", "-C", str(repo), "hash-object", "-t", "tree", "--stdin"], input=b"",
capture_output=True, check=True).stdout.decode().strip()
found, warnings = measure(repo, empty, git(repo, "rev-parse", head).strip())
# measure() fails open (a warning) when the diff, the generic-signature context or this rule fails; a count
# that silently left the rule out would read as debt paid down, so the count fails closed instead.
failed = [text for source, text in warnings if source == "guardrails"
and text.startswith(("could not read", "generic-signature context", f"rule {rule_id} "))]
if failed:
raise SystemExit("guardrails --sites: no count, the rule did not run on the whole tree:\n" + "\n".join(failed))
refusals, _ = settle(found, [])
sites = [f"{h.path}:{h.number}: {h.text.strip()}" for rule, hits in refusals if rule.id == rule_id for h in hits]
sites += [f"{h.path}:{h.number}: {h.text.strip()}" for rule, hits in found if rule.id == rule_id
for h in hits if getattr(h, "warn", False) and not (isinstance(h, NetHit) and h.additive and h.excess <= 0)]
return sorted(sites)


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--repo")
parser.add_argument("--base")
parser.add_argument("--head", default="HEAD")
parser.add_argument("--list", action="store_true", help="print the rules")
parser.add_argument("--sites", metavar="RULE",
help="print every site of RULE in HEAD's whole tree and their count (base debt included)")
args = parser.parse_args(argv)
if args.sites:
if args.sites not in {rule.id for rule in RULES}:
raise SystemExit(f"guardrails: no rule {args.sites} (--list prints them)")
repo = Path(args.repo or git(Path.cwd(), "rev-parse", "--show-toplevel").strip())
sites = tree_sites(repo, args.head, args.sites)
for site in sites:
print(site)
print(f"guardrails --sites {args.sites}: {len(sites)} at {git(repo, 'rev-parse', '--short=11', args.head).strip()}")
return 0
if args.list:
for rule in RULES:
print(f"{rule.id}: {rule.title}\n why: {rule.why}\n do: {rule.fix}\n escape: {rule.escape or '-'}")
Expand Down
30 changes: 30 additions & 0 deletions tests/test_cmux_next_guardrails_blocking.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
python3 tests/test_cmux_next_guardrails_blocking.py
"""
import collections
import dataclasses
import importlib.util
import os
from pathlib import Path
Expand All @@ -15,6 +16,7 @@
import threading
import time
import unittest
from unittest import mock

SOURCE = Path(__file__).resolve().parents[1]
PKG = "Packages/macOS/CmuxNext"
Expand Down Expand Up @@ -253,6 +255,34 @@ def test_base_debt_and_the_seams_never_count(self):
self.commit("edits elsewhere and adds seams")
self.assertEqual(self.refused_ids(), [])

def test_sites_counts_the_whole_tree_base_debt_included(self):
# #25852: `guardrails.py --sites main-blocking` is the debt count a slice of work pays down.
self.write(f"{UI}/Added.swift", "func save() {\n try? data.write(to: url)\n}\n")
self.write(f"{SERVICE}/Fine.swift", "func save() {\n try? data.blockingWrite(to: url)\n}\n")
self.commit("adds one more")
saved = {k: os.environ.pop(k) for k in list(os.environ) if k.startswith("GIT_")}
try:
sites = guardrails.tree_sites(self.repo, "HEAD", "main-blocking")
at_base = guardrails.tree_sites(self.repo, self.base, "main-blocking")
finally:
os.environ.update(saved)
self.assertEqual(sites, [f"{UI}/Added.swift:2: try? data.write(to: url)",
f"{UI}/Old.swift:2: try? Data(contentsOf: url)"])
self.assertEqual(at_base, [f"{UI}/Old.swift:2: try? Data(contentsOf: url)"])

def test_sites_fails_closed_when_the_rule_does_not_run(self):
# A count that silently left a crashed rule out would read as debt paid down (REVIEW.md "Fail closed").
def crash(files, moved):
raise RuntimeError("matcher bug")
rules = [dataclasses.replace(r, find=crash) if r.id == "main-blocking" else r for r in guardrails.RULES]
saved = {k: os.environ.pop(k) for k in list(os.environ) if k.startswith("GIT_")}
try:
with mock.patch.object(guardrails, "RULES", rules), self.assertRaises(SystemExit) as raised:
guardrails.tree_sites(self.repo, "HEAD", "main-blocking")
finally:
os.environ.update(saved)
self.assertIn("rule main-blocking failed", str(raised.exception))

def test_a_missing_doc_fails_thread_doc_open_and_keeps_the_others(self):
self.git("rm", "--quiet", "plans/cmux-next/threading-model.md")
self.write(f"{SERVICE}/Added.swift", 'let q = DispatchQueue(label: "x")\nfunc f() { Thread.sleep(forTimeInterval: 1) }\n')
Expand Down
Loading