Add runtime validation gate and known issues baseline
This commit is contained in:
@@ -8,6 +8,8 @@ import sys
|
||||
from dataclasses import dataclass, asdict
|
||||
from pathlib import Path
|
||||
|
||||
from known_issues import classify_issue_ids, load_known_issue_ids
|
||||
|
||||
|
||||
BASE_MODEL_RE = re.compile(r'^BaseModelFileName\s+"([^"]+)"', re.IGNORECASE)
|
||||
EFFECT_SCRIPT_RE = re.compile(r'^EffectScriptName\s+"([^"]+)"', re.IGNORECASE)
|
||||
@@ -57,6 +59,17 @@ def parse_args() -> argparse.Namespace:
|
||||
action="store_true",
|
||||
help="Emit JSON output.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--known-issues",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Optional known issues baseline JSON. Defaults to repo known_issues/runtime_known_issues.json if present.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict-known-issues",
|
||||
action="store_true",
|
||||
help="Also fail when the known-issues baseline contains stale entries not observed anymore.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -122,7 +135,7 @@ def parse_msm_references(path: Path) -> tuple[str | None, list[str], list[str],
|
||||
return base_model, effect_scripts, hit_effects, hit_sounds
|
||||
|
||||
|
||||
def validate_actor_dir(pack: str, actor_dir: Path, asset_index: set[str]) -> ActorDirCheck:
|
||||
def validate_actor_dir(pack: str, pack_dir: Path, actor_dir: Path, asset_index: set[str]) -> ActorDirCheck:
|
||||
motlist_path = actor_dir / "motlist.txt"
|
||||
motions = parse_motlist(motlist_path) if motlist_path.is_file() else []
|
||||
|
||||
@@ -161,7 +174,7 @@ def validate_actor_dir(pack: str, actor_dir: Path, asset_index: set[str]) -> Act
|
||||
|
||||
return ActorDirCheck(
|
||||
pack=pack,
|
||||
actor_dir=actor_dir.as_posix(),
|
||||
actor_dir=actor_dir.relative_to(pack_dir).as_posix(),
|
||||
motlist_present=motlist_path.is_file(),
|
||||
motlist_entries=len(motions),
|
||||
missing_msa=missing_msa,
|
||||
@@ -196,12 +209,16 @@ def main() -> int:
|
||||
packs = args.pack or ["Monster", "NPC", "PC"]
|
||||
checks: list[ActorDirCheck] = []
|
||||
failures: list[str] = []
|
||||
issue_map: dict[str, str] = {}
|
||||
asset_index = build_asset_index(runtime_assets)
|
||||
|
||||
for pack in packs:
|
||||
pack_dir = runtime_assets / pack
|
||||
if not pack_dir.is_dir():
|
||||
failures.append(f"missing pack dir: {pack}")
|
||||
issue_id = f"actor:pack_dir:{pack}"
|
||||
message = f"missing pack dir: {pack}"
|
||||
failures.append(message)
|
||||
issue_map[issue_id] = message
|
||||
continue
|
||||
|
||||
actor_dirs = collect_actor_dirs(pack_dir)
|
||||
@@ -209,26 +226,55 @@ def main() -> int:
|
||||
actor_dirs = actor_dirs[: args.limit]
|
||||
|
||||
for actor_dir in actor_dirs:
|
||||
check = validate_actor_dir(pack, actor_dir, asset_index)
|
||||
check = validate_actor_dir(pack, pack_dir, actor_dir, asset_index)
|
||||
checks.append(check)
|
||||
for motion in check.missing_msa:
|
||||
failures.append(f"{check.actor_dir}: missing motion file {motion}")
|
||||
issue_id = f"actor:motion_file:{check.actor_dir}:{motion.lower()}"
|
||||
message = f"{check.actor_dir}: missing motion file {motion}"
|
||||
failures.append(message)
|
||||
issue_map[issue_id] = message
|
||||
for gr2_name in check.missing_gr2_for_motions:
|
||||
failures.append(f"{check.actor_dir}: missing paired model {gr2_name}")
|
||||
issue_id = f"actor:paired_model:{check.actor_dir}:{gr2_name.lower()}"
|
||||
message = f"{check.actor_dir}: missing paired model {gr2_name}"
|
||||
failures.append(message)
|
||||
issue_map[issue_id] = message
|
||||
for base_model in check.missing_base_models:
|
||||
failures.append(f"{check.actor_dir}: missing base model {base_model}")
|
||||
issue_id = f"actor:base_model:{check.actor_dir}:{normalize_virtual_path(base_model)}"
|
||||
message = f"{check.actor_dir}: missing base model {base_model}"
|
||||
failures.append(message)
|
||||
issue_map[issue_id] = message
|
||||
for effect_script in check.missing_effect_scripts:
|
||||
failures.append(f"{check.actor_dir}: missing effect script {effect_script}")
|
||||
issue_id = f"actor:effect_script:{check.actor_dir}:{normalize_virtual_path(effect_script)}"
|
||||
message = f"{check.actor_dir}: missing effect script {effect_script}"
|
||||
failures.append(message)
|
||||
issue_map[issue_id] = message
|
||||
for hit_effect in check.missing_hit_effects:
|
||||
failures.append(f"{check.actor_dir}: missing hit effect {hit_effect}")
|
||||
issue_id = f"actor:hit_effect:{check.actor_dir}:{normalize_virtual_path(hit_effect)}"
|
||||
message = f"{check.actor_dir}: missing hit effect {hit_effect}"
|
||||
failures.append(message)
|
||||
issue_map[issue_id] = message
|
||||
for hit_sound in check.missing_hit_sounds:
|
||||
failures.append(f"{check.actor_dir}: missing hit sound {hit_sound}")
|
||||
issue_id = f"actor:hit_sound:{check.actor_dir}:{normalize_virtual_path(hit_sound)}"
|
||||
message = f"{check.actor_dir}: missing hit sound {hit_sound}"
|
||||
failures.append(message)
|
||||
issue_map[issue_id] = message
|
||||
|
||||
observed_issue_ids = set(issue_map.keys())
|
||||
known_path, known_issue_ids = load_known_issue_ids(__file__, "actor", args.known_issues)
|
||||
known_observed, unexpected_issue_ids, stale_known_issue_ids = classify_issue_ids(observed_issue_ids, known_issue_ids)
|
||||
|
||||
result = {
|
||||
"ok": not failures,
|
||||
"ok": not unexpected_issue_ids and (not args.strict_known_issues or not stale_known_issue_ids),
|
||||
"checked_actor_dirs": len(checks),
|
||||
"packs": packs,
|
||||
"failures": failures,
|
||||
"issue_ids": sorted(observed_issue_ids),
|
||||
"known_issue_ids": sorted(known_observed),
|
||||
"unexpected_issue_ids": sorted(unexpected_issue_ids),
|
||||
"stale_known_issue_ids": sorted(stale_known_issue_ids),
|
||||
"unexpected_failures": [issue_map[issue_id] for issue_id in sorted(unexpected_issue_ids)],
|
||||
"stale_known_failures": sorted(stale_known_issue_ids),
|
||||
"known_issues_path": str(known_path) if known_path else None,
|
||||
"checks": [asdict(check) for check in checks],
|
||||
}
|
||||
|
||||
@@ -236,8 +282,10 @@ def main() -> int:
|
||||
print(json.dumps(result, indent=2))
|
||||
else:
|
||||
print(f"ok={result['ok']} checked_actor_dirs={result['checked_actor_dirs']}")
|
||||
for failure in failures:
|
||||
for failure in result["unexpected_failures"]:
|
||||
print(f"FAIL: {failure}")
|
||||
for issue_id in result["stale_known_issue_ids"]:
|
||||
print(f"STALE: {issue_id}")
|
||||
|
||||
return 0 if result["ok"] else 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user