from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
TOOL_PREFIX = ""
def run(binary: str, args: list[str]) -> str:
result = subprocess.run(
[binary, *args], check=True, capture_output=True, text=True
)
return result.stdout
def help_model(binary: str, command_path: list[str]) -> dict:
event = json.loads(run(binary, [*command_path, "--help", "--output", "json"]))
assert event["kind"] == "result"
assert event["result"]["code"] == "help"
model = event["result"]["help"]
assert model["schema"] == "cli-help-v2"
return model
def plain_usages(text: str) -> list[str]:
return [line for line in text.splitlines() if line.startswith(TOOL_PREFIX)]
def subcommand_path(command: str) -> list[str]:
prefix = "afdata "
suffix = " --help"
if not command.startswith(prefix) or not command.endswith(suffix):
raise ValueError(f"non-canonical subcommand help template: {command!r}")
return command[len(prefix) : -len(suffix)].split()
def main(argv: list[str]) -> int:
binary = argv[1] if len(argv) > 1 else str(ROOT / "target" / "debug" / "afdata")
global TOOL_PREFIX
TOOL_PREFIX = Path(binary).stem + " "
if not Path(binary).exists():
print(f"binary not found: {binary}", file=sys.stderr)
return 1
pending: list[list[str]] = [[]]
visited: set[tuple[str, ...]] = set()
shapes_count = 0
while pending:
command_path = pending.pop()
key = tuple(command_path)
if key in visited:
continue
visited.add(key)
model = help_model(binary, command_path)
plain = run(binary, [*command_path, "--help", "--output", "plain"])
shapes = model.get("shapes", [])
shapes_count += len(shapes)
expected = [shape["usage"] for shape in shapes]
actual = plain_usages(plain)
if expected != actual:
raise ValueError(
f"help syntax differs for {model['command_path']}: "
f"json={expected!r}, plain={actual!r}"
)
missing_notes = [
f"{name}: {note}"
for name, note in model.get("notes", {}).items()
if name not in plain or note not in plain
]
if missing_notes:
raise ValueError(
f"plain help for {model['command_path']} omits argument notes "
f"the JSON carries: {missing_notes!r}"
)
missing_defaults = [
name for name in model.get("defaults", {}) if name not in plain
]
if missing_defaults:
raise ValueError(
f"plain help for {model['command_path']} omits defaults "
f"the JSON carries: {missing_defaults!r}"
)
if len(shapes) > 1:
undescribed = [shape["id"] for shape in shapes if not shape.get("about")]
if undescribed:
raise ValueError(
f"{model['command_path']} has sibling shapes without a description: "
f"{undescribed!r}"
)
elif shapes and shapes[0].get("about"):
raise ValueError(
f"{model['command_path']} has one shape that repeats a description"
)
pending.extend(
subcommand_path(command)
for command in model.get("subcommands", [])
)
print(
f"help formats agree: {len(visited)} commands, "
f"{shapes_count} registered shapes"
)
return 0
if __name__ == "__main__":
try:
sys.exit(main(sys.argv))
except (AssertionError, KeyError, ValueError) as error:
print(f"Help formats disagree: {error}", file=sys.stderr)
sys.exit(1)