Commit 21f83b50 by PLN (Algolia)

fix(tests): orphan-orbits derives its counts, lcxl3-display collects 30 tests

Both were pre-existing, unrelated to today's check-mix.py/gig-log.py work
(spotted while running the suite for that change).

test_orphan_orbits.py: 9 of 41 tests failed (16-row setlist vs a frozen "13",
and 6 HAND_MEASURED orbit sets vs the parser). Root cause confirmed by git log
+ grep, not a parser bug: the setlist grew from 13 to 16 tracks since the
fixture was written, and commit 2376e431 (2026-08-01, "d10 is the riser") added
a d10 safe-riser idiom to 5 of the hand-measured tracks (bombe_dj, wap,
you_my_sunshine, mafia_sans_serif, desire) after the 2026-07-28 hand
measurement date; piment_bresilien went the other way (d10->d9, PLN's own
request, commit 71bb9bc2/5e5a37ad). Fix: the setlist-length assertions are now
derived from an independent sed-based row count of the setlist file (same
pipeline check-tracks.sh uses) instead of a literal that rots every time PLN
adds a track; the HAND_MEASURED dict and the vague_de_crime->bombe_dj orphan
assertion are re-measured by hand (grep on each file's dN lines, not by
copying the parser's own output) against the corpus as it stands today. No
assertion loosened — 41/41 pass, same invariants, current facts.

test_lcxl3_display.py: pytest collected 0 items. Cause: this was written as a
plain script (module-level `check()` calls, run via `python3
tools/tests/test_lcxl3_display.py`), like test_setlist.py, with no `test_*`
function pytest could find — a file that cannot fail under pytest, i.e.
silent coverage loss. Converted to 30 ordinary pytest test_* functions (one
assert group per logical check), same assertions, nothing dropped or
loosened. Verified live: broke rec_line's hour rollover (divmod 3600->3601)
and watched test_rec_line_formats_hmmss_past_an_hour fail with the exact
wrong value (REC 1:02:02 vs 1:02:03), then reverted.

Suite: 234 passed/9 failed/2 skipped -> 273 passed/2 skipped (ignoring
test_setlist.py, a separate plain-script file, out of scope here — it already
breaks whole-directory pytest collection via a module-level sys.exit and
predates this fix). test_gig_log.py untouched: 169 passed before and after.
parent e72eca1d
#!/usr/bin/env python3
"""What can be checked about a screen without looking at it.
The display protocol has NO readback — `configure_display` and `set_text` are
......@@ -7,13 +6,15 @@ a mistake would be silent AND invisible: the bytes on the wire, and the text we
chose to put in them. The evening this fixes was lost to a wrong TARGET, not a
wrong byte, and a wrong target looks exactly like a working driver from the host
side — hence a test that pins the target numbers against the guide's own table.
Run: python3 tools/tests/test_lcxl3_display.py (silence + exit 0 = pass)
"""
from __future__ import annotations
import importlib.util
import pathlib
import sys
import pytest
TOOLS = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(TOOLS))
......@@ -25,12 +26,7 @@ _spec = importlib.util.spec_from_file_location("drv", TOOLS / "lcxl3-driver.py")
drv = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(drv)
FAILED = []
def check(cond, msg):
if not cond:
FAILED.append(msg)
NOW = 1_000_000.0
class FakeOut:
......@@ -53,146 +49,243 @@ def fake_surface():
return s
# ---- the guide's target table, quoted in lcxl3.py, pinned here --------------
# "05h (5) - 24h (36): Temporary display for Analogue controls (same as CC
# indices, 05h (5) - 0Ch (12): Faders, 0Dh (13) - 24h (36): Encoders)"
check(L.TGT_CONTROL_FIRST == 0x05, "per-control target range must start at 05h")
check(L.TGT_CONTROL_LAST == 0x24, "per-control target range must end at 24h")
check(all(L.is_analogue(i) for i in L.FADERS), "all 8 faders own a display target")
check(all(L.is_analogue(i) for i in L.ENCODERS), "all 24 encoders own one")
check(not any(L.is_analogue(i) for i in L.BUTTONS),
"buttons own NO per-control target — that is why they never lost the screen")
check(L.TGT_CONTROL_LAST + 1 == min(L.BUTTONS),
"the analogue targets must end exactly where the buttons begin")
# ---- config byte: bits 5+6 are the native display's on-switch ---------------
# "Bit 6: Allow ... automatically on Change (default: Set).
# Bit 5: Allow ... automatically on Touch (default: Set)."
check(L.display_config(4, True, True) == 0x64, "arr 4 + both auto bits = 0x64")
check(L.display_config(4, False, False) == 0x04, "arr 4, natives off = 0x04")
check(L.display_config(2, False, False) == 0x02,
"0x02 is what the OLD code sent to 0x36: arrangement 2, autos irrelevant "
"there — it never touched the per-control targets at all")
check(L.display_config(1, True, False) == 0x41, "bit 6 only = 0x41")
# The arrangement must survive as bits 0-4 and never collide with the auto bits.
check(all(L.display_config(a) & 0x1F == a for a in range(32)),
"arrangement occupies bits 0-4 intact")
# ---- label_control puts the right bytes on the wire ------------------------
s = fake_surface()
s.label_control(0x18, "d4 crushbus") # encoder r2c4
data = [list(m.data) for m in s.out.sent]
check(len(data) == 2, "label_control sends exactly configure + set_text")
check(data[0] == [0x00, 0x20, 0x29, 0x02, 0x15, 0x04, 0x18, 0x64],
f"configure bytes wrong: {data[0]}")
check(data[1][:8] == [0x00, 0x20, 0x29, 0x02, 0x15, 0x06, 0x18, 0x00],
f"set_text header wrong (target 0x18, field 0): {data[1][:8]}")
check(bytes(data[1][8:]).decode() == "d4 crushbus", "the name must arrive verbatim")
# The DJF path: same target, autos CLEARED so our own overlay wins uncontested.
s = fake_surface()
s.label_control(0x1D, "DJF percs", native=False)
check(list(s.out.sent[0].data)[-1] == 0x04,
"native=False must clear bits 5+6 (config 0x04, arrangement still non-zero "
"because 0 would CANCEL the display)")
# A button has no target and must be refused loudly, not sent into the void.
try:
fake_surface().label_control(0x25, "gate")
FAILED.append("label_control(button) must raise, not silently no-op")
except ValueError:
pass
# ---- set_text must stop eating the reassigned control codes ----------------
s = fake_surface()
s.set_text(L.TGT_TEMPORARY, 2, lang.ZERO_FRAMES[0]) # hearts around a zero
body = bytes(list(s.out.sent[0].data)[8:])
check(body.count(0x1E) == 2,
f"the two hearts (1Eh) must survive the ASCII filter, got {body!r}")
# ---- the labels themselves -------------------------------------------------
# Faders carry no ^NN at all — they are Ardour-learned levels — so their label
# has to come from the grid's role table, not from a track's context.
d1 = grid.CELL_TO_CC[("D", 1)]
check(lang.control_label(d1, None) == "d1 level [ard]",
f"fader label from CC_ROLE, marked Ardour-owned: {lang.control_label(d1, None)!r}")
check(lang.control_label(d1, None, mapped=False) == "D1 unmapped",
"an unmapped control must SAY so — the screen may not contradict a dark LED")
c1 = grid.CELL_TO_CC[("C", 1)]
check(lang.control_label(c1, None) == "DJF percs", "family filter names its bloc")
f1 = grid.CELL_TO_CC[("F", 1)]
check(lang.control_label(f1, None) == "MUTE kick",
"gMute groups by ROLE, not bloc — kick alone is family 1")
b4 = grid.CELL_TO_CC[("B", 4)]
check(lang.control_label(b4, "# crushbus") == "d4 # crushbus",
"orbit first, then what the control does in this track")
check(len(lang.control_label(b4, "# a_very_long_effect_name")) <= 16,
"labels must fit the panel")
# ---- parse_context: the trailing stage note beats the Haskell ---------------
# This line is real (piment_bresilien.tidal:87) and used to yield `# n "23")) --`.
ctx = drv.parse_context(' $ midiOn "^56" ((# n "23")) -- Raise COMEON!\n')
check(ctx.get(56) == "Raise COMEON!",
f"PLN's own words must win over the Haskell body: {ctx.get(56)!r}")
# With no stage note, the useful noun is the sample, not the combinator.
ctx = drv.parse_context(' $ midiOn "^28" (loopAt 1 . (# "break:18"))\n')
check(ctx.get(28) == "break:18",
f"no comment -> first quoted string, not `loopAt 1 . (#`: {ctx.get(28)!r}")
ctx = drv.parse_context(' $ midiOn "^25" ((# n "6"))\n')
check(ctx.get(25) == "n 6",
f"a numeric string keeps its identifier for meaning: {ctx.get(25)!r}")
# A commented-out line must still never label a control.
check(drv.parse_context('-- $ midiOn "^56" (id) -- nope\n') == {},
"commented-out code labels nothing")
# ---- label_table: every analogue control, and only those -------------------
m = drv.build_map()
rows = drv.label_table(m, {}, set(m.values()), lang, grid)
check(len(rows) == 32, f"32 analogue controls own a display, got {len(rows)}")
check(sorted(r[0] for r in rows) == list(range(0x05, 0x25)),
"the table must cover targets 05h-24h exactly")
check(all(r[2] for r in rows), "no control may be left nameless")
djf = [r for r in rows if grid.CC_ROLE.get(r[1], ("", 0))[0] == "family_filter"]
check(len(djf) == 3 and not any(r[3] for r in djf),
"the 3 DJF knobs keep their rich readout: drawn by US, natives suppressed")
check(all(r[3] for r in rows if r not in djf),
"everything else is drawn by the firmware, from our name")
# The fallback flag must hand the whole surface back to our own overlay.
rows = drv.label_table(m, {}, set(m.values()), lang, grid, overlay_only=True)
check(not any(r[3] for r in rows), "--oled-overlay suppresses every native display")
# ---- home_lines: the third row defaults to the drip, footer overrides it --
check(lang.home_lines("rose_rouge", "120BPM 3:45") == ("ROSE_ROUGE", "120BPM 3:45", "ParVagues"),
"no footer arg -> unchanged behaviour, the everyday drip")
check(lang.home_lines("x", "y", "REC 01:23") == ("X", "y", "REC 01:23"),
"an explicit footer replaces the drip verbatim")
check(len(lang.home_lines("x", "y", "?REC 1:02:03")[2]) <= 16,
"a footer must be clipped to the 16-char panel like the other two rows")
# ---- rec_line: pure (rec dict, now) -> optional "REC h:mm:ss" ---------------
NOW = 1_000_000.0
check(drv.rec_line(None, NOW) is None, "no rec record at all -> keep the drip")
check(drv.rec_line({}, NOW) is None, "an empty/malformed record -> keep the drip")
check(drv.rec_line({"k": "rec", "rec": False, "t": NOW, "el": 12}, NOW) is None,
"rec:false -> keep the drip even with a fresh timestamp")
check(drv.rec_line({"k": "rec", "rec": True, "t": NOW, "el": 83}, NOW) == "REC 01:23",
"83s elapsed -> mm:ss, zero-padded")
check(drv.rec_line({"k": "rec", "rec": True, "t": NOW, "el": 3723}, NOW) == "REC 1:02:03",
"past one hour -> h:mm:ss")
check(drv.rec_line({"k": "rec", "rec": True, "t": NOW, "el": 5, "conflict": True}, NOW)
== "?REC 00:05",
"conflict:true prefixes '?' — the two lenses disagree, don't trust blindly")
check(drv.rec_line({"k": "rec", "rec": True, "t": NOW - 10, "el": 5}, NOW) is None,
"a 10s-old rec line is a dead heartbeat -> keep the drip, never freeze a REC")
check(drv.rec_line({"k": "rec", "rec": True, "t": NOW, "el": -1}, NOW) is None,
"a negative elapsed is malformed, not a countdown -> keep the drip")
check(drv.rec_line({"k": "rec", "rec": True, "el": 5}, NOW) is None,
"a record with no timestamp at all can never be judged fresh -> keep the drip")
# ---- last_rec: tail-reads the newest k:"rec" line, ignores everything else --
import tempfile
with tempfile.TemporaryDirectory() as td:
p = pathlib.Path(td) / "gig-fake.jsonl"
# --------------------------------------------------------------------------- #
# the guide's target table, quoted in lcxl3.py, pinned here
# --------------------------------------------------------------------------- #
def test_control_target_range_matches_the_guide():
""""05h (5) - 24h (36): Temporary display for Analogue controls (same as CC
indices, 05h (5) - 0Ch (12): Faders, 0Dh (13) - 24h (36): Encoders)" """
assert L.TGT_CONTROL_FIRST == 0x05, "per-control target range must start at 05h"
assert L.TGT_CONTROL_LAST == 0x24, "per-control target range must end at 24h"
assert all(L.is_analogue(i) for i in L.FADERS), "all 8 faders own a display target"
assert all(L.is_analogue(i) for i in L.ENCODERS), "all 24 encoders own one"
assert not any(L.is_analogue(i) for i in L.BUTTONS), (
"buttons own NO per-control target — that is why they never lost the screen")
assert L.TGT_CONTROL_LAST + 1 == min(L.BUTTONS), (
"the analogue targets must end exactly where the buttons begin")
# --------------------------------------------------------------------------- #
# config byte: bits 5+6 are the native display's on-switch
# --------------------------------------------------------------------------- #
def test_display_config_native_auto_bits():
""""Bit 6: Allow ... automatically on Change (default: Set).
Bit 5: Allow ... automatically on Touch (default: Set)." """
assert L.display_config(4, True, True) == 0x64, "arr 4 + both auto bits = 0x64"
assert L.display_config(4, False, False) == 0x04, "arr 4, natives off = 0x04"
assert L.display_config(2, False, False) == 0x02, (
"0x02 is what the OLD code sent to 0x36: arrangement 2, autos irrelevant "
"there — it never touched the per-control targets at all")
assert L.display_config(1, True, False) == 0x41, "bit 6 only = 0x41"
def test_display_config_arrangement_bits_never_collide_with_auto_bits():
"""The arrangement must survive as bits 0-4 and never collide with the auto bits."""
assert all(L.display_config(a) & 0x1F == a for a in range(32)), (
"arrangement occupies bits 0-4 intact")
# --------------------------------------------------------------------------- #
# label_control puts the right bytes on the wire
# --------------------------------------------------------------------------- #
def test_label_control_sends_configure_then_set_text():
s = fake_surface()
s.label_control(0x18, "d4 crushbus") # encoder r2c4
data = [list(m.data) for m in s.out.sent]
assert len(data) == 2, "label_control sends exactly configure + set_text"
assert data[0] == [0x00, 0x20, 0x29, 0x02, 0x15, 0x04, 0x18, 0x64], (
f"configure bytes wrong: {data[0]}")
assert data[1][:8] == [0x00, 0x20, 0x29, 0x02, 0x15, 0x06, 0x18, 0x00], (
f"set_text header wrong (target 0x18, field 0): {data[1][:8]}")
assert bytes(data[1][8:]).decode() == "d4 crushbus", "the name must arrive verbatim"
def test_label_control_native_false_clears_auto_bits_keeps_arrangement():
"""The DJF path: same target, autos CLEARED so our own overlay wins uncontested."""
s = fake_surface()
s.label_control(0x1D, "DJF percs", native=False)
assert list(s.out.sent[0].data)[-1] == 0x04, (
"native=False must clear bits 5+6 (config 0x04, arrangement still non-zero "
"because 0 would CANCEL the display)")
def test_label_control_on_a_button_raises_instead_of_silently_no_opping():
"""A button has no target and must be refused loudly, not sent into the void."""
with pytest.raises(ValueError):
fake_surface().label_control(0x25, "gate")
# --------------------------------------------------------------------------- #
# set_text must stop eating the reassigned control codes
# --------------------------------------------------------------------------- #
def test_set_text_preserves_reassigned_control_codes():
s = fake_surface()
s.set_text(L.TGT_TEMPORARY, 2, lang.ZERO_FRAMES[0]) # hearts around a zero
body = bytes(list(s.out.sent[0].data)[8:])
assert body.count(0x1E) == 2, (
f"the two hearts (1Eh) must survive the ASCII filter, got {body!r}")
# --------------------------------------------------------------------------- #
# the labels themselves
# --------------------------------------------------------------------------- #
def test_fader_label_comes_from_the_role_table_not_track_context():
"""Faders carry no ^NN at all — they are Ardour-learned levels — so their label
has to come from the grid's role table, not from a track's context."""
d1 = grid.CELL_TO_CC[("D", 1)]
assert lang.control_label(d1, None) == "d1 level [ard]", (
f"fader label from CC_ROLE, marked Ardour-owned: {lang.control_label(d1, None)!r}")
assert lang.control_label(d1, None, mapped=False) == "D1 unmapped", (
"an unmapped control must SAY so — the screen may not contradict a dark LED")
def test_control_label_family_filter_and_role_naming():
c1 = grid.CELL_TO_CC[("C", 1)]
assert lang.control_label(c1, None) == "DJF percs", "family filter names its bloc"
f1 = grid.CELL_TO_CC[("F", 1)]
assert lang.control_label(f1, None) == "MUTE kick", (
"gMute groups by ROLE, not bloc — kick alone is family 1")
b4 = grid.CELL_TO_CC[("B", 4)]
assert lang.control_label(b4, "# crushbus") == "d4 # crushbus", (
"orbit first, then what the control does in this track")
assert len(lang.control_label(b4, "# a_very_long_effect_name")) <= 16, (
"labels must fit the panel")
# --------------------------------------------------------------------------- #
# parse_context: the trailing stage note beats the Haskell
# --------------------------------------------------------------------------- #
def test_parse_context_stage_note_beats_the_haskell():
"""This line is real (piment_bresilien.tidal:87) and used to yield `# n "23")) --`."""
ctx = drv.parse_context(' $ midiOn "^56" ((# n "23")) -- Raise COMEON!\n')
assert ctx.get(56) == "Raise COMEON!", (
f"PLN's own words must win over the Haskell body: {ctx.get(56)!r}")
def test_parse_context_falls_back_to_the_quoted_sample_with_no_comment():
"""With no stage note, the useful noun is the sample, not the combinator."""
ctx = drv.parse_context(' $ midiOn "^28" (loopAt 1 . (# "break:18"))\n')
assert ctx.get(28) == "break:18", (
f"no comment -> first quoted string, not `loopAt 1 . (#`: {ctx.get(28)!r}")
def test_parse_context_numeric_string_keeps_its_identifier():
ctx = drv.parse_context(' $ midiOn "^25" ((# n "6"))\n')
assert ctx.get(25) == "n 6", (
f"a numeric string keeps its identifier for meaning: {ctx.get(25)!r}")
def test_parse_context_commented_out_line_labels_nothing():
assert drv.parse_context('-- $ midiOn "^56" (id) -- nope\n') == {}, (
"commented-out code labels nothing")
# --------------------------------------------------------------------------- #
# label_table: every analogue control, and only those
# --------------------------------------------------------------------------- #
def test_label_table_covers_every_analogue_control_and_only_those():
m = drv.build_map()
rows = drv.label_table(m, {}, set(m.values()), lang, grid)
assert len(rows) == 32, f"32 analogue controls own a display, got {len(rows)}"
assert sorted(r[0] for r in rows) == list(range(0x05, 0x25)), (
"the table must cover targets 05h-24h exactly")
assert all(r[2] for r in rows), "no control may be left nameless"
djf = [r for r in rows if grid.CC_ROLE.get(r[1], ("", 0))[0] == "family_filter"]
assert len(djf) == 3 and not any(r[3] for r in djf), (
"the 3 DJF knobs keep their rich readout: drawn by US, natives suppressed")
assert all(r[3] for r in rows if r not in djf), (
"everything else is drawn by the firmware, from our name")
def test_label_table_overlay_only_suppresses_every_native_display():
"""The fallback flag must hand the whole surface back to our own overlay."""
m = drv.build_map()
rows = drv.label_table(m, {}, set(m.values()), lang, grid, overlay_only=True)
assert not any(r[3] for r in rows), "--oled-overlay suppresses every native display"
# --------------------------------------------------------------------------- #
# home_lines: the third row defaults to the drip, footer overrides it
# --------------------------------------------------------------------------- #
def test_home_lines_default_third_row_is_the_drip():
assert lang.home_lines("rose_rouge", "120BPM 3:45") == (
"ROSE_ROUGE", "120BPM 3:45", "ParVagues"), (
"no footer arg -> unchanged behaviour, the everyday drip")
def test_home_lines_explicit_footer_replaces_the_drip():
assert lang.home_lines("x", "y", "REC 01:23") == ("X", "y", "REC 01:23"), (
"an explicit footer replaces the drip verbatim")
def test_home_lines_footer_is_clipped_to_the_panel_width():
assert len(lang.home_lines("x", "y", "?REC 1:02:03")[2]) <= 16, (
"a footer must be clipped to the 16-char panel like the other two rows")
# --------------------------------------------------------------------------- #
# rec_line: pure (rec dict, now) -> optional "REC h:mm:ss"
# --------------------------------------------------------------------------- #
def test_rec_line_no_record_at_all_keeps_the_drip():
assert drv.rec_line(None, NOW) is None, "no rec record at all -> keep the drip"
def test_rec_line_empty_record_keeps_the_drip():
assert drv.rec_line({}, NOW) is None, "an empty/malformed record -> keep the drip"
def test_rec_line_false_keeps_the_drip_even_with_a_fresh_timestamp():
assert drv.rec_line({"k": "rec", "rec": False, "t": NOW, "el": 12}, NOW) is None, (
"rec:false -> keep the drip even with a fresh timestamp")
def test_rec_line_formats_mmss_under_an_hour():
assert drv.rec_line({"k": "rec", "rec": True, "t": NOW, "el": 83}, NOW) == "REC 01:23", (
"83s elapsed -> mm:ss, zero-padded")
def test_rec_line_formats_hmmss_past_an_hour():
assert drv.rec_line({"k": "rec", "rec": True, "t": NOW, "el": 3723}, NOW) == "REC 1:02:03", (
"past one hour -> h:mm:ss")
def test_rec_line_conflict_flag_prefixes_a_question_mark():
assert drv.rec_line(
{"k": "rec", "rec": True, "t": NOW, "el": 5, "conflict": True}, NOW
) == "?REC 00:05", (
"conflict:true prefixes '?' — the two lenses disagree, don't trust blindly")
def test_rec_line_stale_heartbeat_never_freezes_a_rec_display():
assert drv.rec_line({"k": "rec", "rec": True, "t": NOW - 10, "el": 5}, NOW) is None, (
"a 10s-old rec line is a dead heartbeat -> keep the drip, never freeze a REC")
def test_rec_line_negative_elapsed_is_malformed_not_a_countdown():
assert drv.rec_line({"k": "rec", "rec": True, "t": NOW, "el": -1}, NOW) is None, (
"a negative elapsed is malformed, not a countdown -> keep the drip")
def test_rec_line_missing_timestamp_can_never_be_judged_fresh():
assert drv.rec_line({"k": "rec", "rec": True, "el": 5}, NOW) is None, (
"a record with no timestamp at all can never be judged fresh -> keep the drip")
# --------------------------------------------------------------------------- #
# last_rec: tail-reads the newest k:"rec" line, ignores everything else
# --------------------------------------------------------------------------- #
def _fake_gig_log(tmp_path):
p = tmp_path / "gig-fake.jsonl"
p.write_text(
'{"k": "s", "t": 1.0}\n'
'{"k": "rec", "t": 2.0, "rec": true, "el": 1}\n'
......@@ -200,16 +293,21 @@ with tempfile.TemporaryDirectory() as td:
'{"k": "cc", "t": 3.0}\n'
'{"k": "rec", "t": 4.0, "rec": true, "el": 3}\n'
)
return p
def test_last_rec_tail_reads_the_newest_rec_line(tmp_path):
p = _fake_gig_log(tmp_path)
got = drv.last_rec(p)
check(got is not None and got.get("t") == 4.0,
f"must return the LAST rec line, not the first or a non-rec one: {got!r}")
check(drv.last_rec(pathlib.Path(td) / "gone.jsonl") is None,
"a missing file must degrade to None, never raise")
check(drv.newest_gig_log(pathlib.Path(td) / "no-such-dir") is None,
"a missing gig-log directory must degrade to None, never raise")
if FAILED:
print(f"{len(FAILED)} FAILED:")
for f in FAILED:
print(" -", f)
sys.exit(1)
assert got is not None and got.get("t") == 4.0, (
f"must return the LAST rec line, not the first or a non-rec one: {got!r}")
def test_last_rec_missing_file_degrades_to_none(tmp_path):
assert drv.last_rec(tmp_path / "gone.jsonl") is None, (
"a missing file must degrade to None, never raise")
def test_newest_gig_log_missing_directory_degrades_to_none(tmp_path):
assert drv.newest_gig_log(tmp_path / "no-such-dir") is None, (
"a missing gig-log directory must degrade to None, never raise")
......@@ -156,11 +156,26 @@ def test_sound_hints_never_raise_on_an_orbit_with_no_quoted_name(tmp_path):
# the setlist
# --------------------------------------------------------------------------- #
def _raw_setlist_rows() -> list[str]:
"""Independent ground truth for what the setlist file names: the same sed
pipeline check-tracks.sh uses (strip full-line + trailing comments, drop
blanks), NOT oo.load_setlist() counting itself. The set grows over time —
13 became 16 between 2026-07-28 and 2026-09-05 as PLN added tracks — so any
expectation here has to track the file, not a frozen literal."""
import subprocess
out = subprocess.run(
["sed", "-e", "s/#.*//", "-e", "s/[[:space:]]*$//", str(oo.SETLIST)],
capture_output=True, text=True, check=True).stdout
return [ln for ln in out.splitlines() if ln.strip()]
def test_the_real_setlist_loads_and_every_track_exists():
"""A setlist that silently drops a track silently drops a transition, so
load_setlist raises rather than skipping."""
load_setlist raises rather than skipping. The row count is checked against an
independent count of the setlist file (see _raw_setlist_rows), not a frozen
literal — a hardcoded number rots the first time PLN adds a track."""
setlist = oo.load_setlist()
assert len(setlist) == 13
assert len(setlist) == len(_raw_setlist_rows())
for p, label in setlist:
assert p.exists()
assert label
......@@ -187,24 +202,35 @@ def test_setlist_comments_and_blank_lines_are_ignored(tmp_path, monkeypatch):
# parser that only agrees with fixtures
# --------------------------------------------------------------------------- #
# Hand-measured by PLN + me on 2026-07-28, before this parser existed. If the
# parser and the hand count ever disagree, ONE of them is wrong and the tool must
# not be trusted until that is resolved.
# Hand-measured by PLN + me on 2026-07-28, before this parser existed, and
# RE-measured on 2026-09-05 (by reading each file's column-0 `dN` declarations
# with grep, not by copying the parser's output — a hand measurement that quotes
# the thing it checks is a tautology). If the parser and the hand count ever
# disagree, ONE of them is wrong and the tool must not be trusted until that is
# resolved.
HAND_MEASURED = {
"bombe_dj": {1, 2, 3, 4, 5, 7, 8, 9},
"wap": {1, 2, 3, 4, 5, 7, 8, 9},
"piment_bresilien": {1, 2, 3, 4, 5, 7, 8, 10},
# 2026-08-01, commit 2376e43 ("d10 is the riser") added a d10 safe-riser
# idiom to 7 tracks that lacked one — bombe_dj, wap, you_my_sunshine,
# mafia_sans_serif and desire among them. The 07-28 fixture predates that
# commit and was missing d10 on all five; re-measured against the file as
# it stands now.
"bombe_dj": {1, 2, 3, 4, 5, 7, 8, 9, 10},
"wap": {1, 2, 3, 4, 5, 7, 8, 9, 10},
# piment_bresilien is the ONE track in this set that moved its riser the
# other way: PLN asked (2026-08-02, commits 71bb9bc/5e5a37a) to move d10 to
# d9 ("eg a synmenace iirc") rather than adopt the new d10 idiom — the file
# carries a `-- Menace` comment on d9 confirming it. So this one loses d10,
# not gains it.
"piment_bresilien": {1, 2, 3, 4, 5, 7, 8, 9},
"perfect": {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
"gimme_acid": {1, 2, 3, 4, 5, 8, 9, 10, 11, 12},
"vague_de_crime": {1, 2, 3, 4, 5, 6, 7, 8, 10},
"mafia_sans_serif": {1, 2, 3, 4, 5, 7, 8},
"mafia_sans_serif": {1, 2, 3, 4, 5, 7, 8, 10},
# d7 became d6 on 2026-07-30 (PLN's edit, committed in ce887b7: the Guitar
# Sunshine block moved down a slot with ^91->^90 and ^59->^58, the same
# consolidation he made in do_it_right). Re-derived by reading the file's
# column-0 `dN` declarations, not by copying the parser's output — a hand
# measurement that quotes the thing it checks is a tautology.
"you_my_sunshine": {1, 2, 3, 4, 5, 6, 8, 9, 11},
"desire": {1, 2, 3, 4, 5, 6, 7, 8, 9},
# consolidation he made in do_it_right).
"you_my_sunshine": {1, 2, 3, 4, 5, 6, 8, 9, 10, 11},
"desire": {1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
"the_revolution_will_be_sampled": {1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12},
}
......@@ -217,8 +243,12 @@ def test_the_parser_agrees_with_the_hand_measurement(stem, want):
def test_the_ear_report_reproduces_vague_de_crime_to_bombe_dj():
"""The ORIGINAL bug report, as a test. PLN heard the crimewave synth survive
into the next track; the mechanism says d6 and d10 are the survivors."""
assert oo.orphans(oo.resolve("vague_de_crime"), oo.resolve("bombe_dj")) == {6, 10}
into the next track; the mechanism says d6 is the survivor.
d10 was a second survivor when this was first written, but commit 2376e43
(2026-08-01) gave bombe_dj its own d10 safe riser — both tracks now declare
d10, so it stopped being a ghost. Only d6 (crimewave) still orphans."""
assert oo.orphans(oo.resolve("vague_de_crime"), oo.resolve("bombe_dj")) == {6}
def test_d6_of_vague_de_crime_really_is_the_crimewave_sound_he_heard():
......@@ -281,14 +311,11 @@ def test_check_tracks_reads_the_SAME_setlist_file():
def test_the_setlist_survives_the_shell_parse_too():
"""The setlist annotates each path with codename + BPM after a '#'. The shell
reader has to strip TRAILING comments, not just full-line ones, or every track
reports "no such file"."""
reports "no such file". Row count is checked against oo.load_setlist() itself
(derived), not a frozen literal — see _raw_setlist_rows."""
import re as _re
import subprocess
out = subprocess.run(
["sed", "-e", "s/#.*//", "-e", "s/[[:space:]]*$//", str(oo.SETLIST)],
capture_output=True, text=True, check=True).stdout
rows = [ln for ln in out.splitlines() if ln.strip()]
assert len(rows) == 13
rows = _raw_setlist_rows()
assert len(rows) == len(oo.load_setlist())
for r in rows:
assert not _re.search(r"[#\[]", r), f"comment leaked into {r!r}"
assert (oo.REPO / r).exists(), r
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment