Files
propresenter-auto-notes/tests/run_tests.py
T
christianandClaude Opus 5.5 d7eee1bc05 Campus kit for other ProPresenter Macs (v1.3.0)
- settings.py: per-Mac settings.json (campus code, youtube); ProPresenter
  folder, network port and template library read from ProPresenter itself
- First run / --setup / y asks the campus; YouTube text only at NA
- Preacher picked for this campus from the notes email; a lone surname
  ("Pastor Geyser") matches the most recent speaker with it
- Never restart ProPresenter when its network API is off or not answering
- tools/make_kit.sh builds "Message Notes Kit <version>.zip" (installer,
  program, launcher, READ ME FIRST); deploy.sh writes NA's settings
- Email setup text no longer names media@

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

151 lines
7.5 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)
# 7. other campuses: their template names, and who preaches there according to the email
import app
import settings
for n, want in (("_TEMPLATE - DO NOT DELETE 2608 Lower Thirds.pro", True),
("_TEMPLATE_GENERIC - DO NOT DELETE 2601.pro", True),
("_RESI MESSAGE TEMPLATE.pro", False), ("._TEMPLATE - DO NOT DELETE 2608.pro", False),
("261011 Where Is Your Faith Part 7.pro", False)):
check("template name %s" % n, settings.is_template(n) == want)
known = [("LOCATION PASTOR", "Mehdi Chniouli"), ("PASTOR", "Test Geyser"), ("LEAD PASTOR", "Tim Hatch"),
("PASTOR", "Other Geyser")]
mail = ("Subject: Notes/Slides - Where Is Your Faith - Part 7\n\nHi all,\nPastor Mehdi will be preaching live "
"from NA with RESI to SC and WS. I have attached the notes/slides. Pastor Geyser will be preaching "
"live in AB. Please have blank notes ready.\n")
real_load = settings.load
try:
for campus, want in (("NA", "Mehdi Chniouli"), ("SC", "Mehdi Chniouli"), ("WS", "Mehdi Chniouli"),
("AB", "Test Geyser")):
settings.load = lambda c=campus: {"campus": c}
got = app.speaker_from_email(mail, known)
check("speaker at %s" % campus, got and got[1] == want, "got %s" % (got,))
settings.load = lambda: {"campus": "AB"}
got = app.speaker_from_email("Pastor Tim will be preaching live from NA with RESI to all other locations.", known)
check("speaker at AB from 'all other locations'", got and got[1] == "Tim Hatch", "got %s" % (got,))
check("youtube only at NA", not settings.youtube_enabled())
settings.load = lambda: {"campus": "NA"}
check("youtube at NA", settings.youtube_enabled())
finally:
settings.load = real_load
print()
print("FAILED: %s" % failures if failures else "all checks passed")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())