#!/usr/bin/env python3 """Suite Song: turn a build or test log into a short square-wave WAV. Each recognised result line becomes one note. A pass (or an expected failure) climbs a pentatonic scale; an unexpected failure plays a held, sour semitone clash. The song ends on a calm A440 if everything passed, or a low buzz if not. Recognised lines (first match wins, one result per line): rc=N / rc=N (expected M) shell-style return codes, 0 = pass "verdict": "PASS" | "FAIL" JSON result lines ok N ... / not ok N ... TAP a bare PASS/PASSED or FAIL/FAILED/ERROR word Stdlib only. Exit codes: 0 ok, 1 nothing recognised, 2 bad usage or I/O error. """ import argparse, io, os, re, struct, sys, tempfile, wave RATE = 22050 SCALE = [0, 2, 4, 7, 9, 12, 14, 16, 19, 21, 24] # semitones above A3 MAX_NOTES = 400 # keeps any WAV under ~1 MB and ~60 s _RC = re.compile(r"\brc=(\d+)(?:\s*\(expected (\d+)\))?") _VERDICT = re.compile(r'"verdict"\s*:\s*"(PASS|FAIL)"', re.I) _TAP = re.compile(r"^\s*(not ok|ok)\b") _WORD = re.compile(r"\b(PASS(?:ED)?|FAIL(?:ED)?|ERROR)\b") def classify(line): """Return True (pass), False (unexpected failure) or None (not a result).""" m = _RC.search(line) if m: return int(m.group(1)) == int(m.group(2) or 0) m = _VERDICT.search(line) if m: return m.group(1).upper() == "PASS" m = _TAP.match(line) if m: return m.group(1) == "ok" m = _WORD.search(line) if m: return m.group(1).startswith("PASS") return None def parse(lines): return [r for r in (classify(l) for l in lines) if r is not None] def _square(freq, dur, vol=0.25): n, period = int(RATE * dur), RATE / freq return [vol * min(1.0, i / 200) * (1 - i / n) ** 0.5 * (1 if (i % period) < period / 2 else -1) for i in range(n)] def render(results): samples, step = [], 0 for ok in results[:MAX_NOTES]: base = 220 * 2 ** (SCALE[step % len(SCALE)] / 12) if ok: samples += _square(base, 0.11) step += 1 else: samples += _square(base, 0.12) + _square(base * 2 ** (1 / 12), 0.35, 0.3) samples += [0.0] * int(RATE * 0.02) samples += _square(440, 0.6) if all(results) else _square(110, 0.6, 0.3) return samples def write_wav(samples, path): data = b"".join(struct.pack(" MAX_NOTES else "" print(f"{a.output}: {len(samples) / RATE:.1f}s, {len(results)} results{note}, {fails} unexpected failure(s)") return 0 if __name__ == "__main__": sys.exit(main())