from __future__ import annotations
import argparse
import pathlib
import re
import sys
def parse_manifest(manifest_path: pathlib.Path) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
if not manifest_path.exists():
raise RuntimeError(f"state-schema-manifest.toml not found at {manifest_path}")
text = manifest_path.read_text(encoding="utf-8")
stable_entries: list[dict[str, str]] = []
preview_entries: list[dict[str, str]] = []
current: list[dict[str, str]] | None = None
current_entry: dict[str, str] = {}
for raw_line in text.splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line == "[[stable_state]]":
if current_entry and current is not None:
current.append(current_entry)
current = stable_entries
current_entry = {}
continue
if line == "[[preview_state]]":
if current_entry and current is not None:
current.append(current_entry)
current = preview_entries
current_entry = {}
continue
match = re.match(r'^(\w+)\s*=\s*"([^"]*)"', line)
if match and current is not None:
current_entry[match.group(1)] = match.group(2)
continue
if current_entry and current is not None:
current.append(current_entry)
return stable_entries, preview_entries
def grep_validate_state_impl(root: pathlib.Path, type_name: str) -> bool:
return _grep_pattern_in_source(root, rf"impl\s+ValidateState\s+for\s+{re.escape(type_name)}\b")
def grep_custom_deserialize_impl(root: pathlib.Path, type_name: str) -> bool:
return _grep_pattern_in_source(
root,
rf"impl.*serde::Deserialize.*\s+for\s+{re.escape(type_name)}\b",
)
def _grep_pattern_in_source(root: pathlib.Path, pattern_str: str) -> bool:
pattern = re.compile(pattern_str)
for search_dir in (root / "src", root / "crates"):
if not search_dir.exists():
continue
for rust_file in search_dir.rglob("*.rs"):
try:
text = rust_file.read_text(encoding="utf-8")
except OSError:
continue
if pattern.search(text):
return True
return False
def validate_coverage(root: pathlib.Path) -> list[str]:
errors: list[str] = []
manifest_path = root / "state-schema-manifest.toml"
fixture_dir = root / "tests" / "fixtures" / "state"
try:
stable_entries, preview_entries = parse_manifest(manifest_path)
except RuntimeError as e:
return [str(e)]
stable_types: list[str] = [e["type"] for e in stable_entries]
seen: set[str] = set()
for t in stable_types:
if t in seen:
errors.append(f"Duplicate Stable state type: {t!r}")
seen.add(t)
preview_types: list[str] = [e["type"] for e in preview_entries]
seen_preview: set[str] = set()
for t in preview_types:
if t in seen_preview:
errors.append(f"Duplicate Preview state type: {t!r}")
seen_preview.add(t)
overlap = set(stable_types) & set(preview_types)
if overlap:
errors.append(f"Types appear in both Stable and Preview groups: {sorted(overlap)}")
for entry in stable_entries:
t = entry["type"]
fixture = entry.get("fixture", "")
if not fixture:
errors.append(f"Stable state type {t!r} has no 'fixture' field in manifest")
continue
v0_path = fixture_dir / "v0.13.0" / f"{fixture}.json"
v1_path = fixture_dir / "v1" / f"{fixture}.json"
if not v0_path.exists():
errors.append(f"Stable state type {t!r}: missing v0.13.0 fixture at {v0_path}")
if not v1_path.exists():
errors.append(f"Stable state type {t!r}: missing v1 fixture at {v1_path}")
for entry in stable_entries:
t = entry["type"]
validation = entry.get("validation", "validate_state")
if validation == "validate_state":
if not grep_validate_state_impl(root, t):
errors.append(
f"Stable state type {t!r}: does not implement ValidateState "
f"(no 'impl ValidateState for {t}' found in src/ or crates/)"
)
elif validation == "deserialize":
if not grep_custom_deserialize_impl(root, t):
errors.append(
f"Stable state type {t!r}: validation='deserialize' but no custom "
f"Deserialize impl found for {t} in src/ or crates/"
)
elif validation == "derive":
pass
else:
errors.append(
f"Stable state type {t!r}: unknown validation mode {validation!r} "
f"(expected 'validate_state', 'deserialize', or 'derive')"
)
for entry in preview_entries:
t = entry["type"]
pass
return errors
def main() -> int:
parser = argparse.ArgumentParser(
description="Validate state schema fixture coverage against the manifest."
)
parser.add_argument(
"--root",
type=pathlib.Path,
default=None,
help="Repository root path (default: auto-detected from script location).",
)
args = parser.parse_args()
root = args.root or pathlib.Path(__file__).resolve().parent.parent
errors = validate_coverage(root)
if errors:
print(f"check_state_fixture_coverage: {len(errors)} error(s) found:", file=sys.stderr)
for e in errors:
print(f" - {e}", file=sys.stderr)
return 1
manifest_path = root / "state-schema-manifest.toml"
stable_entries, preview_entries = parse_manifest(manifest_path)
print(f"check_state_fixture_coverage: validation passed.")
print(f" Stable state types: {len(stable_entries)}")
print(f" Preview state types: {len(preview_entries)}")
print(f" All Stable types have v0.13.0 + v1 fixtures and implement ValidateState.")
return 0
if __name__ == "__main__":
raise SystemExit(main())