Files
hockey_new/tests/test_build196_delay_settings.py

194 lines
9.2 KiB
Python

from __future__ import annotations
import json
from copy import deepcopy
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from hockey_data.config import DEFAULT_SCOREBOARD_TEAM_STATES, HockeySettingsStore
from hockey_data.models import Game, Tournament
from hockey_data.service import HockeyDataService
from tests.support import LocalTestDatabase
from ui_builder.manager import DEFAULT_CONFIG, UIBuilderManager
from ui_builder.router import create_ui_builder_router
DELAY_CASES = [
pytest.param(0, 0.0, id="zero"),
pytest.param(0.1, 0.1, id="fraction"),
pytest.param("2.36", 2.4, id="numeric-string-rounded"),
pytest.param(60, 60.0, id="maximum"),
pytest.param(-1, 0.0, id="negative-clamped"),
pytest.param(61, 60.0, id="large-clamped"),
pytest.param(None, 0.0, id="null"),
pytest.param(True, 0.0, id="boolean"),
pytest.param("invalid", 0.0, id="invalid-string"),
pytest.param(float("nan"), 0.0, id="nan"),
pytest.param(float("inf"), 0.0, id="infinity"),
pytest.param(float("-inf"), 0.0, id="negative-infinity"),
]
def timer_config(delay):
config = deepcopy(DEFAULT_CONFIG)
config["data_source"] = "hockey"
config["shortcut_sequences"] = [{
"id": "scoreboard", "name": "Scoreboard", "combo": "F7",
"is_scoreboard_sequence": True,
"steps": [{
"id": "timers", "type": "hockey_vmix_timers_start",
"game_vmix_input": "SCORE", "game_vmix_selected_name": "Clock.Text",
"scoreboard_delay_seconds": delay,
"home_penalty_targets": [{
"id": "home", "input": "PEN HOME", "selected_name": "Penalty.Text",
"overlay": "4", "auto_hide_on_finish": False,
"scoreboard_delay_seconds": delay,
}],
"away_penalty_targets": [{
"id": "away", "input": "PEN AWAY", "selected_name": "Penalty.Text",
"overlay": "5", "scoreboard_delay_seconds": delay,
}],
}, {
"id": "delayed-flag", "type": "vmix_command",
"function": "OverlayInput3In", "input": "EMPTY NET",
"condition": "home_empty_net", "scoreboard_delay_seconds": delay,
}],
}]
return config
def assert_delays(config, expected):
timer_step, flag_step = config["shortcut_sequences"][0]["steps"]
for step in (timer_step, flag_step):
assert step["scoreboard_delay_seconds"] == expected
assert isinstance(step["scoreboard_delay_seconds"], float)
for key in ("home_penalty_targets", "away_penalty_targets"):
assert timer_step[key][0]["scoreboard_delay_seconds"] == expected
assert timer_step["home_penalty_targets"][0]["input"] == "PEN HOME"
assert timer_step["home_penalty_targets"][0]["selected_name"] == "Penalty.Text"
assert timer_step["home_penalty_targets"][0]["overlay"] == "4"
assert timer_step["home_penalty_targets"][0]["auto_hide_on_finish"] is False
assert timer_step["game_vmix_selected_name"] == "Clock.Text"
assert flag_step["condition"] == "home_empty_net"
assert flag_step["function"] == "OverlayInput3In"
def read_strict_json(path):
def reject_constant(value):
pytest.fail(f"Nonfinite JSON constant persisted: {value}")
return json.loads(path.read_text(encoding="utf-8"), parse_constant=reject_constant)
@pytest.mark.parametrize("delay,expected", DELAY_CASES)
def test_manager_saves_finite_delay_for_penalty_slots_and_conditional_steps(tmp_path, delay, expected):
manager = UIBuilderManager(tmp_path)
assert_delays(manager.save(timer_config(delay)), expected)
assert_delays(manager.load(), expected)
assert_delays(read_strict_json(manager.settings_file), expected)
def test_manager_defaults_old_targets_and_steps_to_zero_without_losing_legacy_fields(tmp_path):
config = timer_config(2.0)
timer_step, flag_step = config["shortcut_sequences"][0]["steps"]
for step in (timer_step, flag_step):
step.pop("scoreboard_delay_seconds")
timer_step["home_penalty_targets"] = None
timer_step["home_penalty_inputs"] = "PEN HOME"
timer_step["home_penalty_selected_names"] = "Penalty.Text"
timer_step["away_penalty_targets"][0].pop("scoreboard_delay_seconds")
saved = UIBuilderManager(tmp_path).save(config)
timer_step, flag_step = saved["shortcut_sequences"][0]["steps"]
assert timer_step["scoreboard_delay_seconds"] == 0.0
assert flag_step["scoreboard_delay_seconds"] == 0.0
assert timer_step["home_penalty_targets"][0]["scoreboard_delay_seconds"] == 0.0
assert timer_step["away_penalty_targets"][0]["scoreboard_delay_seconds"] == 0.0
assert timer_step["home_penalty_targets"][0]["input"] == "PEN HOME"
assert timer_step["home_penalty_targets"][0]["selected_name"] == "Penalty.Text"
def test_actual_config_save_load_publish_and_restart_preserve_delays(tmp_path):
draft = UIBuilderManager(tmp_path, filename="ui_builder_draft.json")
published = UIBuilderManager(tmp_path, filename="ui_builder_published.json")
app = FastAPI()
app.include_router(create_ui_builder_router(draft, {}, publish_manager=published))
with TestClient(app) as client:
response = client.post("/api/ui-builder/config", json=timer_config(2.3))
assert response.status_code == 200, response.text
assert_delays(response.json()["config"], 2.3)
assert_delays(client.get("/api/ui-builder/config").json(), 2.3)
response = client.post("/api/ui-builder/publish")
assert response.status_code == 200, response.text
assert_delays(response.json()["config"], 2.3)
assert_delays(published.load(), 2.3)
restarted = UIBuilderManager(tmp_path, filename="ui_builder_published.json")
assert restarted.load() == draft.load()
assert_delays(read_strict_json(restarted.settings_file), 2.3)
@pytest.mark.parametrize("delay,expected", DELAY_CASES)
def test_team_state_settings_normalize_before_write_and_survive_restart(tmp_path, delay, expected):
settings = HockeySettingsStore(tmp_path)
states = {key: {"scoreboard_delay_seconds": delay} for key in DEFAULT_SCOREBOARD_TEAM_STATES}
result = settings.update_public({"scoreboard_team_states": states})
persisted = read_strict_json(settings.timer_rules_file)["scoreboard_team_states"]
restarted = HockeySettingsStore(tmp_path).public()["scoreboard_team_states"]
for key in DEFAULT_SCOREBOARD_TEAM_STATES:
assert result["scoreboard_team_states"][key]["scoreboard_delay_seconds"] == expected
assert persisted[key]["scoreboard_delay_seconds"] == expected
assert restarted[key]["scoreboard_delay_seconds"] == expected
def test_old_client_update_preserves_delay_and_operator_team_state_configuration(tmp_path):
settings = HockeySettingsStore(tmp_path)
settings.update_public({"scoreboard_team_states": {"home_empty_net": {
"ru": "Мой титр", "en": "My title", "input": "stable-key",
"input_title": "CUSTOM EMPTY NET", "overlay": "7", "enabled": False,
"scoreboard_delay_seconds": 3.2,
}}})
# A client predating the delay field may update only the old properties.
settings.update_public({"scoreboard_team_states": {"home_empty_net": {"ru": "Новая подпись"}}})
actual = HockeySettingsStore(tmp_path).public()["scoreboard_team_states"]["home_empty_net"]
assert actual == {
"ru": "Новая подпись", "en": "My title", "input": "stable-key",
"input_title": "CUSTOM EMPTY NET", "overlay": "7", "enabled": False,
"scoreboard_delay_seconds": 3.2,
}
def test_legacy_public_read_defaults_all_four_team_states_to_zero(tmp_path):
settings = HockeySettingsStore(tmp_path)
raw = read_strict_json(settings.timer_rules_file)
for state in raw["scoreboard_team_states"].values():
state.pop("scoreboard_delay_seconds", None)
settings.timer_rules_file.write_text(json.dumps(raw, ensure_ascii=False), encoding="utf-8")
states = HockeySettingsStore(tmp_path).public()["scoreboard_team_states"]
assert set(states) == set(DEFAULT_SCOREBOARD_TEAM_STATES)
assert all(state["scoreboard_delay_seconds"] == 0.0 for state in states.values())
def test_actual_game_control_projects_all_four_team_state_delays(tmp_path):
settings = HockeySettingsStore(tmp_path / "settings")
delays = dict(zip(DEFAULT_SCOREBOARD_TEAM_STATES, (0.0, 0.1, 2.3, 60.0)))
settings.update_public({"scoreboard_team_states": {
key: {"scoreboard_delay_seconds": value, "input": f"input-{key}"}
for key, value in delays.items()
}})
database = LocalTestDatabase(tmp_path / "control.sqlite")
database.create_all()
try:
with database.session() as session:
session.add(Tournament(external_id="t", season_part="regular", level="khl"))
session.add(Game(external_id="g", tournament_external_id="t"))
service = HockeyDataService(base_dir=tmp_path, database=database, settings=settings)
result = service.game_control("g")
assert result is not None
assert set(result["scoreboard_team_states"]) == set(delays)
for key, expected in delays.items():
state = result["scoreboard_team_states"][key]
assert state["scoreboard_delay_seconds"] == expected
assert state["input"] == f"input-{key}"
finally:
database.engine.dispose()