Files
video-sync/desktop-client/test_ready_server.py
T

134 lines
6.0 KiB
Python

"""Exercise ready checks through real WebSockets against an isolated local server."""
import json
import os
from pathlib import Path
import shutil
import socket
import subprocess
import time
import pytest
from websockets.sync.client import connect
@pytest.fixture
def server_url():
with socket.socket() as reservation:
reservation.bind(('127.0.0.1', 0))
port = reservation.getsockname()[1]
bun = shutil.which('bun')
assert bun, 'Bun is required to test the local server'
process = subprocess.Popen(
[bun, 'run', 'server.ts'], cwd=Path(__file__).resolve().parent.parent,
env={**os.environ, 'PORT': str(port)}, stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE, creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
try:
deadline = time.monotonic() + 5
while True:
if process.poll() is not None:
pytest.fail(process.stderr.read().decode())
try:
with socket.create_connection(('127.0.0.1', port), timeout=.1):
break
except OSError:
if time.monotonic() >= deadline:
pytest.fail('Local server did not start')
time.sleep(.05)
yield f'ws://127.0.0.1:{port}/ws'
finally:
process.terminate()
process.wait(timeout=5)
process.stderr.close()
def send(ws, **message):
ws.send(json.dumps(message))
def receive(ws, kind, predicate=lambda message: True):
deadline = time.monotonic() + 3
while True:
message = json.loads(ws.recv(timeout=max(.01, deadline - time.monotonic())))
if message['type'] == kind and predicate(message):
return message
def test_room_ready_check_lifecycle(server_url):
file_info = {'name': 'test.mkv', 'size': 1234, 'duration': 60}
with connect(server_url) as host, connect(server_url) as guest:
send(host, type='create_room', username='Host', fileInfo=file_info)
code = receive(host, 'room_created')['code']
send(guest, type='join_room', username='Guest', code=code)
receive(guest, 'room_file_check')
send(guest, type='confirm_join', fileInfo=file_info)
receive(guest, 'room_joined')
receive(host, 'user_joined')
send(host, type='sync', action='play', position=12)
receive(guest, 'sync')
send(host, type='ready_check', action='begin')
pause = receive(host, 'sync')
assert pause['action'] == 'pause' and pause['position'] >= 12
check = receive(host, 'ready_check')
check_id = check['id']
assert check['requestedBy'] == 'Host'
assert not any(p['ready'] for p in check['participants'])
assert receive(guest, 'ready_check')['id'] == check_id
# Unconfirmed peers cannot create or vote in a check.
with connect(server_url) as pending:
send(pending, type='join_room', username='Pending', code=code)
receive(pending, 'room_file_check')
send(pending, type='ready_check', action='begin')
send(pending, type='ready_check', action='vote', id=check_id, ready=True)
send(host, type='ready_check', action='start', id=check_id)
send(host, type='ready_check', action='vote', id=check_id, ready=True)
check = receive(host, 'ready_check')
assert check['active'] and sum(p['ready'] for p in check['participants']) == 1
receive(guest, 'ready_check')
send(host, type='ready_check', action='begin')
repeated = receive(host, 'ready_check')
assert repeated['id'] == check_id
assert sum(p['ready'] for p in repeated['participants']) == 1
receive(guest, 'ready_check')
send(guest, type='ready_check', action='vote', id=check_id, ready=True)
assert all(p['ready'] for p in receive(host, 'ready_check')['participants'])
receive(guest, 'ready_check')
# Non-initiators cannot start. Stale IDs cannot change votes.
send(guest, type='ready_check', action='start', id=check_id)
send(guest, type='ready_check', action='vote', id='stale', ready=False)
send(guest, type='ready_check', action='begin')
assert all(p['ready'] for p in receive(host, 'ready_check')['participants'])
receive(guest, 'ready_check')
# Dropping a connection revokes readiness and blocks starting.
guest.close()
disconnected = receive(host, 'ready_check')
guest_state = next(p for p in disconnected['participants'] if p['username'] == 'Guest')
assert not guest_state['connected'] and not guest_state['ready']
send(host, type='ready_check', action='start', id=check_id)
with connect(server_url) as rejoined:
send(rejoined, type='rejoin_room', username='Guest', code=code)
restored = receive(rejoined, 'room_rejoined')['readyCheck']
assert restored['id'] == check_id
assert not next(p for p in restored['participants'] if p['username'] == 'Guest')['ready']
receive(host, 'ready_check')
receive(rejoined, 'ready_check')
send(rejoined, type='ready_check', action='vote', id=check_id, ready=True)
receive(host, 'ready_check')
receive(rejoined, 'ready_check')
send(host, type='ready_check', action='start', id=check_id)
assert receive(host, 'sync')['action'] == 'play'
assert receive(rejoined, 'sync')['action'] == 'play'
assert not receive(host, 'ready_check')['active']
assert not receive(rejoined, 'ready_check')['active']
send(host, type='ready_check', action='begin')
new_check = receive(host, 'ready_check')
assert new_check['id'] != check_id
assert not any(p['ready'] for p in new_check['participants'])
receive(rejoined, 'ready_check')
send(host, type='ready_check', action='cancel', id=new_check['id'])
assert not receive(host, 'ready_check')['active']
assert not receive(rejoined, 'ready_check')['active']