Files
christianandClaude Opus 5.5 f2306f781e Carry underlines to the slides; keep Bible names in the typo check (v1.3.1)
- Underlined words in the notes are underlined on every slide type, as the
  operator does by hand (Part 8: "saved", "saves"); segments are now
  (text, bold, italic, underline); TextEdit edit format uses __word__
- Typo check never changes capitalised words inside scripture, or names
  the notes' scripture uses ("Gilead was the father..." became "Gilled")
- Tests: Part 8 + Together Tuesday notes, hand-fixed Part 8 deck

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:11:30 -04:00

183 lines
9.8 KiB
Python
Raw Permalink 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. underlined words in the notes are underlined on the slides (as the operator does by hand)
def underlined_runs(data):
p = deck.C.decode(data, "rv.data.Presentation")
out = []
for c in deck.C.all(p, "cues"):
a = deck.slide_action(c)
for el in (deck.slide_elements(a) if a is not None else []):
t = deck.element_text(el)
d = deck.C.get(t, "rtf_data") if t is not None else None
for para in (rtf.read(d).paras if d else []):
out += [r.text.strip() for r in para.runs if "ul" in r.state and r.text.strip()]
return sorted(out)
p8 = notes.parse(open(os.path.join(NOTES, "Slides - Where is Your Faith - Part 8.rtf"), "rb").read())
got = underlined_runs(deck.build(tpl, p8, "TEST", tpl.speaker()))
hand = underlined_runs(open(os.path.join(PRO, "261018 Where Is Your Faith Part 8.pro"), "rb").read())
check("part 8 underlines match the hand-fixed deck", got == hand == ["saved", "saves"], "%s vs %s" % (got, hand))
p7l = notes.parse(open(os.path.join(NOTES, "Slides - 7 Letters - Part 2.rtf"), "rb").read())
got = underlined_runs(deck.build(tpl, p7l, "TEST", tpl.speaker()))
hand = underlined_runs(open(os.path.join(PRO, "260628 The 7 Letters Part 2.pro"), "rb").read())
check("7 Letters 2 underlines match the hand-made deck", got == hand, "%s vs %s" % (got, hand))
tt = notes.parse(open(os.path.join(NOTES, "Slides - Together Tuesday - October 2026.rtf"), "rb").read())
pt = [s for s in tt.slides if s.kind == "point" and s.lines[0].text.startswith("Prophesy")]
check("underline kept in a point", pt and [x[0] for x in pt[0].lines[0].segs_plain() if x[3]] == ["Prophesy,"],
"%s" % (pt[0].lines[0].segs_plain() if pt else "no point"))
rt = plantext.load(plantext.dump(p8, "x"))
styled = lambda plan: [[(ch,) + tuple(seg[1:]) for seg in l.segs for ch in seg[0] if not ch.isspace()]
for s in plan.slides for l in s.lines] # spaces at bold edges may move
check("underline survives the TextEdit round trip", styled(p8) == styled(rt))
check("fill-in blanks are not underline marks", plantext.parse_bold("Faith is ______.") ==
[("Faith is ______.", False, False, False)])
# 8. 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())