Commit e8f7066a by PLN (Algolia)

fix(setlist): a soundcheck is not a track, and the suite can run again

Two bugs that were hiding each other, which is why both survived a month.

THE SUITE RAN ZERO TESTS. test_setlist.py was a plain script ending in a bare
module-level sys.exit(), and pytest raises that during COLLECTION — so
'pytest tools/tests/' aborted the whole directory with an INTERNALERROR and ran
nothing, while 273 tests passed when invoked file-by-file. Every test in this
repo was invisible to the obvious command. Guarded the exit under __main__ (the
script still works run directly) and added one assertion so pytest actually sees
the checks. Whole suite in one command now: 274 passed, 2 skipped, 0 failed.

AND THE THING IT WAS HIDING: test_setlist had been failing all along, on a check
whose name says exactly what is wrong — 'a deliberation note is not a track (the
quand_on_decolle phantom)'. OPAL 2026's backlog section opens with

    ## SOUNDCHECK
    -- Quand on decolle

and entries() takes every list item under the gig heading, so every one of the
tools reading this setlist believed the set had 16 tracks opening on Quand on
décolle. The recorded set has 15 and opens on Ceci n'est pas Une Bombe
(segments_v3/v4 track 1). The parser now yields 15, in the right order.

The fix is deliberately NOT the section whitelist entries() refuses to build —
that one tried to enumerate which of PLN's prose headings are part of the set and
died on '## Livecoding Techno DNB Nu-jazz'. This is the inverse and far narrower:
naming the one phase that is definitionally not a performance. Three patterns,
and skips print to stderr, because a dropped track and a skipped soundcheck must
never look alike.

Fixed the parser, never backlog.md — the backlog is PLN's own file and it was
right. A soundcheck IS in his set list; it just isn't in his set.

Verified by breaking it: neutered _NOT_THE_SET_RE, watched
test_backlog_setlist_checks_all_pass fail naming the quand_on_decolle phantom,
restored, 274 green.

    the first track was never the first track.
    sixteen tools agreed with each other
    and none of them agreed with the recording.
parent 21f83b50
...@@ -101,6 +101,21 @@ class SetlistError(RuntimeError): ...@@ -101,6 +101,21 @@ class SetlistError(RuntimeError):
# real track to a typo'd marker. # real track to a typo'd marker.
_NOTE_RE = re.compile(r"^\s*(?:Q|N\.?B|NOTE|TODO|FIXME|IDEA)\s*[:?]", re.I) _NOTE_RE = re.compile(r"^\s*(?:Q|N\.?B|NOTE|TODO|FIXME|IDEA)\s*[:?]", re.I)
# A SOUNDCHECK is not the set. This is NOT the section whitelist that `entries()`
# rightly refuses to build — that one tried to enumerate which of PLN's prose
# headings are *part of* the set, and died on "## Livecoding Techno DNB Nu-jazz".
# This is the inverse and much narrower: naming the one phase that is definitionally
# not a performance. It stays short on purpose, and skips are announced on stderr,
# because a dropped track and a skipped soundcheck must never look alike.
#
# Found 2026-09-05: OPAL 2026's section opens with "## SOUNDCHECK / -- Quand on
# decolle", so every tool reading this setlist believed the set had 16 tracks
# opening on Quand on décolle. The recorded set has 15 and opens on Ceci n'est pas
# Une Bombe (segments_v3/v4 track 1). test_setlist.py had been asserting exactly
# this since it was written, and nobody could see it fail — the file was a plain
# script that pytest could not collect.
_NOT_THE_SET_RE = re.compile(r"^(sound\s*check|line\s*check|balance)$", re.I)
# How many words of decoration a real entry may carry around its track name — # How many words of decoration a real entry may carry around its track name —
# "[lightT][120] Vague de CRIME" spends 2, "[129] Desire [TODO BASS ETC!]" spends 4. # "[lightT][120] Vague de CRIME" spends 2, "[129] Desire [TODO BASS ETC!]" spends 4.
_MAX_DECORATION = 4 _MAX_DECORATION = 4
...@@ -230,6 +245,11 @@ def entries(gig: str = GIG, backlog: pathlib.Path | None = None) -> list[Entry]: ...@@ -230,6 +245,11 @@ def entries(gig: str = GIG, backlog: pathlib.Path | None = None) -> list[Entry]:
if _NOTE_RE.match(raw): if _NOTE_RE.match(raw):
print(f"setlist: note, not a track — skipped: {raw}", file=sys.stderr) print(f"setlist: note, not a track — skipped: {raw}", file=sys.stderr)
continue continue
# Announced, never silent — see _NOT_THE_SET_RE.
if _NOT_THE_SET_RE.match(section):
print(f"setlist: {section} is not the set — skipped: {raw}",
file=sys.stderr)
continue
tags = tuple(t.strip() for t in BRACKET.findall(raw)) tags = tuple(t.strip() for t in BRACKET.findall(raw))
name = BRACKET.sub(" ", raw) name = BRACKET.sub(" ", raw)
bpm = next((int(BPM.match(t).group(1)) for t in tags if BPM.match(t)), None) bpm = next((int(BPM.match(t).group(1)) for t in tags if BPM.match(t)), None)
......
...@@ -158,4 +158,24 @@ check("the generated .txt round-trips through the parser", ...@@ -158,4 +158,24 @@ check("the generated .txt round-trips through the parser",
== [e.path for e in real]) == [e.path for e in real])
print(f"\n{len(FAILED)} failed" if FAILED else "\nall passed") print(f"\n{len(FAILED)} failed" if FAILED else "\nall passed")
sys.exit(1 if FAILED else 0)
def test_backlog_setlist_checks_all_pass():
"""Expose this plain-script suite to pytest.
The checks above run at import. Until 2026-09-05 this module ended in a bare
`sys.exit()`, which raises SystemExit during pytest's collection and aborted
the WHOLE directory with an INTERNALERROR — `pytest tools/tests/` ran ZERO
tests while 273 of them passed when invoked individually. So this file's own
real failure (the quand_on_decolle phantom) sat unseen, and so did everybody
else's.
A test file that cannot fail is not a passing test; a test file that stops the
suite is worse. The exit is now guarded under __main__ so the script still
works run directly, and this assertion is what makes pytest see it.
"""
assert not FAILED, "setlist checks failed:\n " + "\n ".join(FAILED)
if __name__ == "__main__":
sys.exit(1 if FAILED else 0)
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