Files
propresenter-auto-notes/tests/run_tests.py
T
christianandClaude Opus 5.5 153ca3263d Add typo check with undo
The summary now has a Spelling line: "no typos found" or each fix listed by
slide ("fixed "OBEDIANCE" -> "OBEDIENCE""), with r to undo all or some.

- Uses macOS's own spelling checker and autocorrect via JXA, so it runs
  offline with nothing installed; ALL-CAPS emphasis words are checked too,
  and doubled small words ("the the") are removed
- Never changes names it doesn't know, Greek/Hebrew sermon words, or Bible
  compounds it would split (lampstand, firstfruits)
- An undone word goes into allowed-words.txt and is never changed again;
  undo re-runs the check from the text as it was before the fixes
- Tests: planted typos are fixed, all 18 real notes files are unchanged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:37:18 -04:00

122 lines
5.7 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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())