Full rearchitecture using Claude

- event bus systen
- all components are independent
- preparation for web frontend
This commit is contained in:
2026-08-26 13:22:28 +02:00
parent 55bcc9448c
commit d44c24ec97
70 changed files with 7591 additions and 1011 deletions

View File

@@ -0,0 +1,104 @@
"""Run every file in ``scenarios/`` as a test.
Same driver, same verbs, same code path as the interactive simulator - only the clock
differs, so a scenario that takes half a minute by hand runs in microseconds here.
"""
from __future__ import annotations
from collections.abc import AsyncIterator
from pathlib import Path
import pytest
from musicmouse.config import load_config
from musicmouse.simulator.driver import ExpectationError, ScriptError
from musicmouse.simulator.harness import Simulation, build_simulation
from musicmouse.simulator.script import run_script_file
from tests.conftest import VALID_CONFIG, write_config
SCENARIO_DIR = Path(__file__).resolve().parents[1] / "scenarios"
SCENARIOS = sorted(SCENARIO_DIR.glob("*.txt"))
@pytest.fixture
async def sim(config_dir: Path) -> AsyncIterator[Simulation]:
config = load_config(write_config(config_dir, VALID_CONFIG))
simulation = await build_simulation(config)
try:
yield simulation
finally:
await simulation.aclose()
def test_scenario_files_exist() -> None:
assert SCENARIOS, f"no scenario files found in {SCENARIO_DIR}"
@pytest.mark.parametrize("scenario", SCENARIOS, ids=lambda p: p.stem)
async def test_scenario(sim: Simulation, scenario: Path) -> None:
await run_script_file(sim, scenario)
# --------------------------------------------------------------- the script parser
async def test_comments_and_blank_lines_are_ignored(sim: Simulation) -> None:
await sim.driver.run_script("# just a comment\n\n \nplace fuchs # trailing\n")
sim.driver.check("figure", "fuchs")
async def test_a_failing_expectation_names_the_line(sim: Simulation) -> None:
with pytest.raises(ExpectationError) as excinfo:
await sim.driver.run_script("place fuchs\nexpect track 7\n")
message = str(excinfo.value)
assert "line 2" in message
assert "expected track to be '7', but it is '0'" in message
async def test_an_unknown_verb_is_reported_with_its_line(sim: Simulation) -> None:
with pytest.raises(ScriptError, match="line 1"):
await sim.driver.run_script("frobnicate the widget\n")
async def test_an_unknown_figure_lists_the_configured_ones(sim: Simulation) -> None:
with pytest.raises(ScriptError, match="configured: eule, fuchs"):
await sim.driver.run_script("place giraffe\n")
async def test_an_unknown_property_lists_the_valid_ones(sim: Simulation) -> None:
with pytest.raises(ScriptError, match="unknown property"):
await sim.driver.run_script("expect loudness 5\n")
async def test_an_unknown_touch_button_lists_the_valid_ones(sim: Simulation) -> None:
with pytest.raises(ScriptError, match="left_foot, right_foot"):
await sim.driver.run_script("touch nose\n")
@pytest.mark.parametrize(
("text", "seconds"),
[("2", 2.0), ("2s", 2.0), ("500ms", 0.5), ("1m", 60.0), ("0.5s", 0.5)],
)
async def test_duration_formats(text: str, seconds: float) -> None:
from musicmouse.simulator.driver import Duration
assert Duration.parse(text).seconds == pytest.approx(seconds)
async def test_a_bad_duration_is_reported() -> None:
from musicmouse.simulator.driver import Duration
with pytest.raises(ScriptError, match="is not a duration"):
Duration.parse("soon")
async def test_status_and_leds_produce_output(sim: Simulation) -> None:
await sim.driver.place("fuchs")
status = await sim.driver.execute("status")
leds = await sim.driver.execute("leds")
assert status is not None and "fuchs" in status and "playing" in status
assert leds is not None and "ring" in leds and "shelf" in leds