Files

163 lines
5.3 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""The stage message must go up once per cue, not on every resync or pause/resume.
ProPresenter re-shows (flashes) the stage message on every PUT, even with the same text.
The bridge used to re-send it every resync_seconds and on each play/pause flip, so a
stalling Resi feed made the stage message flash over and over.
Runs the real loop against a FAKE ProPresenter on localhost -- the real one is never
touched. About 40 seconds.
python3 test_stage_message.py
"""
import http.server
import json
import threading
import time
import resi_to_propresenter as R
CALLS = []
class FakePP(http.server.BaseHTTPRequestHandler):
def _ok(self):
n = int(self.headers.get("Content-Length") or 0)
body = self.rfile.read(n) if n else b""
CALLS.append((time.time(), self.command, self.path, body.decode()))
reply = b""
if self.path == "/v1/timers": # the startup check looks for the timer by name
reply = json.dumps([{"id": {"name": "NextCue"}}]).encode()
self.send_response(200)
self.end_headers()
self.wfile.write(reply)
do_GET = do_PUT = do_DELETE = _ok
def log_message(self, *a):
pass
t0 = time.time()
def fmt(ms):
ms = int(max(0, ms))
return "%02d:%02d:%02d.%03d" % (ms // 3600000, (ms % 3600000) // 60000,
(ms % 60000) // 1000, ms % 1000)
class FakeResi:
def __init__(self, *a, **k):
self.customer_id = "C"
def login(self):
pass
def venues(self):
return [{"uuid": "V1"}]
def players(self, venue_uuid):
media = 10 * 60000 + int(time.time() - t0) * 1000 # moves every second
return [{"playerHardwareId": "DEC1", "userName": "Test", "position": fmt(media),
"eventId": "EV1"}]
def event(self, event_id):
return {"name": "SIM", "eventProfileId": "P1"}
def cues(self, profile_id, event_id):
return [{"name": "Message", "ms": 30 * 60000}] # one cue, far away
def run_for(seconds, stage_message, flap):
"""Run the real loop; with flap, playback reads as paused/playing every 3s --
what a stalling Resi feed looks like to the loop."""
del CALLS[:]
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), FakePP)
threading.Thread(target=server.serve_forever, daemon=True).start()
real_read = R.Playhead.read
def flapping(self):
est, playing, cadence = real_read(self)
if flap and int(time.time() - t0) // 3 % 2:
return est, False, cadence
return est, playing, cadence
R.Playhead.read = flapping
R.Resi = FakeResi
R.keychain_password = lambda account: "x"
cfg = dict(R.DEFAULTS)
cfg.update({"resi_user": "t@example.com", "player": "DEC1",
"propresenter": "http://127.0.0.1:%d" % server.server_address[1],
"timer_name": "NextCue", "poll_seconds": 0.5, "resync_seconds": 2,
"stage_message": stage_message})
# The loop's only exit is an exception from its sleep; BaseException gets past its
# catch-all, so the end of the scenario stops it cleanly.
stop = threading.Event()
class Done(BaseException):
pass
def sleep(s):
if stop.wait(s):
raise Done()
def worker():
try:
R.run(cfg, False)
except Done:
pass
R.time = type("T", (), {"time": staticmethod(time.time), "sleep": staticmethod(sleep),
"strftime": staticmethod(time.strftime)})
th = threading.Thread(target=worker, daemon=True)
th.start()
time.sleep(seconds)
stop.set()
th.join(5)
R.time = time
R.Playhead.read = real_read
server.shutdown()
return list(CALLS)
def count(calls, method, path):
return sum(1 for c in calls if c[1] == method and c[2] == path)
def main():
failures = []
calls = run_for(12, stage_message=True, flap=False)
arms = count(calls, "PUT", "/v1/timer/NextCue/start")
msgs = count(calls, "PUT", "/v1/stage/message")
print("steady play, resync every 2s: %d timer pushes, %d stage-message sends" % (arms, msgs))
if arms < 4:
failures.append("sanity: the timer should have been resynced several times (%d)" % arms)
if msgs != 1:
failures.append("steady play sent the stage message %d times, want 1" % msgs)
calls = run_for(16, stage_message=True, flap=True)
stops = count(calls, "GET", "/v1/timer/NextCue/stop")
msgs = count(calls, "PUT", "/v1/stage/message")
print("pause/resume every 3s: %d timer stops, %d stage-message sends" % (stops, msgs))
if stops < 2:
failures.append("sanity: the flapping should have stopped the timer repeatedly (%d)" % stops)
if msgs != 1:
failures.append("pause/resume sent the stage message %d times, want 1" % msgs)
calls = run_for(8, stage_message=False, flap=True)
touched = [c for c in calls if c[2] == "/v1/stage/message"]
print("stage_message off: %d stage-message calls" % len(touched))
if touched:
failures.append("stage_message off still touched the stage message: %s" % touched)
if failures:
raise SystemExit("\nFAILED:\n - " + "\n - ".join(failures))
print("\nstage message tests passed")
if __name__ == "__main__":
main()