#!/usr/bin/env python3 """Deep audit of golden JSON analysis exports.""" import json import re from collections import defaultdict from pathlib import Path GOLDEN = Path(__file__).resolve().parent.parent / "src/test/resources/golden" PH = re.compile(r"\$\{") def fn_form(identifier): if not identifier: return identifier last = identifier.rfind(".") if last <= 0: return identifier prev = identifier.rfind(".", 0, last) return identifier[prev + 1 :] if prev > 0 else identifier def sid(st): if isinstance(st, dict): return st.get("fullIdentifier") or st.get("rawName") return st def eid(ev): if isinstance(ev, dict): return ev.get("fullIdentifier") or ev.get("rawName") return ev def audit_file(path, data): name = data.get("name", path.stem) issues = [] st_fqn = data.get("stateTypeFqn") ev_fqn = data.get("eventTypeFqn") transitions = data.get("transitions") or [] starts = list(data.get("startStates") or []) ends = list(data.get("endStates") or []) states = data.get("states") or [] meta = data.get("metadata") or {} is_pkg_enum = st_fqn and "." in st_fqn and st_fqn not in ("String", "java.lang.String") is_string = st_fqn in (None, "String", "java.lang.String") or ( not st_fqn and not ev_fqn ) if PH.search(json.dumps(data)): issues.append("BUG: unresolved ${placeholder} in export") # Duplicate fullIdentifiers in states[] by_full = defaultdict(list) for s in states: by_full[sid(s)].append(s.get("rawName")) for fid, raws in by_full.items(): if fid and len(raws) > 1 and len(set(raws)) > 1: issues.append(f"NOISE: duplicate state entry {fid!r} with rawNames {raws}") # Collect transition state/event ids t_events, t_sources, t_targets = set(), set(), set() for t in transitions: if eid(t.get("event")): t_events.add(eid(t.get("event"))) for s in t.get("sourceStates") or []: t_sources.add(sid(s)) for s in t.get("targetStates") or []: t_targets.add(sid(s)) graph = t_sources | t_targets # startStates canonical consistency for ss in starts: if is_string and ss and not ss.startswith("String."): if any(x.startswith("String.") for x in graph): issues.append(f"BUG: startStates {ss!r} bare but transitions use String.* form") if ss not in graph and fn_form(ss) not in {fn_form(x) for x in graph}: issues.append(f"WARN: startStates {ss!r} not in transition graph") # endStates for es in ends: if es not in graph and fn_form(es) not in {fn_form(x) for x in graph}: issues.append(f"WARN: endStates {es!r} not in transition graph") # Enum canonical fullIdentifiers if is_pkg_enum: for i, t in enumerate(transitions): for side in ("sourceStates", "targetStates"): for s in t.get(side) or []: fid = sid(s) if fid and not fid.startswith(st_fqn + ".") and not fid.startswith('"'): if "." in fid and not fid.startswith("<"): issues.append(f"BUG: transitions[{i}].{side} {fid!r} not under {st_fqn}") ev = eid(t.get("event")) if ev and ev_fqn and "." in ev_fqn and not ev.startswith("<") and not ev.startswith(ev_fqn + "."): if ev not in ("event",) and "." in ev and not ev.startswith(ev_fqn): issues.append(f"BUG: transitions[{i}].event {ev!r} not under {ev_fqn}") for i, tr in enumerate(meta.get("triggers") or []): for field in ("event", "sourceState"): val = tr.get(field) if not val or val in ("event", "payload", "customMessage", "eventProvider"): continue type_fqn = ev_fqn if field == "event" else st_fqn if type_fqn and "." in val and not val.startswith("<") and not val.startswith(type_fqn + "."): if field == "sourceState" and val not in graph and fn_form(val) not in {fn_form(x) for x in graph}: issues.append(f"BUG: triggers[{i}].sourceState {val!r} not a machine state") elif field == "event" and type_fqn and val.isupper() is False and "." not in val and not val.startswith("<"): if val not in ("event", "payload", "customMessage", "eventProvider", "true", "false"): issues.append(f"INFO: triggers[{i}].event dynamic {val!r}") # matchedTransitions for i, chain in enumerate(meta.get("callChains") or []): for j, mt in enumerate(chain.get("matchedTransitions") or []): me, ms, mtgt = mt.get("event"), mt.get("sourceState"), mt.get("targetState") ok = False for t in transitions: if fn_form(eid(t.get("event"))) != fn_form(me): continue srcs = [sid(s) for s in t.get("sourceStates") or []] tgts = [sid(s) for s in t.get("targetStates") or []] if (not ms or any(fn_form(x) == fn_form(ms) for x in srcs)) and ( not mtgt or any(fn_form(x) == fn_form(mtgt) for x in tgts) ): ok = True break if not ok: issues.append( f"BUG: callChains[{i}].matchedTransitions[{j}] ({ms!r},{me!r},{mtgt!r}) no transition" ) # String machine: mixed NEW vs String.NEW if is_string or st_fqn == "String": all_ids = set(graph) | set(starts) | set(ends) | {sid(s) for s in states} for x in list(all_ids): if not x: continue bare = x.strip('"') if bare and "." not in bare and f"String.{bare}" in all_ids: issues.append(f"BUG: mixed bare {x!r} and String.{bare}") # stateTypeFqn missing on enum machine if is_pkg_enum and not st_fqn: issues.append("BUG: enum machine missing stateTypeFqn") return name, issues def main(): total = 0 for path in sorted(GOLDEN.rglob("*.json")): data = json.loads(path.read_text()) machine, issues = audit_file(path, data) rel = path.relative_to(GOLDEN) bugs = [i for i in issues if i.startswith("BUG")] warns = [i for i in issues if i.startswith("WARN")] noise = [i for i in issues if i.startswith("NOISE")] info = [i for i in issues if i.startswith("INFO")] if bugs or warns: print(f"\n{'='*70}\n{rel}\n machine: {machine}") for i in bugs + warns + noise + info: print(f" [{i.split(':')[0]}] {i.split(': ', 1)[-1]}") total += len(bugs) + len(warns) print(f"\nTotal BUG+WARN: {total}") if __name__ == "__main__": main()