aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-15 00:09:23 +0200
committeruser <user@clank>2026-08-15 00:09:23 +0200
commitf4e4e52ff9293b413bd477464ccf492c063e9442 (patch)
treea8714ea0bf1bd1d02fa23e96cae2f0c6f219dc19 /tests
parentcontrib: presets worth stealing, and a test that they do anything (diff)
downloadslosh-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.py152
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())