diff options
| author | user <user@clank> | 2026-08-15 00:09:23 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-15 00:09:23 +0200 |
| commit | f4e4e52ff9293b413bd477464ccf492c063e9442 (patch) | |
| tree | a8714ea0bf1bd1d02fa23e96cae2f0c6f219dc19 /tests | |
| parent | contrib: presets worth stealing, and a test that they do anything (diff) | |
| download | slosh-f4e4e52ff9293b413bd477464ccf492c063e9442.tar.gz slosh-f4e4e52ff9293b413bd477464ccf492c063e9442.tar.xz slosh-f4e4e52ff9293b413bd477464ccf492c063e9442.zip | |
contrib: the shadertoy presets, usable in sl0ppty
They only existed in a browser, which is a strange place for a terminal
multiplexer's shaders to live. Now each one is a file you can paste.
contrib/shaders/ holds all thirty-two, one per file, *generated* from the
presets in shadertoy.html by contrib/gen-shaders. The page stays the source:
it is where they are written, previewed and conformance-tested against the
compiler, and a hand-kept second copy would drift the day after it was
written. `gen-shaders --check` says whether the files are current, which is a
test, so the drift is a failing build rather than a stale example.
contrib/shader-tour cycles a session through them, in the shape theme-tour
already established — write the config the session watches and let the
watcher do the rest. It takes one shader, one group, or nothing for all of
them, and it resolves a short name (`torch`) as well as the whole slug
(`guides-torch`), because nobody remembers which group a shader is in. The
tour writes the theme alongside the shader: shaders resolve default-coloured
text through theme's default_fg/default_bg, so a preset judged without one is
being judged against a guess.
The new test is the one that matters. Every shipped file is loaded into a
real session and has to *change the cells of a pane* -- an example that
previews beautifully and does nothing when pasted is worse than no example,
and neither the toy's conformance test nor a config parse would notice. Three
of them are then checked for the specific thing they claim: scanlines
alternate row by row, indent guides land on every fourth column, a flat dim
is flat. One session, reloaded per preset, which is both quick and a fair
exercise of the path anyone will actually use.
Renamed the "the built-ins, as expressions" group to drop the qualifier from
its filenames; it was generating built-ins-as-expressions-scanlines.kdl,
which is a sentence rather than a name.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_shader_presets.py | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/tests/test_shader_presets.py b/tests/test_shader_presets.py new file mode 100644 index 0000000..f6a2b14 --- /dev/null +++ b/tests/test_shader_presets.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Every shader in contrib/shaders/ works in sl0ppty. + +The toy proves the *language* matches (test_shadertoy.py). This proves the +shipped files: that each one parses as config, is accepted by the shader +registry, and actually changes the cells of a real pane. An example that +previews beautifully and does nothing when you paste it is worse than no +example, and nothing else would catch that. + +One session, reloaded per preset, because that is both fast and a fair +exercise of the reload path -- which is how anyone will actually try these. +""" +import glob +import os +import shutil +import subprocess +import sys +import tempfile + +from harness import Session, check, report + +HERE = os.path.dirname(os.path.abspath(__file__)) +CONTRIB = os.path.join(HERE, "..", "contrib") +FILES = sorted(glob.glob(os.path.join(CONTRIB, "shaders", "*.kdl"))) + +# Prints a screenful of known-coloured text, then waits. +SH = ["/bin/sh", "-c", + 'printf "\\033]2;p\\007"; ' + 'i=0; while [ $i -lt 12 ]; do printf "%s\\n" ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789; ' + 'i=$((i+1)); done; read x'] + +# Every colour in the pane's content area, so "did anything change" is a +# question about the whole pane rather than one lucky cell. +def cells(snap, pane): + out = [] + for row in range(pane["content_h"]): + y = pane["content_y"] + row + for col in range(pane["content_w"]): + run = snap.style_at(pane["content_x"] + col, y) + out.append((run or {}).get("fg")) + return out + + +def test_every_preset_changes_the_pane(): + check("contrib ships shader presets", len(FILES) >= 20, str(len(FILES))) + + cfg = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False) + cfg.write('theme { default_fg "#ffffff" default_bg "#000000" }\n') + cfg.close() + + with Session(SH, cols=70, rows=18, config=cfg.name) as s: + s.until_text("ABCDEF") + base = cells(s.snapshot(), s.pane()) + + for path in FILES: + name = os.path.basename(path) + # The theme keeps "what colour is a cell" answerable; the preset is + # appended so it is exactly the file a user would paste. + with open(cfg.name, "w") as f: + f.write('theme { default_fg "#ffffff" default_bg "#000000" }\n') + f.write(open(path).read()) + reply = s.api("reload") + check(f"{name} loads", reply.get("ok") is True, str(reply)) + + s.settle(20) + now = cells(s.snapshot(), s.pane()) + check(f"{name} changes the pane", now != base, + f"identical to unshaded: {now[:3]}") + + os.unlink(cfg.name) + + +def test_a_few_of_them_do_the_specific_thing_they_claim(): + """Spot checks with a known answer, so "it changed something" cannot be + satisfied by an effect that changed the wrong thing.""" + cfg = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False) + + def load(basename): + with open(cfg.name, "w") as f: + f.write('theme { default_fg "#ffffff" default_bg "#000000" }\n') + f.write(open(os.path.join(CONTRIB, "shaders", basename)).read()) + + load("built-ins-scanlines.kdl") + with Session(SH, cols=70, rows=18, config=cfg.name) as s: + s.until_text("ABCDEF") + pane = s.pane() + rows = [cells(s.snapshot(), pane)[r * pane["content_w"]] + for r in range(4)] + check("scanlines alternate row by row", + rows[0] == rows[2] and rows[1] == rows[3] and rows[0] != rows[1], + str(rows)) + + load("guides-indent-guides.kdl") + with Session(SH, cols=70, rows=18, config=cfg.name) as s: + s.until_text("ABCDEF") + pane = s.pane() + row = cells(s.snapshot(), pane)[:9] + check("indent guides mark every fourth column", + row[0] == row[4] == row[8] and row[0] != row[1] and row[1] == row[2], + str(row)) + + load("pane-states-unfocused.kdl") + with Session(SH, cols=70, rows=18, config=cfg.name) as s: + s.until_text("ABCDEF") + pane = s.pane() + flat = cells(s.snapshot(), pane) + check("a flat dim is the same everywhere", + len({c for c in flat[:40] if c}) == 1, str(flat[:6])) + + os.unlink(cfg.name) + + +def test_the_tour_can_name_every_one_of_them(): + """contrib/shader-tour resolves a short name (`torch`) as well as the whole + slug. A tour that cannot name a shader it ships is a broken demo.""" + tour = os.path.join(CONTRIB, "shader-tour") + conf = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False) + conf.close() + env = dict(os.environ, SL0PPTY_CONFIG=conf.name) + + # Named explicitly rather than derived: a group name with a hyphen in it + # (built-ins) makes "the short half" ambiguous to compute and easy to get + # accidentally right, which is not a test. + for name in ("guides-torch", "torch", "matrix-rain"): + out = subprocess.run([tour, name], capture_output=True, text=True, env=env) + check(f"the tour applies {name!r}", + out.returncode == 0 and "shaders {" in open(conf.name).read(), + (out.stdout + out.stderr).strip()) + + out = subprocess.run([tour, "definitely-not-a-shader"], + capture_output=True, text=True, env=env) + check("and refuses one it does not have", out.returncode != 0, + (out.stdout + out.stderr).strip()) + os.unlink(conf.name) + + +def test_the_files_match_the_page_they_came_from(): + """They are generated from contrib/shadertoy.html. A hand-edit there and a + forgotten regeneration would leave the two disagreeing silently.""" + if not shutil.which("node"): + print("SKIP no node: cannot check the generated files are current") + return + out = subprocess.run([os.path.join(CONTRIB, "gen-shaders"), "--check"], + capture_output=True, text=True) + check("contrib/shaders is current with contrib/shadertoy.html", + out.returncode == 0, (out.stderr or out.stdout).strip()) + + +for name, fn in sorted(list(globals().items())): + if name.startswith("test_"): + fn() +sys.exit(report()) |
