#!/usr/bin/env python3 """test_factory.py — validate the ✦ CANON factory patches' SHAPE. Not a taste check (the human re-voices by ear) — this asserts every factory patch loads cleanly through the client's migrateSeq expectations: 16-step seq arrays, wlen in 1..16, sane bpm, valid route/groove/scale shape, and a ✦-prefixed name. Stdlib only; excluded from the deploy by the test_*.py filter. python3 test_factory.py """ import json import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) FACTORY = os.path.join(HERE, "factory") SCALES = {"chromatic", "major", "minor", "dorian", "phrygian", "lydian", "mixolydian", "pentatonic", "minor_pent", "wholetone", "hirajoshi"} SEQ_ARRAYS = ("steps", "vel", "chance", "rat", "lock", "len") def check_seq(key, s, errs): for a in SEQ_ARRAYS: if a in s: if not isinstance(s[a], list) or len(s[a]) != 16: errs.append(f"{key}.{a} must be a 16-length array") if "steps" not in s or not isinstance(s.get("steps"), list) or len(s["steps"]) != 16: errs.append(f"{key}.steps missing/not-16 (migrateSeq would reject → dropped)") if "wlen" in s and not (isinstance(s["wlen"], int) and 1 <= s["wlen"] <= 16): errs.append(f"{key}.wlen must be an int 1..16, got {s.get('wlen')!r}") if "on" in s and not isinstance(s["on"], (bool, int)): errs.append(f"{key}.on must be boolean-ish") def check_patch(name, p, errs): if not isinstance(p.get("bpm"), (int, float)) or not (40 <= p["bpm"] <= 180): errs.append(f"bpm must be 40..180, got {p.get('bpm')!r}") for r in p.get("routes", []): if not r.get("source") or not r.get("dest"): errs.append("route missing source/dest") if not isinstance(r.get("amount"), (int, float)): errs.append(f"route {r.get('source')}→{r.get('dest')} amount not numeric") for key, s in (p.get("seqs") or {}).items(): check_seq(key, s, errs) sc = p.get("scale") or {} if sc and sc.get("name") not in SCALES: errs.append(f"scale.name {sc.get('name')!r} not a known scale") gr = p.get("groove") or {} for k in ("swing", "humanize", "velo"): if k in gr and not (0 <= gr[k] <= 1): errs.append(f"groove.{k} out of 0..1") def main(): if not os.path.isdir(FACTORY): print("FAIL: no factory/ dir"); sys.exit(1) files = sorted(f for f in os.listdir(FACTORY) if f.endswith(".json")) if len(files) < 4: print(f"FAIL: expected ≥4 factory patches, found {len(files)}"); sys.exit(1) total_errs = 0 names = [] for fn in files: with open(os.path.join(FACTORY, fn), encoding="utf-8") as f: obj = json.load(f) name, data = obj.get("name", ""), obj.get("data") errs = [] if not name.startswith("✦ "): errs.append(f"name {name!r} must start with '✦ '") if not isinstance(data, dict): errs.append("missing 'data' object") else: check_patch(name, data, errs) names.append(name) if errs: total_errs += len(errs) print(f" ✗ {fn}: " + "; ".join(errs)) else: nseq = len(data.get("seqs") or {}) nroute = len(data.get("routes") or []) print(f" ✓ {name} (bpm {data['bpm']}, {nseq} seqs, {nroute} routes)") # the four the brief names for want in ("✦ the heartbeat", "✦ the wheel within the wheel", "✦ the squeeze", "✦ quiet arrival"): if want not in names: print(f" ✗ missing canonical patch: {want}"); total_errs += 1 if total_errs: print(f"\nFAIL: {total_errs} shape error(s)"); sys.exit(1) print(f"\nALL {len(files)} FACTORY PATCHES VALID (shape only — beauty is the human's)") if __name__ == "__main__": main()