"""prop_guard.py (2026-10-25, Southcoast): a song's timeline started again when the timecode drops, and the stage display put on the worship layout when the first slide's macro did not, against a fake ProPresenter API.""" import os import sys import unittest sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import prop_guard as pg # noqa: E402 class FakeProP: def __init__(self): self.t, self.running, self.slide, self.layout, self.calls, self.song = 0.0, False, 0, "MC", [], "Jehovah" def __call__(self, path, timeout=2.0): self.calls.append(path) if path == "/v1/presentation/active": return {"presentation": {"id": {"uuid": "U-" + self.song, "name": self.song}, "has_timeline": True, "groups": [{"slides": [{}] * 10}]}} if self.song else {"presentation": None} if path == "/v1/presentation/active/timeline": return {"is_running": self.running, "current_time": self.t} if path == "/v1/presentation/active/timeline/play": self.running = True return {} if path == "/v1/presentation/slide_index": return {"presentation_index": {"index": self.slide}} if path == "/v1/stage/layout_map": return [{"screen": {"uuid": "S0", "name": "Stage Display"}, "layout": {"name": self.layout}}] if path == "/v1/stage/layouts": return [{"id": {"uuid": "L1", "name": "Worship"}}, {"id": {"uuid": "L2", "name": "MC"}}] if path.startswith("/v1/stage/screen/S0/layout/"): self.layout = "Worship" if path.endswith("L1") else self.layout return {} return None class TestGuard(unittest.TestCase): def setUp(self): self.fake, self.clock = FakeProP(), [1000.0] self.orig = pg.api pg.api = self.fake self.g = pg.Guard(now=lambda: self.clock[0]) def tearDown(self): pg.api = self.orig def at(self, t): self.clock[0] = 1000.0 + t self.g.tick() def test_stage_put_on_worship_when_the_macro_missed(self): self.at(0) self.assertEqual(self.fake.layout, "MC") # judged only after a few seconds self.at(3) self.assertEqual(self.fake.layout, "Worship") self.fake.layout = "Message" # someone picks another layout later: left alone self.at(20) self.assertEqual(self.fake.layout, "Message") def test_a_timecode_drop_starts_the_timeline_again(self): self.fake.layout = "Worship" self.fake.running = True self.at(0) self.at(10) self.fake.running, self.fake.slide = False, 4 # LTC gone mid-song self.at(10.5) self.assertNotIn("/v1/presentation/active/timeline/play", self.fake.calls) # a blink is not a drop self.at(11.6) self.assertTrue(self.fake.running) self.assertEqual(len(self.g.state["timecode_drops"]), 1) self.assertTrue(self.g.state["timecode_drops"][0]["resumed"]) def test_stopped_on_purpose_or_at_the_end_is_left_alone(self): self.fake.layout, self.fake.running = "Worship", True self.at(0) self.fake.running, self.fake.slide = False, 4 self.at(1) self.at(2.1) # restarted self.fake.running = False # and stopped again at once: an operator self.at(3) self.at(10) self.assertEqual(self.fake.calls.count("/v1/presentation/active/timeline/play"), 1) self.fake.song, self.fake.running, self.fake.slide = "Owe You Praise", True, 0 self.at(20) self.fake.running, self.fake.slide = False, 9 # its last slide: the song is over self.at(21) self.at(25) self.assertEqual(self.fake.calls.count("/v1/presentation/active/timeline/play"), 1) class TestTimecode(unittest.TestCase): def test_timecode_lost_in_a_song_and_back(self): fake, clock = FakeProP(), [1000.0] fake.layout = "Worship" orig_api, orig_ltc = pg.api, pg.Guard.ltc ltc = {"running": True, "tc": "00:01:00:00"} pg.api = fake pg.Guard.ltc = lambda self: dict(ltc, time=clock[0]) try: g = pg.Guard(now=lambda: clock[0]) def at(t): clock[0] = 1000.0 + t g.tick() at(0) at(5) ltc["running"] = False # gone mid-song at(6) self.assertFalse(g.lost) # not yet 2 s at(8.5) self.assertTrue(g.lost) self.assertEqual(g.state["ltc_drops"][0]["song"], "Jehovah") ltc["running"] = True at(10) self.assertFalse(g.lost) self.assertIn("back", g.state["ltc_drops"][0]) fake.song = None # between songs no timecode is fine ltc["running"] = False at(20) at(30) self.assertFalse(g.lost) finally: pg.api, pg.Guard.ltc = orig_api, orig_ltc if __name__ == "__main__": unittest.main()