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. """What can be checked about a screen without looking at it.
The display protocol has NO readback — `configure_display` and `set_text` are 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 ...@@ -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 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 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. 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 importlib.util
import pathlib import pathlib
import sys import sys
import pytest
TOOLS = pathlib.Path(__file__).resolve().parent.parent TOOLS = pathlib.Path(__file__).resolve().parent.parent
sys.path.insert(0, str(TOOLS)) sys.path.insert(0, str(TOOLS))
...@@ -25,12 +26,7 @@ _spec = importlib.util.spec_from_file_location("drv", TOOLS / "lcxl3-driver.py") ...@@ -25,12 +26,7 @@ _spec = importlib.util.spec_from_file_location("drv", TOOLS / "lcxl3-driver.py")
drv = importlib.util.module_from_spec(_spec) drv = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(drv) _spec.loader.exec_module(drv)
FAILED = [] NOW = 1_000_000.0
def check(cond, msg):
if not cond:
FAILED.append(msg)
class FakeOut: class FakeOut:
...@@ -53,146 +49,243 @@ def fake_surface(): ...@@ -53,146 +49,243 @@ def fake_surface():
return s 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 # the guide's target table, quoted in lcxl3.py, pinned here
# 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") def test_control_target_range_matches_the_guide():
check(all(L.is_analogue(i) for i in L.FADERS), "all 8 faders own a display target") """"05h (5) - 24h (36): Temporary display for Analogue controls (same as CC
check(all(L.is_analogue(i) for i in L.ENCODERS), "all 24 encoders own one") indices, 05h (5) - 0Ch (12): Faders, 0Dh (13) - 24h (36): Encoders)" """
check(not any(L.is_analogue(i) for i in L.BUTTONS), assert L.TGT_CONTROL_FIRST == 0x05, "per-control target range must start at 05h"
"buttons own NO per-control target — that is why they never lost the screen") assert L.TGT_CONTROL_LAST == 0x24, "per-control target range must end at 24h"
check(L.TGT_CONTROL_LAST + 1 == min(L.BUTTONS), assert all(L.is_analogue(i) for i in L.FADERS), "all 8 faders own a display target"
"the analogue targets must end exactly where the buttons begin") 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), (
# ---- config byte: bits 5+6 are the native display's on-switch --------------- "buttons own NO per-control target — that is why they never lost the screen")
# "Bit 6: Allow ... automatically on Change (default: Set). assert L.TGT_CONTROL_LAST + 1 == min(L.BUTTONS), (
# Bit 5: Allow ... automatically on Touch (default: Set)." "the analogue targets must end exactly where the buttons begin")
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 " # config byte: bits 5+6 are the native display's on-switch
"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. def test_display_config_native_auto_bits():
check(all(L.display_config(a) & 0x1F == a for a in range(32)), """"Bit 6: Allow ... automatically on Change (default: Set).
"arrangement occupies bits 0-4 intact") Bit 5: Allow ... automatically on Touch (default: Set)." """
assert L.display_config(4, True, True) == 0x64, "arr 4 + both auto bits = 0x64"
# ---- label_control puts the right bytes on the wire ------------------------ assert L.display_config(4, False, False) == 0x04, "arr 4, natives off = 0x04"
s = fake_surface() assert L.display_config(2, False, False) == 0x02, (
s.label_control(0x18, "d4 crushbus") # encoder r2c4 "0x02 is what the OLD code sent to 0x36: arrangement 2, autos irrelevant "
data = [list(m.data) for m in s.out.sent] "there — it never touched the per-control targets at all")
check(len(data) == 2, "label_control sends exactly configure + set_text") assert L.display_config(1, True, False) == 0x41, "bit 6 only = 0x41"
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], def test_display_config_arrangement_bits_never_collide_with_auto_bits():
f"set_text header wrong (target 0x18, field 0): {data[1][:8]}") """The arrangement must survive as bits 0-4 and never collide with the auto bits."""
check(bytes(data[1][8:]).decode() == "d4 crushbus", "the name must arrive verbatim") assert all(L.display_config(a) & 0x1F == a for a in range(32)), (
"arrangement occupies bits 0-4 intact")
# 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, # label_control puts the right bytes on the wire
"native=False must clear bits 5+6 (config 0x04, arrangement still non-zero " # --------------------------------------------------------------------------- #
"because 0 would CANCEL the display)")
def test_label_control_sends_configure_then_set_text():
# A button has no target and must be refused loudly, not sent into the void. s = fake_surface()
try: s.label_control(0x18, "d4 crushbus") # encoder r2c4
fake_surface().label_control(0x25, "gate") data = [list(m.data) for m in s.out.sent]
FAILED.append("label_control(button) must raise, not silently no-op") assert len(data) == 2, "label_control sends exactly configure + set_text"
except ValueError: assert data[0] == [0x00, 0x20, 0x29, 0x02, 0x15, 0x04, 0x18, 0x64], (
pass f"configure bytes wrong: {data[0]}")
assert data[1][:8] == [0x00, 0x20, 0x29, 0x02, 0x15, 0x06, 0x18, 0x00], (
# ---- set_text must stop eating the reassigned control codes ---------------- f"set_text header wrong (target 0x18, field 0): {data[1][:8]}")
s = fake_surface() assert bytes(data[1][8:]).decode() == "d4 crushbus", "the name must arrive verbatim"
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, def test_label_control_native_false_clears_auto_bits_keeps_arrangement():
f"the two hearts (1Eh) must survive the ASCII filter, got {body!r}") """The DJF path: same target, autos CLEARED so our own overlay wins uncontested."""
s = fake_surface()
# ---- the labels themselves ------------------------------------------------- s.label_control(0x1D, "DJF percs", native=False)
# Faders carry no ^NN at all — they are Ardour-learned levels — so their label assert list(s.out.sent[0].data)[-1] == 0x04, (
# has to come from the grid's role table, not from a track's context. "native=False must clear bits 5+6 (config 0x04, arrangement still non-zero "
d1 = grid.CELL_TO_CC[("D", 1)] "because 0 would CANCEL the display)")
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", def test_label_control_on_a_button_raises_instead_of_silently_no_opping():
"an unmapped control must SAY so — the screen may not contradict a dark LED") """A button has no target and must be refused loudly, not sent into the void."""
c1 = grid.CELL_TO_CC[("C", 1)] with pytest.raises(ValueError):
check(lang.control_label(c1, None) == "DJF percs", "family filter names its bloc") fake_surface().label_control(0x25, "gate")
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)] # set_text must stop eating the reassigned control codes
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, def test_set_text_preserves_reassigned_control_codes():
"labels must fit the panel") s = fake_surface()
s.set_text(L.TGT_TEMPORARY, 2, lang.ZERO_FRAMES[0]) # hearts around a zero
# ---- parse_context: the trailing stage note beats the Haskell --------------- body = bytes(list(s.out.sent[0].data)[8:])
# This line is real (piment_bresilien.tidal:87) and used to yield `# n "23")) --`. assert body.count(0x1E) == 2, (
ctx = drv.parse_context(' $ midiOn "^56" ((# n "23")) -- Raise COMEON!\n') f"the two hearts (1Eh) must survive the ASCII filter, got {body!r}")
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') # the labels themselves
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') def test_fader_label_comes_from_the_role_table_not_track_context():
check(ctx.get(25) == "n 6", """Faders carry no ^NN at all — they are Ardour-learned levels — so their label
f"a numeric string keeps its identifier for meaning: {ctx.get(25)!r}") has to come from the grid's role table, not from a track's context."""
# A commented-out line must still never label a control. d1 = grid.CELL_TO_CC[("D", 1)]
check(drv.parse_context('-- $ midiOn "^56" (id) -- nope\n') == {}, assert lang.control_label(d1, None) == "d1 level [ard]", (
"commented-out code labels nothing") 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", (
# ---- label_table: every analogue control, and only those ------------------- "an unmapped control must SAY so — the screen may not contradict a dark LED")
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)}") def test_control_label_family_filter_and_role_naming():
check(sorted(r[0] for r in rows) == list(range(0x05, 0x25)), c1 = grid.CELL_TO_CC[("C", 1)]
"the table must cover targets 05h-24h exactly") assert lang.control_label(c1, None) == "DJF percs", "family filter names its bloc"
check(all(r[2] for r in rows), "no control may be left nameless") f1 = grid.CELL_TO_CC[("F", 1)]
djf = [r for r in rows if grid.CC_ROLE.get(r[1], ("", 0))[0] == "family_filter"] assert lang.control_label(f1, None) == "MUTE kick", (
check(len(djf) == 3 and not any(r[3] for r in djf), "gMute groups by ROLE, not bloc — kick alone is family 1")
"the 3 DJF knobs keep their rich readout: drawn by US, natives suppressed") b4 = grid.CELL_TO_CC[("B", 4)]
check(all(r[3] for r in rows if r not in djf), assert lang.control_label(b4, "# crushbus") == "d4 # crushbus", (
"everything else is drawn by the firmware, from our name") "orbit first, then what the control does in this track")
# The fallback flag must hand the whole surface back to our own overlay. assert len(lang.control_label(b4, "# a_very_long_effect_name")) <= 16, (
rows = drv.label_table(m, {}, set(m.values()), lang, grid, overlay_only=True) "labels must fit the panel")
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"), # parse_context: the trailing stage note beats the Haskell
"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") def test_parse_context_stage_note_beats_the_haskell():
check(len(lang.home_lines("x", "y", "?REC 1:02:03")[2]) <= 16, """This line is real (piment_bresilien.tidal:87) and used to yield `# n "23")) --`."""
"a footer must be clipped to the 16-char panel like the other two rows") ctx = drv.parse_context(' $ midiOn "^56" ((# n "23")) -- Raise COMEON!\n')
assert ctx.get(56) == "Raise COMEON!", (
# ---- rec_line: pure (rec dict, now) -> optional "REC h:mm:ss" --------------- f"PLN's own words must win over the Haskell body: {ctx.get(56)!r}")
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") def test_parse_context_falls_back_to_the_quoted_sample_with_no_comment():
check(drv.rec_line({"k": "rec", "rec": False, "t": NOW, "el": 12}, NOW) is None, """With no stage note, the useful noun is the sample, not the combinator."""
"rec:false -> keep the drip even with a fresh timestamp") ctx = drv.parse_context(' $ midiOn "^28" (loopAt 1 . (# "break:18"))\n')
check(drv.rec_line({"k": "rec", "rec": True, "t": NOW, "el": 83}, NOW) == "REC 01:23", assert ctx.get(28) == "break:18", (
"83s elapsed -> mm:ss, zero-padded") f"no comment -> first quoted string, not `loopAt 1 . (#`: {ctx.get(28)!r}")
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) def test_parse_context_numeric_string_keeps_its_identifier():
== "?REC 00:05", ctx = drv.parse_context(' $ midiOn "^25" ((# n "6"))\n')
"conflict:true prefixes '?' — the two lenses disagree, don't trust blindly") assert ctx.get(25) == "n 6", (
check(drv.rec_line({"k": "rec", "rec": True, "t": NOW - 10, "el": 5}, NOW) is None, f"a numeric string keeps its identifier for meaning: {ctx.get(25)!r}")
"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") def test_parse_context_commented_out_line_labels_nothing():
check(drv.rec_line({"k": "rec", "rec": True, "el": 5}, NOW) is None, assert drv.parse_context('-- $ midiOn "^56" (id) -- nope\n') == {}, (
"a record with no timestamp at all can never be judged fresh -> keep the drip") "commented-out code labels nothing")
# ---- last_rec: tail-reads the newest k:"rec" line, ignores everything else --
import tempfile # --------------------------------------------------------------------------- #
with tempfile.TemporaryDirectory() as td: # label_table: every analogue control, and only those
p = pathlib.Path(td) / "gig-fake.jsonl" # --------------------------------------------------------------------------- #
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( p.write_text(
'{"k": "s", "t": 1.0}\n' '{"k": "s", "t": 1.0}\n'
'{"k": "rec", "t": 2.0, "rec": true, "el": 1}\n' '{"k": "rec", "t": 2.0, "rec": true, "el": 1}\n'
...@@ -200,16 +293,21 @@ with tempfile.TemporaryDirectory() as td: ...@@ -200,16 +293,21 @@ with tempfile.TemporaryDirectory() as td:
'{"k": "cc", "t": 3.0}\n' '{"k": "cc", "t": 3.0}\n'
'{"k": "rec", "t": 4.0, "rec": true, "el": 3}\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) got = drv.last_rec(p)
check(got is not None and got.get("t") == 4.0, 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}") 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, def test_last_rec_missing_file_degrades_to_none(tmp_path):
"a missing gig-log directory must degrade to None, never raise") assert drv.last_rec(tmp_path / "gone.jsonl") is None, (
"a missing file must degrade to None, never raise")
if FAILED:
print(f"{len(FAILED)} FAILED:")
for f in FAILED: def test_newest_gig_log_missing_directory_degrades_to_none(tmp_path):
print(" -", f) assert drv.newest_gig_log(tmp_path / "no-such-dir") is None, (
sys.exit(1) "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): ...@@ -156,11 +156,26 @@ def test_sound_hints_never_raise_on_an_orbit_with_no_quoted_name(tmp_path):
# the setlist # 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(): def test_the_real_setlist_loads_and_every_track_exists():
"""A setlist that silently drops a track silently drops a transition, so """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() setlist = oo.load_setlist()
assert len(setlist) == 13 assert len(setlist) == len(_raw_setlist_rows())
for p, label in setlist: for p, label in setlist:
assert p.exists() assert p.exists()
assert label assert label
...@@ -187,24 +202,35 @@ def test_setlist_comments_and_blank_lines_are_ignored(tmp_path, monkeypatch): ...@@ -187,24 +202,35 @@ def test_setlist_comments_and_blank_lines_are_ignored(tmp_path, monkeypatch):
# parser that only agrees with fixtures # parser that only agrees with fixtures
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# Hand-measured by PLN + me on 2026-07-28, before this parser existed. If the # Hand-measured by PLN + me on 2026-07-28, before this parser existed, and
# parser and the hand count ever disagree, ONE of them is wrong and the tool must # RE-measured on 2026-09-05 (by reading each file's column-0 `dN` declarations
# not be trusted until that is resolved. # 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 = { HAND_MEASURED = {
"bombe_dj": {1, 2, 3, 4, 5, 7, 8, 9}, # 2026-08-01, commit 2376e43 ("d10 is the riser") added a d10 safe-riser
"wap": {1, 2, 3, 4, 5, 7, 8, 9}, # idiom to 7 tracks that lacked one — bombe_dj, wap, you_my_sunshine,
"piment_bresilien": {1, 2, 3, 4, 5, 7, 8, 10}, # 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}, "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}, "gimme_acid": {1, 2, 3, 4, 5, 8, 9, 10, 11, 12},
"vague_de_crime": {1, 2, 3, 4, 5, 6, 7, 8, 10}, "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 # 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 # 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 # consolidation he made in do_it_right).
# column-0 `dN` declarations, not by copying the parser's output — a hand "you_my_sunshine": {1, 2, 3, 4, 5, 6, 8, 9, 10, 11},
# measurement that quotes the thing it checks is a tautology. "desire": {1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
"you_my_sunshine": {1, 2, 3, 4, 5, 6, 8, 9, 11},
"desire": {1, 2, 3, 4, 5, 6, 7, 8, 9},
"the_revolution_will_be_sampled": {1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12}, "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): ...@@ -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(): 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 """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.""" into the next track; the mechanism says d6 is the survivor.
assert oo.orphans(oo.resolve("vague_de_crime"), oo.resolve("bombe_dj")) == {6, 10}
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(): 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(): ...@@ -281,14 +311,11 @@ def test_check_tracks_reads_the_SAME_setlist_file():
def test_the_setlist_survives_the_shell_parse_too(): def test_the_setlist_survives_the_shell_parse_too():
"""The setlist annotates each path with codename + BPM after a '#'. The shell """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 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 re as _re
import subprocess rows = _raw_setlist_rows()
out = subprocess.run( assert len(rows) == len(oo.load_setlist())
["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
for r in rows: for r in rows:
assert not _re.search(r"[#\[]", r), f"comment leaked into {r!r}" assert not _re.search(r"[#\[]", r), f"comment leaked into {r!r}"
assert (oo.REPO / r).exists(), 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