mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 21:01:40 +00:00
* feat(sloppak): load multiple drum parts (feedpak 1.17.0 drums-as-arrangements)
A song can now ship SEVERAL drum charts (a second drummer, an aux-percussion
layer). The Arrangement Editor already writes them per the feedpak 1.17.0 FEP
(feedpak-spec#63): the primary stays the song-level `drum_tab:` key (what this
app has always played), and each part rides the manifest as a `type: drums`
arrangement entry carrying a per-arrangement `drum_tab` file pointer and NO
note `file` — an entry this loader's file/notation gate already skips, which
is exactly why old builds are unaffected by such packs.
lib/sloppak.py:
- The arrangements loop collects drum-part pointer entries instead of merely
skipping them — but still NEVER turns one into a fretted Arrangement. That
skip is the grading invariant (an empty drum chart must not reach the
fretted pipeline / note-detection grading) and is now pinned by test.
- New `LoadedSloppak.drum_parts`: [{id, name, drum_tab}], primary FIRST. The
entry aliasing the song-level file contributes its id/name but is never
loaded twice (the primary's payload IS `loaded.drum_tab`, same object).
Legacy single-drum packs read as a one-part list; a pointer-only pack (a
writer omitted the alias) promotes its first part so has_drum_tab, the
default stream, and the drum-only placeholder keep working.
- The song-level drum_tab loading block is extracted verbatim into
`_load_drum_tab_file()` and shared by both paths, so every part gets the
same permissive posture: missing file → that part silently absent;
traversal / parse / validation failure → that part skipped with a warning,
never an aborted load. (The 9 pinned drumtab-load tests pass unchanged.)
lib/routers/ws_highway.py:
- `song_info` gains `drum_parts` (names only; always a list, empty without
drums) so a part picker can bind unconditionally.
- `?drum_part=<id>` on the WS URL selects which part's tab streams as the
`drum_tab`/`drum_hits` messages; the default and any unknown id fall back
to the primary — byte-identical legacy behavior. The `drum_tab` message
carries `part_id` only when a parts list exists, keeping the legacy frame
unchanged.
Tests: tests/test_sloppak_drum_parts.py (9) — the grading invariant +
parallel-ids pin, primary-first resolution with alias identity, legacy
one-part list, pointer-only promotion, per-part failure isolation (bad JSON,
path traversal, duplicate rels), and the drum-only placeholder with pointer
entries. Full suite: the only failures are 9 machine-environmental tests
(installed desktop plugins under LOCALAPPDATA, CRLF/path-shape assertions)
that fail identically on an untouched origin/main checkout on this box.
tools/check_spec_conformance.py passes against the spec's current HEAD
(`drum_tab` and `type` are declared keys); the semantics of the
per-arrangement placement land in feedpak-spec#63 — this PR should merge
after it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xGPjDBF8NTwTK7VQvizix
Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
* Fix drum-part review findings
* Normalize drum part pointer identities
* fix(sloppak): enforce drums grading invariant + green the suite
- Gate the drum-pointer skip on type FIRST: a type:drums/drum entry never becomes
a fretted Arrangement even if it carries a note file/notation (with drum_tab it
is collected as a drum part, without it dropped+warned). Closes the spec
§5.2/§7.5 MUST-NOT hole (a malformed drums+file entry was being fretted-graded).
- Make test_drum_pointer_with_wrong_type_logs_warning robust (attach handler to the
feedBack logger + set WARNING, restore in finally) and fix the root-cause level
leak in test_tuning_provider_isolation.py (finally restored the handler but not
the level, leaking ERROR onto the feedBack tree and turning the suite red under
full ordering).
- Restore the chart-transform CHANGELOG bullet (#952) the drum entry had truncated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: ChrisBeWithYou <christian.a.cowan@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: byrongamatos <xasiklas@gmail.com>
75 lines
2.8 KiB
Python
75 lines
2.8 KiB
Python
"""A raising tuning provider must not take down get_merged() for everyone. (#899)
|
|
|
|
`TuningProviderRegistry.get_merged()` wraps each provider in a try/except precisely so one
|
|
misbehaving plugin cannot break tunings for the rest. The handler called `logger.exception`
|
|
— and there is no `logger` in server.py; the module logger is `log`. So the handler MEANT
|
|
to swallow-and-report instead raised NameError, which propagated out of get_merged().
|
|
|
|
The net effect was the exact opposite of the handler's purpose: one bad provider took the
|
|
whole merged-tunings call down, and the traceback named the wrong problem.
|
|
|
|
Nothing exercised the failure path, which is why it survived. This is that path.
|
|
"""
|
|
|
|
import importlib
|
|
import logging
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture()
|
|
def registry(monkeypatch, tmp_path):
|
|
monkeypatch.setenv("CONFIG_DIR", str(tmp_path))
|
|
sys.modules.pop("server", None)
|
|
mod = importlib.import_module("server")
|
|
yield mod.TuningProviderRegistry()
|
|
|
|
|
|
def test_a_raising_provider_does_not_break_the_others(registry, caplog):
|
|
"""The whole point of the try/except. Before the fix this raised NameError."""
|
|
def boom():
|
|
raise RuntimeError("provider exploded")
|
|
|
|
def good():
|
|
return {"guitar": {"My Tuning": [82.41, 110.0, 146.83, 196.0, 246.94, 329.63]}}
|
|
|
|
registry.register("bad-plugin", boom)
|
|
registry.register("good-plugin", good)
|
|
|
|
merged = registry.get_merged() # must NOT raise
|
|
|
|
assert "My Tuning" in merged["guitar"], (
|
|
"the healthy provider's tuning is missing — one raising provider took down the "
|
|
"merged result for everyone"
|
|
)
|
|
# and the default tunings survive
|
|
assert merged["guitar"], "default tunings were lost"
|
|
|
|
|
|
def test_the_failure_is_actually_logged(registry, caplog):
|
|
"""Swallowing is only acceptable if it is reported. A NameError in the handler meant
|
|
nothing was ever logged — the failure was both fatal AND silent about its real cause."""
|
|
def boom():
|
|
raise RuntimeError("provider exploded")
|
|
|
|
registry.register("bad-plugin", boom)
|
|
|
|
# The feedBack logger sets propagate=False, so pytest's root-logger capture sees
|
|
# NOTHING from it. Attach caplog's handler directly. (test_plugins.py has a
|
|
# capture_logger() context manager for this, but it is not importable from here:
|
|
# pyproject pins pythonpath to [".", "lib"], so `tests` is not a package.)
|
|
lg = logging.getLogger("feedBack")
|
|
orig_level = lg.level
|
|
lg.addHandler(caplog.handler)
|
|
lg.setLevel(logging.ERROR)
|
|
try:
|
|
registry.get_merged()
|
|
finally:
|
|
lg.removeHandler(caplog.handler)
|
|
lg.setLevel(orig_level) # restore, or ERROR leaks onto the feedBack tree
|
|
|
|
assert any("bad-plugin" in r.getMessage() for r in caplog.records), (
|
|
"the raising provider was never named in the logs"
|
|
)
|