"""Offline checks (no ProPresenter, no fonts needed). Run: python3 tests/run_tests.py""" import os import re import sys import traceback HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(HERE, "..", "src")) import deck # noqa: E402 import notes # noqa: E402 import pbwire # noqa: E402 import plantext # noqa: E402 import rtf # noqa: E402 NOTES = os.path.join(HERE, "notes") PRO = os.path.join(HERE, "pro") SERIES = os.path.join(PRO, "_TEMPLATE - DO NOT DELETE 2608 Lower Thirds.pro") GENERIC = os.path.join(PRO, "_TEMPLATE_GENERIC - DO NOT DELETE 2609.pro") failures = [] def check(name, cond, detail=""): print("%-6s %s%s" % ("ok" if cond else "FAIL", name, (" -- " + detail) if (detail and not cond) else "")) if not cond: failures.append(name) def slides_of(data): p = deck.C.decode(data, "rv.data.Presentation") cues = {deck.uuid_str(deck.C.get(c, "uuid")): c for c in deck.C.all(p, "cues")} out = [] for g in deck.C.all(p, "cue_groups"): for cid in deck.C.all(g, "cue_identifiers"): a = deck.slide_action(cues[deck.uuid_str(cid)]) texts = [] for el in deck.slide_elements(a): t = deck.element_text(el) d = deck.C.get(t, "rtf_data") if t is not None else None if d: s = " / ".join(p_.text for p_ in rtf.read(d).paras).strip() if s: texts.append((deck.element_name(el), s)) out.append((deck.action_label(a), texts)) return out def main(): # 1. every .pro file survives decode/encode byte for byte for f in sorted(os.listdir(PRO)): if f.endswith(".pro"): b = open(os.path.join(PRO, f), "rb").read() check("roundtrip %s" % f, deck.C.encode(deck.C.decode(b, "rv.data.Presentation")) == b) # 2. every notes file parses; the slide list survives the TextEdit format for f in sorted(os.listdir(NOTES)): try: p = notes.parse(open(os.path.join(NOTES, f), "rb").read()) p2 = plantext.load(plantext.dump(p, f)) same = [(s.kind, s.ref, [(l.text, l.lead) for l in s.lines]) for s in p.slides] == \ [(s.kind, s.ref, [(l.text, l.lead) for l in s.lines]) for s in p2.slides] check("notes %s (%d slides)" % (f, len(p.slides)), same and p.title_slide is not None) except Exception: check("notes %s" % f, False, traceback.format_exc().splitlines()[-1]) # 3. Part 7 from its notes matches the hand-made Part 7 (apart from the known differences) tpl = deck.Template(open(SERIES, "rb").read()) plan = notes.parse(open(os.path.join(NOTES, "Slides - Where Is Your Faith - Part 7.rtf"), "rb").read()) data = deck.build(tpl, plan, "TEST", ("LOCATION PASTOR", "Mehdi Chniouli")) gen = slides_of(data) hand = slides_of(open(os.path.join(PRO, "261011 Where Is Your Faith Part 7.pro"), "rb").read()) check("part 7 slide count", len(gen) == len(hand), "%d vs %d" % (len(gen), len(hand))) check("part 7 labels", [g[0] for g in gen] == [h[0] for h in hand]) norm = lambda s: re.sub(r"\s+", " ", s.replace(" ", " ")).strip() diffs = [] for k, (g, h) in enumerate(zip(gen, hand), 1): if [(n, norm(t)) for n, t in g[1]] != [(n, norm(t)) for n, t in h[1]]: diffs.append(k) # 17: the operator added a manual line break ("can't follow / me THROUGH") check("part 7 texts identical except slide 17", diffs == [17], "differs on %s" % diffs) # 4. both templates build without text measurement, and the result decodes for path in (SERIES, GENERIC): t = deck.Template(open(path, "rb").read()) for f in ("Slides - 7 Letters - Part 2.rtf", "Slides - May 2026 Forge Night.rtf"): p = notes.parse(open(os.path.join(NOTES, f), "rb").read()) d = deck.build(t, p, "TEST", t.speaker()) out = slides_of(d) n_build = sum(1 for s in p.slides if s.kind == "build") want = len(t.head) + sum(1 for s in p.slides if s.kind != "title") + n_build + len(t.tail) check("build %s + %s" % (os.path.basename(path)[:22], f), len(out) == want, "%d vs %d" % (len(out), want)) # 5. fresh ids everywhere, references to shared things untouched tu = set(re.findall(rb"[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}", open(SERIES, "rb").read())) gu = set(re.findall(rb"[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}", data)) shared = tu & gu check("only shared references reused (%d)" % len(shared), all( x.decode() in open(SERIES, "rb").read().decode("latin-1") for x in shared) and len(shared) <= 12) # 6. typo check (macOS's spelling checker, so this needs a Mac): planted typos are fixed, # real notes are left alone import spelling try: p = notes.parse(open(os.path.join(HERE, "typos", "Slides - Typo Test - Part 7.rtf"), "rb").read()) rep = spelling.check_and_fix(p, skip=set()) got = sorted((f.word, f.new) for f in rep.fixes) want = sorted([("teh", "the"), ("the the", "the"), ("OBEDIANCE", "OBEDIENCE"), ("beleived", "believed")]) check("typos: planted typos fixed", got == want, "got %s" % got) for f in sorted(os.listdir(NOTES)): p = notes.parse(open(os.path.join(NOTES, f), "rb").read()) rep = spelling.check_and_fix(p) check("typos: nothing changed in %s" % f, not rep.fixes, ", ".join("%s->%s" % (x.word, x.new) for x in rep.fixes)) except spelling.Unavailable as e: print("skip typo checks (%s)" % e) print() print("FAILED: %s" % failures if failures else "all checks passed") return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())