Files
ledgrab/server/tests/core/test_lol_adapter.py
T
alexei.dolgolyov 492bdb95e3 feat: game integration system
Receive real-time events from games (CS2, Dota 2, LoL, etc.) and drive
LED effects through the existing color strip and value source pipelines.

Core:
- GameEventBus (thread-safe pub/sub) with standardized 23-type event vocabulary
- GameAdapter ABC + AdapterRegistry + MappingAdapter (YAML-driven)
- Built-in adapters: CS2 GSI, Dota 2 GSI, LoL Live Client, Generic Webhook
- Community YAML adapters: Minecraft, Valorant, Rocket League
- GameEventColorStripStream with 5 effects (flash/pulse/sweep/color_shift/breathing)
- GameEventValueSource with EMA smoothing and timeout
- 4 built-in effect presets (FPS Combat, MOBA Health, Racing, Generic Alert)
- Auto-setup for Valve GSI games (Steam path detection, cfg file writing)
- Demo capture engine exposed to non-demo mode

Frontend:
- Game tab in Streams tree navigation with integration cards
- Game integration editor modal with adapter picker, config fields, event mappings
- game_event source type in CSS and ValueSource editors
- Setup instructions overlay (markdown rendered)
- Live event monitor and connection test

API:
- Full CRUD for game integrations
- Event ingestion endpoint (adapter-level auth)
- Adapter metadata, presets, auto-setup, status/diagnostics endpoints
2026-03-31 13:17:52 +03:00

184 lines
6.5 KiB
Python

"""Tests for League of Legends Live Client Data API adapter."""
import threading
import pytest
from wled_controller.core.game_integration.adapters.lol_adapter import (
LoLAdapter,
LoLPoller,
)
def _make_lol_payload(
*,
current_health: float = 1000.0,
max_health: float = 1000.0,
resource_value: float = 500.0,
resource_max: float = 500.0,
level: int = 10,
summoner_name: str = "TestPlayer",
current_gold: float | None = None,
) -> dict:
"""Build a realistic LoL Live Client Data payload."""
payload: dict = {
"activePlayer": {
"summonerName": summoner_name,
"level": level,
"currentHealth": current_health,
"championStats": {
"currentHealth": current_health,
"maxHealth": max_health,
"resourceValue": resource_value,
"resourceMax": resource_max,
"attackDamage": 80.0,
"abilityPower": 0.0,
"armor": 60.0,
"magicResist": 40.0,
"moveSpeed": 345.0,
},
},
"allPlayers": [
{
"summonerName": summoner_name,
"championName": "Jinx",
"team": "ORDER",
"scores": {"kills": 5, "deaths": 2, "assists": 7},
},
],
"gameData": {
"gameMode": "CLASSIC",
"gameTime": 1200.5,
"mapName": "Map11",
"mapNumber": 11,
"mapTerrain": "Default",
},
}
if current_gold is not None:
payload["allPlayers"][0]["currentGold"] = current_gold
return payload
class TestLoLContinuousEvents:
def test_health_normalized(self) -> None:
payload = _make_lol_payload(current_health=500.0, max_health=1000.0)
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {})
hp = [e for e in events if e.event_type == "health"]
assert len(hp) == 1
assert hp[0].value == pytest.approx(0.5)
def test_mana_normalized(self) -> None:
payload = _make_lol_payload(resource_value=300.0, resource_max=600.0)
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {})
mp = [e for e in events if e.event_type == "mana"]
assert len(mp) == 1
assert mp[0].value == pytest.approx(0.5)
def test_level_normalized(self) -> None:
payload = _make_lol_payload(level=9)
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {})
lvl = [e for e in events if e.event_type == "speed"]
assert len(lvl) == 1
assert lvl[0].value == pytest.approx(9.0 / 18.0)
def test_level_max(self) -> None:
payload = _make_lol_payload(level=18)
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {})
lvl = [e for e in events if e.event_type == "speed"]
assert lvl[0].value == pytest.approx(1.0)
def test_gold(self) -> None:
payload = _make_lol_payload(current_gold=15000.0)
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, {})
gold = [e for e in events if e.event_type == "gold"]
assert len(gold) == 1
assert gold[0].value == pytest.approx(15000.0 / 30000.0)
class TestLoLDeathRespawn:
def test_death_detected_when_health_drops_to_zero(self) -> None:
prev = {"alive": True}
payload = _make_lol_payload(current_health=0.0, max_health=1000.0)
events, state = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev)
deaths = [e for e in events if e.event_type == "death"]
assert len(deaths) == 1
assert state["alive"] is False
def test_no_death_when_already_dead(self) -> None:
prev = {"alive": False}
payload = _make_lol_payload(current_health=0.0, max_health=1000.0)
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev)
deaths = [e for e in events if e.event_type == "death"]
assert len(deaths) == 0
def test_respawn_detected(self) -> None:
prev = {"alive": False}
payload = _make_lol_payload(current_health=800.0, max_health=1000.0)
events, state = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev)
respawns = [e for e in events if e.event_type == "objective_progress"]
assert len(respawns) == 1
assert state["alive"] is True
def test_no_respawn_when_already_alive(self) -> None:
prev = {"alive": True}
payload = _make_lol_payload(current_health=800.0, max_health=1000.0)
events, _ = LoLAdapter.parse_payload(payload, {"adapter_id": "lol"}, prev)
respawns = [e for e in events if e.event_type == "objective_progress"]
assert len(respawns) == 0
class TestLoLAuth:
def test_always_accepts(self) -> None:
assert LoLAdapter.validate_auth({}, {}, {}) is True
assert LoLAdapter.validate_auth({"X-Custom": "val"}, {}, {"auth_token": "x"}) is True
class TestLoLMetadata:
def test_adapter_type(self) -> None:
assert LoLAdapter.ADAPTER_TYPE == "lol"
def test_supported_events(self) -> None:
assert "health" in LoLAdapter.SUPPORTED_EVENTS
assert "mana" in LoLAdapter.SUPPORTED_EVENTS
assert "death" in LoLAdapter.SUPPORTED_EVENTS
def test_config_schema(self) -> None:
schema = LoLAdapter.get_config_schema()
assert "poll_interval_ms" in schema["properties"]
def test_setup_instructions(self) -> None:
instructions = LoLAdapter.get_setup_instructions()
assert "League of Legends" in instructions
class TestLoLPoller:
def test_start_stop(self) -> None:
"""Test that the poller starts and stops cleanly."""
called = threading.Event()
def callback(data: dict) -> None:
called.set()
poller = LoLPoller({"poll_interval_ms": 100}, callback)
assert not poller.is_running
poller.start()
assert poller.is_running
poller.stop()
assert not poller.is_running
def test_double_start_no_crash(self) -> None:
"""Starting twice should not create duplicate threads."""
poller = LoLPoller({"poll_interval_ms": 1000}, lambda d: None)
poller.start()
poller.start() # should warn but not crash
poller.stop()
def test_stop_without_start(self) -> None:
"""Stopping without starting should not crash."""
poller = LoLPoller({"poll_interval_ms": 1000}, lambda d: None)
poller.stop() # no-op