mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-21 20:31:21 +00:00
Reword comments/docstrings/strings and rename identifiers that referenced the external game and its file formats: - format-id "psarc" -> "archive"; local vars psarc_path -> song_path, psarc_base -> tone_base - lyrics provenance value "sng" -> "notechart" (legacy "sng" still accepted) - highway_3d fret-ghost scope value "rocksmith" -> "chords" (invalid/legacy values fall back to the default, preserving behaviour) - neutralise references in prose, test names/data, .gitattributes and docs No functional change beyond the renamed identifiers; all Python compiles.
184 lines
7.8 KiB
Python
184 lines
7.8 KiB
Python
"""Unit tests for lib/drums.py — piece vocabulary, presets, and wire helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import drums
|
|
|
|
|
|
# ── Piece vocabulary ──────────────────────────────────────────────────────────
|
|
|
|
def test_pieces_have_required_fields():
|
|
"""Every piece entry must have midi (list), category, shape, color."""
|
|
required = {"midi", "category", "shape", "color"}
|
|
for pid, meta in drums.PIECES.items():
|
|
assert required <= set(meta.keys()), f"{pid} missing fields"
|
|
assert isinstance(meta["midi"], list) and meta["midi"], f"{pid} midi must be non-empty list"
|
|
assert meta["category"] in {"kick", "drum", "cymbal"}, f"{pid} bad category"
|
|
assert isinstance(meta["color"], str) and meta["color"].startswith("#")
|
|
|
|
|
|
def test_midi_to_piece_round_trip_for_gm_drums():
|
|
"""Every MIDI note listed in PIECES round-trips through midi_to_piece."""
|
|
for pid, meta in drums.PIECES.items():
|
|
for m in meta["midi"]:
|
|
resolved = drums.midi_to_piece(m)
|
|
# The reverse map prefers the earliest piece-id for a shared MIDI.
|
|
# The resolved id must still map back to a piece whose midi list
|
|
# contains m, even if it isn't `pid` (e.g. some toms share notes).
|
|
assert resolved is not None, f"MIDI {m} (from {pid}) not in reverse map"
|
|
assert m in drums.PIECES[resolved]["midi"]
|
|
|
|
|
|
def test_midi_to_piece_unknown_returns_none():
|
|
"""MIDI notes outside the GM percussion range we map return None."""
|
|
# 60 is middle C — not a drum note.
|
|
assert drums.midi_to_piece(60) is None
|
|
# 200 is out of MIDI range.
|
|
assert drums.midi_to_piece(200) is None
|
|
|
|
|
|
def test_open_and_closed_hihat_are_distinct():
|
|
"""hh_open and hh_closed must NOT share MIDI notes — the highway relies on
|
|
this to reject a closed-hat strike on an open-hat note."""
|
|
closed = set(drums.PIECES["hh_closed"]["midi"])
|
|
opened = set(drums.PIECES["hh_open"]["midi"])
|
|
assert closed & opened == set(), "hh_closed and hh_open MUST NOT share MIDI"
|
|
|
|
|
|
def test_piece_to_default_midi_returns_list_copy():
|
|
"""piece_to_default_midi must return a fresh list (callers may mutate)."""
|
|
mids = drums.piece_to_default_midi("kick")
|
|
assert mids == [35, 36]
|
|
mids.append(99)
|
|
assert drums.piece_to_default_midi("kick") == [35, 36], "must return a copy"
|
|
|
|
|
|
def test_piece_helpers_have_fallbacks_for_unknown_ids():
|
|
"""Unknown piece-ids must round-trip with safe defaults — a newer sloppak's
|
|
unknown piece should still render rather than crash."""
|
|
assert drums.piece_to_default_midi("future_piece") == []
|
|
assert drums.piece_default_shape("future_piece") == "rect"
|
|
assert drums.piece_default_color("future_piece").startswith("#")
|
|
assert drums.piece_category("future_piece") == "drum"
|
|
|
|
|
|
# ── Presets ──────────────────────────────────────────────────────────────────
|
|
|
|
def test_presets_are_well_formed():
|
|
"""Each preset must be a list of lanes with non-empty `pieces` and a label."""
|
|
for name, preset in drums.PRESETS.items():
|
|
assert isinstance(preset, list) and preset, f"preset {name} empty"
|
|
for lane in preset:
|
|
assert isinstance(lane, dict)
|
|
assert "pieces" in lane and lane["pieces"], f"preset {name}: lane missing pieces"
|
|
assert "label" in lane and lane["label"], f"preset {name}: lane missing label"
|
|
# Every piece-id referenced must exist in PIECES — presets aren't
|
|
# the place to invent new piece-ids.
|
|
for pid in lane["pieces"]:
|
|
assert pid in drums.PIECES, f"preset {name}: unknown piece-id {pid}"
|
|
|
|
|
|
def test_phaseshift8_preserves_legacy_lane_order():
|
|
"""The phase_shift_8 preset must match the v3 drums plugin's HH/Sn/T1/T2/
|
|
T3/Cr/Ri/Ki order so existing sloppaks keep their familiar layout."""
|
|
labels = [lane["label"] for lane in drums.PRESET_PHASESHIFT8]
|
|
assert labels == ["HH", "Sn", "T1", "T2", "T3", "Cr", "Ri", "Ki"]
|
|
|
|
|
|
def test_every_piece_is_routed_by_every_preset():
|
|
"""Adding a new piece-id to PIECES without routing it in every preset
|
|
would let hits for that piece vanish from the highway under that
|
|
preset. This guards against that class of regression."""
|
|
all_pieces = set(drums.PIECES.keys())
|
|
for name, preset in drums.PRESETS.items():
|
|
routed = {pid for lane in preset for pid in lane["pieces"]}
|
|
missing = all_pieces - routed
|
|
assert not missing, (
|
|
f"preset {name!r} doesn't route: {sorted(missing)}. "
|
|
f"Add them to an existing lane (grouped presets) or to their "
|
|
f"own lane (e-kit presets).")
|
|
|
|
|
|
# ── Wire helpers ─────────────────────────────────────────────────────────────
|
|
|
|
def test_validate_drum_tab_accepts_minimal_payload():
|
|
ok, reason = drums.validate_drum_tab({"hits": []})
|
|
assert ok, reason
|
|
|
|
|
|
def test_validate_drum_tab_rejects_non_object():
|
|
ok, _ = drums.validate_drum_tab([])
|
|
assert not ok
|
|
ok, _ = drums.validate_drum_tab(None)
|
|
assert not ok
|
|
|
|
|
|
def test_validate_drum_tab_rejects_missing_hits():
|
|
ok, reason = drums.validate_drum_tab({"version": 1})
|
|
assert not ok
|
|
assert "hits" in reason
|
|
|
|
|
|
def test_validate_drum_tab_rejects_non_int_version():
|
|
ok, _ = drums.validate_drum_tab({"version": "1", "hits": []})
|
|
assert not ok
|
|
|
|
|
|
def test_validate_drum_tab_accepts_unknown_version():
|
|
"""An unknown schema version is logged but the payload is still accepted —
|
|
forward-compat per Principle IV (backwards-compatible custom song library)."""
|
|
ok, _ = drums.validate_drum_tab({"version": 99, "hits": []})
|
|
assert ok
|
|
|
|
|
|
def test_hit_to_wire_canonicalises_known_fields():
|
|
out = drums.hit_to_wire({"t": 0.5001, "p": "kick", "v": 110, "g": True, "f": False, "k": 0.08})
|
|
assert out == {"t": 0.5, "p": "kick", "v": 110, "g": True, "k": 0.08}
|
|
|
|
|
|
def test_hit_to_wire_drops_malformed():
|
|
assert drums.hit_to_wire({"t": 0.5}) is None # no piece
|
|
assert drums.hit_to_wire({"p": "kick"}) is None # no time
|
|
assert drums.hit_to_wire({"t": "x", "p": "kick"}) is None
|
|
assert drums.hit_to_wire({"t": 0.5, "p": ""}) is None
|
|
assert drums.hit_to_wire("not a dict") is None
|
|
|
|
|
|
def test_hit_to_wire_drops_out_of_range_velocity():
|
|
"""Velocity must be 1-127. 0 / 200 / negative get silently dropped."""
|
|
out = drums.hit_to_wire({"t": 0.5, "p": "kick", "v": 0})
|
|
assert "v" not in out
|
|
out = drums.hit_to_wire({"t": 0.5, "p": "kick", "v": 200})
|
|
assert "v" not in out
|
|
out = drums.hit_to_wire({"t": 0.5, "p": "kick", "v": -1})
|
|
assert "v" not in out
|
|
|
|
|
|
def test_hits_to_wire_drops_bad_and_sorts():
|
|
hits = [
|
|
{"t": 2.0, "p": "snare"},
|
|
{"t": 1.0, "p": "kick"},
|
|
{"t": "bad", "p": "kick"}, # dropped
|
|
{"t": 0.5, "p": "ride", "v": 90},
|
|
]
|
|
out = drums.hits_to_wire(hits)
|
|
assert [h["t"] for h in out] == [0.5, 1.0, 2.0]
|
|
assert [h["p"] for h in out] == ["ride", "kick", "snare"]
|
|
|
|
|
|
def test_normalise_kit_dedupes_and_fills_names():
|
|
kit = [
|
|
{"id": "kick", "name": "Kick"},
|
|
{"id": "snare"}, # name derived from id
|
|
{"id": "kick", "name": "duplicate"}, # dedupe wins by first occurrence
|
|
{"id": "", "name": "empty id"}, # dropped
|
|
"not a dict", # dropped
|
|
]
|
|
out = drums.normalise_kit(kit)
|
|
assert out == [{"id": "kick", "name": "Kick"}, {"id": "snare", "name": "Snare"}]
|
|
|
|
|
|
def test_normalise_kit_handles_none():
|
|
assert drums.normalise_kit(None) == []
|
|
assert drums.normalise_kit("not a list") == []
|