aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-17 14:36:59 +0200
committeruser <user@clank>2026-08-17 14:36:59 +0200
commitc0022776d0912042819e4ea330cf7491a8196b45 (patch)
tree3c5d547f273d8304e7cd6834fd5a2c730a495064 /tests
parentsl0ppty: ruff formats the Python, and sorts its imports (diff)
downloadslosh-c0022776d0912042819e4ea330cf7491a8196b45.tar.gz
slosh-c0022776d0912042819e4ea330cf7491a8196b45.tar.xz
slosh-c0022776d0912042819e4ea330cf7491a8196b45.zip
sl0ppty: ruff format the Python
The bumpy one, again. Every `.py` outside `vendor/` plus the three `contrib/` scripts that are Python with no extension: 60 files, 6,714 lines rewritten, no behaviour changed. Nothing here is worth reading -- the next commit adds it to `.git-blame-ignore-revs` so `git blame` walks past it. It is a bigger reformat than the C was, 60% of the lines against 17%, and for a reason worth writing down rather than tuning away: ruff and black break a call that does not fit onto one argument per line, where this code aligned the continuation under the opening paren. The test suite is thousands of `check("some prose", cond, detail)` calls, so nearly every one of them changed shape and the suite grew 3,308 lines. Line length was the one lever that moved it -- 120 columns would have been a 40% reformat and no growth at all -- and it was left at 88, because picking an unusual width to dodge a one-time diff is paying forever to avoid paying once. Import sorting came with it. Two files had `import os, pty, uuid, select, struct, sys, termios, time, fcntl, signal` on one line and now have ten. Verified after, not before, because a formatter that alters behaviour would be worse than none: - `make retest`, 1801 checks green -- and this reformats the suite that does the checking, including `harness.py`, which every one of those files imports. - `make test-live`, the tty tests the fast suite skips. They matter here more than usual: `live_input.py` and `live_m0.py` are two of the files whose comma-imports were split, and nothing in the fast suite would have noticed if that broke them. - All 60 files still compile, `make fmt` twice in a row changes nothing the second time, and `ruff check --select I` has nothing left to say.
Diffstat (limited to 'tests')
-rw-r--r--tests/harness.py12
-rw-r--r--tests/live_input.py20
-rw-r--r--tests/live_m0.py27
-rw-r--r--tests/live_reload.py139
-rw-r--r--tests/test_bell.py140
-rwxr-xr-xtests/test_check.py57
-rwxr-xr-xtests/test_chrome_presets.py39
-rwxr-xr-xtests/test_chrome_shaders.py356
-rw-r--r--tests/test_clipboard.py287
-rw-r--r--tests/test_config.py532
-rw-r--r--tests/test_corner.py137
-rw-r--r--tests/test_dead.py205
-rw-r--r--tests/test_direct_keys.py85
-rwxr-xr-xtests/test_docs.py89
-rw-r--r--tests/test_drag.py273
-rw-r--r--tests/test_dump_config.py99
-rw-r--r--tests/test_dump_layout.py60
-rw-r--r--tests/test_edit_config.py43
-rwxr-xr-xtests/test_equalize.py116
-rw-r--r--tests/test_finder.py190
-rw-r--r--tests/test_graphics.py270
-rw-r--r--tests/test_help.py175
-rw-r--r--tests/test_hints.py115
-rw-r--r--tests/test_hover.py73
-rwxr-xr-xtests/test_inband_shaders.py473
-rwxr-xr-xtests/test_include.py160
-rw-r--r--tests/test_layout.py118
-rw-r--r--tests/test_layout_files.py218
-rw-r--r--tests/test_minimize.py112
-rwxr-xr-xtests/test_move_pane.py370
-rw-r--r--tests/test_osc5577.py267
-rw-r--r--tests/test_palette.py112
-rw-r--r--tests/test_pane_title.py245
-rw-r--r--tests/test_portable_layouts.py270
-rw-r--r--tests/test_prefix.py42
-rw-r--r--tests/test_purpose_editor.py103
-rw-r--r--tests/test_reload.py86
-rw-r--r--tests/test_responsive.py364
-rwxr-xr-xtests/test_rotate.py163
-rw-r--r--tests/test_screen.py157
-rw-r--r--tests/test_scroll.py122
-rw-r--r--tests/test_scrollback.py82
-rw-r--r--tests/test_session.py80
-rw-r--r--tests/test_shader_expr.py85
-rw-r--r--tests/test_shader_plugin.py77
-rw-r--r--tests/test_shader_presets.py80
-rwxr-xr-xtests/test_shader_repl.py290
-rw-r--r--tests/test_shaders.py393
-rw-r--r--tests/test_shadertoy.py241
-rwxr-xr-xtests/test_skill.py143
-rw-r--r--tests/test_split_ux.py380
-rw-r--r--tests/test_states.py119
-rw-r--r--tests/test_tabs.py396
-rw-r--r--tests/test_themes.py21
-rw-r--r--tests/test_wide_marks.py56
-rw-r--r--tests/test_workspaces.py367
-rw-r--r--tests/test_zoom.py104
57 files changed, 6535 insertions, 3300 deletions
diff --git a/tests/harness.py b/tests/harness.py
index d842086..e75fe47 100644
--- a/tests/harness.py
+++ b/tests/harness.py
@@ -8,6 +8,7 @@ No pty, no sleeps beyond settling, no screen scraping: the compositor tells us
what it composed. This is the harness DESIGN.md asks for, and everything from
M1 on is tested through it.
"""
+
import json
import os
import subprocess
@@ -17,7 +18,8 @@ import time
# coverage build, say — without a second copy of the harness.
BIN = os.environ.get(
"SL0PPTY_BIN",
- os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "build", "sl0ppty"))
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "build", "sl0ppty"),
+)
class Snapshot:
@@ -55,11 +57,10 @@ class Snapshot:
"""One row of a pane's content area, chrome excluded."""
y = pane["content_y"] + row
x = pane["content_x"]
- return self.text[y][x:x + pane["content_w"]]
+ return self.text[y][x : x + pane["content_w"]]
def pane_text(self, pane):
- return "\n".join(
- self.pane_line(pane, r) for r in range(pane["content_h"]))
+ return "\n".join(self.pane_line(pane, r) for r in range(pane["content_h"]))
def hit_at(self, x, y):
"""The action registered at a cell, last painted winning."""
@@ -73,8 +74,7 @@ class Snapshot:
class Session:
- def __init__(self, argv, cols=80, rows=24, config=None, env=None,
- layout=None):
+ def __init__(self, argv, cols=80, rows=24, config=None, env=None, layout=None):
self.cols, self.rows = cols, rows
environ = dict(os.environ)
if config:
diff --git a/tests/live_input.py b/tests/live_input.py
index b8ae4cd..6a15625 100644
--- a/tests/live_input.py
+++ b/tests/live_input.py
@@ -6,9 +6,21 @@ tests/test_screen.py, where it runs deterministically through the headless
driver. What is left here is the part that only exists with a real tty: raw
mode, reading stdin, and the prefix key.
"""
-import os, pty, uuid, select, struct, sys, termios, time, fcntl, signal
-BIN = os.environ.get("SL0PPTY_BIN", os.path.join(os.path.dirname(__file__), "..", "build", "sl0ppty"))
+import fcntl
+import os
+import pty
+import select
+import signal
+import struct
+import sys
+import termios
+import time
+import uuid
+
+BIN = os.environ.get(
+ "SL0PPTY_BIN", os.path.join(os.path.dirname(__file__), "..", "build", "sl0ppty")
+)
INNER = ["--", "/bin/sh", "-c", "stty raw -echo; cat -v"]
fails = 0
@@ -52,7 +64,9 @@ def main():
os.write(fd, b"\x1b[1;5A")
out = drain(fd).decode("utf-8", "replace")
- check("a key typed at a real tty reaches the pane", "^[[1;5A" in out, repr(out[-60:]))
+ check(
+ "a key typed at a real tty reaches the pane", "^[[1;5A" in out, repr(out[-60:])
+ )
os.write(fd, b"\x01g") # prefix + a key that is bound to nothing
out = drain(fd)
diff --git a/tests/live_m0.py b/tests/live_m0.py
index 4248fd4..c26b431 100644
--- a/tests/live_m0.py
+++ b/tests/live_m0.py
@@ -4,9 +4,23 @@
The headless dump proves composition; this proves the parts headless cannot:
raw mode, the diff emitter, SIGWINCH, and the quit path.
"""
-import os, pty, uuid, re, select, signal, struct, subprocess, sys, termios, time, fcntl
-BIN = os.environ.get("SL0PPTY_BIN", os.path.join(os.path.dirname(__file__), "..", "build", "sl0ppty"))
+import fcntl
+import os
+import pty
+import re
+import select
+import signal
+import struct
+import subprocess
+import sys
+import termios
+import time
+import uuid
+
+BIN = os.environ.get(
+ "SL0PPTY_BIN", os.path.join(os.path.dirname(__file__), "..", "build", "sl0ppty")
+)
# A unique session per run. Without this the tests attach to whatever "main"
@@ -55,8 +69,9 @@ def main():
os.write(fd, b"echo sl0p-marker-42\n")
out = drain(fd)
- ok &= check("echoed output reaches the screen", b"sl0p-marker-42" in out,
- repr(out[-200:]))
+ ok &= check(
+ "echoed output reaches the screen", b"sl0p-marker-42" in out, repr(out[-200:])
+ )
ok &= check("alt screen entered", b"\x1b[?1049h" in out or True)
# resize: the pane must be told, and the app must see the new width
@@ -76,7 +91,9 @@ def main():
os.write(fd, b"\x01q") # C-a q
tail = drain(fd, idle=0.2, limit=1.5)
- ok &= check("C-a q leaves the alt screen", b"\x1b[?1049l" in tail, repr(tail[-120:]))
+ ok &= check(
+ "C-a q leaves the alt screen", b"\x1b[?1049l" in tail, repr(tail[-120:])
+ )
gone = False
for _ in range(20):
diff --git a/tests/live_reload.py b/tests/live_reload.py
index 6b09f62..2e57a7a 100644
--- a/tests/live_reload.py
+++ b/tests/live_reload.py
@@ -5,6 +5,7 @@ The interesting cases are not "does it notice a write" but the two that a naive
watch gets wrong -- an editor renaming a new file over the old one, and a file
caught mid-save that does not parse.
"""
+
import json
import os
import subprocess
@@ -14,8 +15,8 @@ import uuid
BIN = os.environ.get(
"SL0PPTY_BIN",
- os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "build",
- "sl0ppty"))
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "build", "sl0ppty"),
+)
fails = 0
@@ -29,8 +30,9 @@ def check(name, cond, detail=""):
def frame_colour(session):
"""The focused pane's frame colour, straight from the live session."""
- out = subprocess.run([BIN, "-s", session, "cmd", "snapshot"],
- capture_output=True, text=True).stdout
+ out = subprocess.run(
+ [BIN, "-s", session, "cmd", "snapshot"], capture_output=True, text=True
+ ).stdout
try:
runs = json.loads(out)["styles"]
except Exception:
@@ -53,12 +55,27 @@ def wait_for_colour(session, want, timeout=5.0):
def start(session, cfg_path, extra=()):
env = dict(os.environ, SL0PPTY_CONFIG=cfg_path)
- p = subprocess.Popen([BIN, "--server", "-s", session, *extra,
- "--cols", "60", "--rows", "12",
- "--", "/bin/sh", "-c", "stty raw -echo; cat"],
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
- env=env)
- for _ in range(200): # gate on the socket answering
+ p = subprocess.Popen(
+ [
+ BIN,
+ "--server",
+ "-s",
+ session,
+ *extra,
+ "--cols",
+ "60",
+ "--rows",
+ "12",
+ "--",
+ "/bin/sh",
+ "-c",
+ "stty raw -echo; cat",
+ ],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ env=env,
+ )
+ for _ in range(200): # gate on the socket answering
if frame_colour(session):
return p
time.sleep(0.02)
@@ -66,8 +83,7 @@ def start(session, cfg_path, extra=()):
def stop(session, proc):
- subprocess.run([BIN, "-s", session, "cmd", '{"cmd":"quit"}'],
- capture_output=True)
+ subprocess.run([BIN, "-s", session, "cmd", '{"cmd":"quit"}'], capture_output=True)
try:
proc.wait(timeout=3)
except Exception:
@@ -85,36 +101,48 @@ def test_watch():
write(cfg, "#00ff00")
p = start(name, cfg)
try:
- check("the session starts with the config it was given",
- frame_colour(name) == "#00ff00", str(frame_colour(name)))
+ check(
+ "the session starts with the config it was given",
+ frame_colour(name) == "#00ff00",
+ str(frame_colour(name)),
+ )
write(cfg, "#ff0000")
- check("writing the file in place reloads it",
- wait_for_colour(name, "#ff0000") == "#ff0000",
- str(frame_colour(name)))
+ check(
+ "writing the file in place reloads it",
+ wait_for_colour(name, "#ff0000") == "#ff0000",
+ str(frame_colour(name)),
+ )
# The case a watch on the *file* would miss: the inode is replaced, so
# a file watch fires once and then never again.
write(cfg + ".tmp", "#0000ff")
os.rename(cfg + ".tmp", cfg)
- check("and so does an editor renaming a new file over it",
- wait_for_colour(name, "#0000ff") == "#0000ff",
- str(frame_colour(name)))
+ check(
+ "and so does an editor renaming a new file over it",
+ wait_for_colour(name, "#0000ff") == "#0000ff",
+ str(frame_colour(name)),
+ )
with open(cfg, "w") as f:
f.write('theme {\n frame_focus "unterminated\n')
- time.sleep(0.4) # bounded: proving nothing happened needs a wait
- check("a file that does not parse keeps what works",
- frame_colour(name) == "#0000ff", str(frame_colour(name)))
- alive = subprocess.run([BIN, "-s", name, "cmd", "alive"],
- capture_output=True, text=True).stdout.strip()
- check("and the session is still alive to say so", alive == "true",
- repr(alive))
+ time.sleep(0.4) # bounded: proving nothing happened needs a wait
+ check(
+ "a file that does not parse keeps what works",
+ frame_colour(name) == "#0000ff",
+ str(frame_colour(name)),
+ )
+ alive = subprocess.run(
+ [BIN, "-s", name, "cmd", "alive"], capture_output=True, text=True
+ ).stdout.strip()
+ check("and the session is still alive to say so", alive == "true", repr(alive))
write(cfg, "#ffff00")
- check("and it recovers once the file is valid again",
- wait_for_colour(name, "#ffff00") == "#ffff00",
- str(frame_colour(name)))
+ check(
+ "and it recovers once the file is valid again",
+ wait_for_colour(name, "#ffff00") == "#ffff00",
+ str(frame_colour(name)),
+ )
finally:
stop(name, p)
for f in (cfg, cfg + ".tmp"):
@@ -125,8 +153,9 @@ def test_watch():
def toast_count(session, text):
"""How many times `text` appears on screen. Toasts stack, so a config that
reloaded twice announces itself twice."""
- out = subprocess.run([BIN, "-s", session, "cmd", "snapshot"],
- capture_output=True, text=True).stdout
+ out = subprocess.run(
+ [BIN, "-s", session, "cmd", "snapshot"], capture_output=True, text=True
+ ).stdout
try:
rows = json.loads(out)["text"]
except Exception:
@@ -146,13 +175,18 @@ def test_one_save_is_one_reload():
# Recreate it the way an editor that unlinks first would.
os.unlink(cfg)
write(cfg, "#ff0000")
- check("the recreated file is picked up",
- wait_for_colour(name, "#ff0000") == "#ff0000",
- str(frame_colour(name)))
- time.sleep(0.3) # bounded: let any second reload land if it is coming
+ check(
+ "the recreated file is picked up",
+ wait_for_colour(name, "#ff0000") == "#ff0000",
+ str(frame_colour(name)),
+ )
+ time.sleep(0.3) # bounded: let any second reload land if it is coming
n = toast_count(name, "config reloaded")
- check("and announced exactly once, not once per inotify event",
- n == 1, f"{n} toasts")
+ check(
+ "and announced exactly once, not once per inotify event",
+ n == 1,
+ f"{n} toasts",
+ )
finally:
stop(name, p)
if os.path.exists(cfg):
@@ -165,19 +199,28 @@ def test_no_reload():
write(cfg, "#00ff00")
p = start(name, cfg, extra=("--no-reload",))
try:
- check("--no-reload starts with the config all the same",
- frame_colour(name) == "#00ff00", str(frame_colour(name)))
+ check(
+ "--no-reload starts with the config all the same",
+ frame_colour(name) == "#00ff00",
+ str(frame_colour(name)),
+ )
write(cfg, "#ff0000")
- time.sleep(0.5) # bounded, for the same reason as above
- check("but does not pick up a change on its own",
- frame_colour(name) == "#00ff00", str(frame_colour(name)))
+ time.sleep(0.5) # bounded, for the same reason as above
+ check(
+ "but does not pick up a change on its own",
+ frame_colour(name) == "#00ff00",
+ str(frame_colour(name)),
+ )
# ...and asking explicitly still works, which is the point of the flag
# being about *watching* rather than about reloading.
- subprocess.run([BIN, "-s", name, "cmd", '{"cmd":"reload"}'],
- capture_output=True)
- check("while asking for a reload still works",
- wait_for_colour(name, "#ff0000") == "#ff0000",
- str(frame_colour(name)))
+ subprocess.run(
+ [BIN, "-s", name, "cmd", '{"cmd":"reload"}'], capture_output=True
+ )
+ check(
+ "while asking for a reload still works",
+ wait_for_colour(name, "#ff0000") == "#ff0000",
+ str(frame_colour(name)),
+ )
finally:
stop(name, p)
if os.path.exists(cfg):
diff --git a/tests/test_bell.py b/tests/test_bell.py
index 15f72d9..b152835 100644
--- a/tests/test_bell.py
+++ b/tests/test_bell.py
@@ -5,6 +5,7 @@ A bell exists for the pane you are *not* looking at, so most of what matters is
where it shows up when the pane is off-screen, and that looking at it is what
puts it out.
"""
+
import os
import sys
import tempfile
@@ -30,8 +31,10 @@ def lay(text):
TWO_PANES = 'layout {\n tab name="api" {\n pane\n pane\n }\n}\n'
-TWO_TABS = ('layout {\n tab name="api" {\n pane\n pane\n }\n'
- ' tab name="notes" {\n pane\n }\n}\n')
+TWO_TABS = (
+ 'layout {\n tab name="api" {\n pane\n pane\n }\n'
+ ' tab name="notes" {\n pane\n }\n}\n'
+)
def ring(s, pane_id, back_to):
@@ -45,7 +48,7 @@ def ring(s, pane_id, back_to):
def titlebar(snap, p, n=10):
- return snap.line(p["y"])[p["x"]:p["x"] + n]
+ return snap.line(p["y"])[p["x"] : p["x"] + n]
def test_a_bel_marks_the_pane_that_rang():
@@ -56,16 +59,23 @@ def test_a_bel_marks_the_pane_that_rang():
mine = [p for p in panes if p["focused"]][0]
other = [p for p in panes if not p["focused"]][0]
- check("no mark before anything rings",
- MARK not in titlebar(s.snapshot(), other), "")
+ check(
+ "no mark before anything rings",
+ MARK not in titlebar(s.snapshot(), other),
+ "",
+ )
ring(s, other["id"], mine["id"])
- check("the pane that rang is marked",
- MARK in titlebar(s.snapshot(), other),
- repr(titlebar(s.snapshot(), other)))
- check("and the pane that did not is untouched",
- MARK not in titlebar(s.snapshot(), mine),
- repr(titlebar(s.snapshot(), mine)))
+ check(
+ "the pane that rang is marked",
+ MARK in titlebar(s.snapshot(), other),
+ repr(titlebar(s.snapshot(), other)),
+ )
+ check(
+ "and the pane that did not is untouched",
+ MARK not in titlebar(s.snapshot(), mine),
+ repr(titlebar(s.snapshot(), mine)),
+ )
os.unlink(l)
@@ -82,16 +92,20 @@ def test_looking_at_it_is_what_puts_it_out():
s.api("focus", id=other["id"])
s.settle(30)
- check("focusing it clears the mark",
- MARK not in titlebar(s.snapshot(), other),
- repr(titlebar(s.snapshot(), other)))
+ check(
+ "focusing it clears the mark",
+ MARK not in titlebar(s.snapshot(), other),
+ repr(titlebar(s.snapshot(), other)),
+ )
# It must stay out; a bell is not a thing that comes back on its own.
s.api("focus", id=mine["id"])
s.settle(30)
- check("and it stays cleared once answered",
- MARK not in titlebar(s.snapshot(), other),
- repr(titlebar(s.snapshot(), other)))
+ check(
+ "and it stays cleared once answered",
+ MARK not in titlebar(s.snapshot(), other),
+ repr(titlebar(s.snapshot(), other)),
+ )
os.unlink(l)
@@ -104,8 +118,11 @@ def test_a_bell_in_another_tab_shows_on_the_strip():
here = [p for p in s.panes() if p["focused"]][0]
away = [p for p in s.panes() if p["tab"] == 2][0]
- check("the strip is unmarked to start",
- MARK not in s.snapshot().line(1), repr(s.snapshot().line(1)))
+ check(
+ "the strip is unmarked to start",
+ MARK not in s.snapshot().line(1),
+ repr(s.snapshot().line(1)),
+ )
s.api("select-tab", id=tabs[1]["id"])
s.settle(20)
@@ -118,15 +135,20 @@ def test_a_bell_in_another_tab_shows_on_the_strip():
s.settle(30)
strip = s.snapshot().line(1)
- check("the tab holding the rung pane is marked", MARK in strip,
- repr(strip))
- check("and the mark sits with that tab's own label",
- strip.index(MARK) > strip.index("notes"), repr(strip))
+ check("the tab holding the rung pane is marked", MARK in strip, repr(strip))
+ check(
+ "and the mark sits with that tab's own label",
+ strip.index(MARK) > strip.index("notes"),
+ repr(strip),
+ )
s.api("select-tab", id=tabs[1]["id"])
s.settle(30)
- check("visiting the tab clears it",
- MARK not in s.snapshot().line(1), repr(s.snapshot().line(1)))
+ check(
+ "visiting the tab clears it",
+ MARK not in s.snapshot().line(1),
+ repr(s.snapshot().line(1)),
+ )
os.unlink(l)
@@ -144,9 +166,11 @@ def test_the_strip_and_the_pane_clear_together():
s.api("focus", id=other["id"])
s.settle(30)
snap = s.snapshot()
- check("neither the titlebar nor the strip is left marked",
- MARK not in titlebar(snap, other) and MARK not in snap.line(1),
- repr(titlebar(snap, other)) + " / " + repr(snap.line(1)))
+ check(
+ "neither the titlebar nor the strip is left marked",
+ MARK not in titlebar(snap, other) and MARK not in snap.line(1),
+ repr(titlebar(snap, other)) + " / " + repr(snap.line(1)),
+ )
os.unlink(l)
@@ -160,9 +184,11 @@ def test_it_can_be_turned_off_and_the_mark_chosen():
other = [p for p in panes if not p["focused"]][0]
ring(s, other["id"], mine["id"])
snap = s.snapshot()
- check("bell_indicator false says nothing anywhere",
- MARK not in titlebar(snap, other) and MARK not in snap.line(1),
- repr(titlebar(snap, other)))
+ check(
+ "bell_indicator false says nothing anywhere",
+ MARK not in titlebar(snap, other) and MARK not in snap.line(1),
+ repr(titlebar(snap, other)),
+ )
os.unlink(off)
custom = cfg('bell_mark "!"\n')
@@ -172,18 +198,22 @@ def test_it_can_be_turned_off_and_the_mark_chosen():
mine = [p for p in panes if p["focused"]][0]
other = [p for p in panes if not p["focused"]][0]
ring(s, other["id"], mine["id"])
- check("a chosen mark is the one drawn",
- "!" in titlebar(s.snapshot(), other),
- repr(titlebar(s.snapshot(), other)))
+ check(
+ "a chosen mark is the one drawn",
+ "!" in titlebar(s.snapshot(), other),
+ repr(titlebar(s.snapshot(), other)),
+ )
os.unlink(custom)
os.unlink(l)
# ---- a pane you have put away ----------------------------------------------
-RINGER = ["/bin/sh", "-c",
- 'printf "\\033]2;ringer\\007"; sleep 1; printf "\\007"; '
- 'stty raw -echo; cat']
+RINGER = [
+ "/bin/sh",
+ "-c",
+ 'printf "\\033]2;ringer\\007"; sleep 1; printf "\\007"; stty raw -echo; cat',
+]
def bar_row(s):
@@ -204,34 +234,44 @@ def test_a_minimised_pane_rings_on_the_bar():
gone = s.panes()[0]["id"]
s.api("focus", id=gone)
s.settle(10)
- s.send(r"\x01m") # put it away before it rings
+ s.send(r"\x01m") # put it away before it rings
s.settle(20)
- check("it is on the bar and not on screen",
- [p["w"] for p in s.panes() if p["id"] == gone] == [0],
- str(s.panes()))
+ check(
+ "it is on the bar and not on screen",
+ [p["w"] for p in s.panes() if p["id"] == gone] == [0],
+ str(s.panes()),
+ )
row = s.snapshot().text[bar_row(s)]
check("with no mark yet", MARK not in row, repr(row))
- for _ in range(60): # gate on the mark rather than guess at it
+ for _ in range(60): # gate on the mark rather than guess at it
s.settle(30)
if MARK in s.snapshot().text[bar_row(s)]:
break
row = s.snapshot().text[bar_row(s)]
check("ringing while put away marks its entry", MARK in row, repr(row))
- check("and the tab it lives in", MARK in s.snapshot().line(1),
- repr(s.snapshot().line(1)))
+ check(
+ "and the tab it lives in",
+ MARK in s.snapshot().line(1),
+ repr(s.snapshot().line(1)),
+ )
- entry = [h for h in s.snapshot().hits
- if h["action"] == f"focus:{gone}" and h["h"] == 1]
+ entry = [
+ h
+ for h in s.snapshot().hits
+ if h["action"] == f"focus:{gone}" and h["h"] == 1
+ ]
check("the entry is still one target", len(entry) == 1, str(entry))
if not entry:
return
s.click(entry[0]["x"] + 1, entry[0]["y"])
s.settle(30)
- check("restoring it answers the bell everywhere",
- MARK not in s.snapshot().line(1)
- and [p["w"] for p in s.panes() if p["id"] == gone] != [0],
- repr(s.snapshot().line(1)))
+ check(
+ "restoring it answers the bell everywhere",
+ MARK not in s.snapshot().line(1)
+ and [p["w"] for p in s.panes() if p["id"] == gone] != [0],
+ repr(s.snapshot().line(1)),
+ )
if __name__ == "__main__":
diff --git a/tests/test_check.py b/tests/test_check.py
index 058024d..c531c67 100755
--- a/tests/test_check.py
+++ b/tests/test_check.py
@@ -10,6 +10,7 @@ first mistake makes you run it once per mistake.
Exit status is the contract: 0 clean, 1 with anything to say. That is what lets
it drop into an editor's compile step or a hook without glue.
"""
+
import os
import subprocess
import sys
@@ -26,8 +27,9 @@ def conf(text):
def run(*args, env=None):
- return subprocess.run([BIN, "--check"] + list(args), capture_output=True,
- text=True, env=env)
+ return subprocess.run(
+ [BIN, "--check"] + list(args), capture_output=True, text=True, env=env
+ )
def test_a_good_config_is_quiet_and_says_what_it_read():
@@ -42,27 +44,34 @@ def test_a_good_config_is_quiet_and_says_what_it_read():
def test_it_reports_every_problem_not_just_the_first():
"""The whole reason for it. A session shows one; this shows the file."""
- r = run(conf('keys {\n'
- ' prefix "ctrl+nosuchkey"\n'
- ' bind "nope" "zoom"\n'
- ' bind "z" "fly"\n'
- '}\n'
- 'shaders {\n'
- ' bloom amount=200\n'
- '}\n'
- 'theme { frame_focus "not a colour" }\n'))
+ r = run(
+ conf(
+ "keys {\n"
+ ' prefix "ctrl+nosuchkey"\n'
+ ' bind "nope" "zoom"\n'
+ ' bind "z" "fly"\n'
+ "}\n"
+ "shaders {\n"
+ " bloom amount=200\n"
+ "}\n"
+ 'theme { frame_focus "not a colour" }\n'
+ )
+ )
check("exit 1", r.returncode == 1, r.stdout)
- for want in ("bad prefix: ctrl+nosuchkey", "bad key: nope",
- "unknown action: fly",
- "unknown shader: bloom", "bad colour for frame_focus"):
+ for want in (
+ "bad prefix: ctrl+nosuchkey",
+ "bad key: nope",
+ "unknown action: fly",
+ "unknown shader: bloom",
+ "bad colour for frame_focus",
+ ):
check(f"it reports {want!r}", want in r.stderr, r.stderr)
check("and counts them", "5 problems" in r.stderr, r.stderr)
def test_every_problem_carries_a_file_and_a_line():
r = run(conf('gap 1\n\nkeys {\n bind "nope" "zoom"\n}\n'))
- check("the line number is the binding's",
- ":4: bad key: nope" in r.stderr, r.stderr)
+ check("the line number is the binding's", ":4: bad key: nope" in r.stderr, r.stderr)
check("and the file is named", ".kdl:4:" in r.stderr, r.stderr)
@@ -76,12 +85,11 @@ def test_a_problem_in_an_included_file_names_that_file():
f.write('include "theme.kdl"\n')
r = run(main)
check("exit 1", r.returncode == 1, r.stdout)
- check("the included file is the one named",
- "theme.kdl:1:" in r.stderr, r.stderr)
+ check("the included file is the one named", "theme.kdl:1:" in r.stderr, r.stderr)
def test_a_file_that_will_not_parse_says_nothing_applied():
- r = run(conf('keys {\n bind "z" "zoom"\n')) # never closed
+ r = run(conf('keys {\n bind "z" "zoom"\n')) # never closed
check("exit 1", r.returncode == 1, r.stdout)
check("it says the file did not load", "not loaded" in r.stderr, r.stderr)
@@ -89,12 +97,11 @@ def test_a_file_that_will_not_parse_says_nothing_applied():
def test_a_missing_file_is_a_problem_not_a_crash():
r = run("/nonexistent/sl0ppty.kdl")
check("exit 1", r.returncode == 1, r.stdout)
- check("and it says so", "not loaded" in r.stderr or "cannot" in r.stderr,
- r.stderr)
+ check("and it says so", "not loaded" in r.stderr or "cannot" in r.stderr, r.stderr)
def test_without_a_path_it_checks_the_config_a_session_would_read():
- good = conf('gap 3\n')
+ good = conf("gap 3\n")
env = dict(os.environ, SL0PPTY_CONFIG=good)
r = subprocess.run([BIN, "--check"], capture_output=True, text=True, env=env)
check("it read $SL0PPTY_CONFIG", good in r.stdout, r.stdout + r.stderr)
@@ -106,12 +113,10 @@ def test_the_dump_is_a_config_it_accepts():
file it writes has to be one the loader accepts. It was not: the dump wrote
chords in the cheatsheet's notation (`C-a`, `S-←`) and the parser only spoke
the config's."""
- dump = subprocess.run([BIN, "--dump-config"], capture_output=True,
- text=True).stdout
+ dump = subprocess.run([BIN, "--dump-config"], capture_output=True, text=True).stdout
path = conf(dump)
r = run(path)
- check("a dumped config lints clean", r.returncode == 0,
- r.stderr or r.stdout)
+ check("a dumped config lints clean", r.returncode == 0, r.stderr or r.stdout)
if __name__ == "__main__":
diff --git a/tests/test_chrome_presets.py b/tests/test_chrome_presets.py
index a06cceb..b5a2170 100755
--- a/tests/test_chrome_presets.py
+++ b/tests/test_chrome_presets.py
@@ -11,6 +11,7 @@ Two things are asserted of each, and the second matters as much as the first:
the frame moves, and the pane's own output does not. A chrome pass that leaked
into the contents would be a shader bug wearing a preset's clothes.
"""
+
import glob
import os
import sys
@@ -23,16 +24,18 @@ HERE = os.path.dirname(os.path.abspath(__file__))
FILES = sorted(glob.glob(os.path.join(HERE, "..", "contrib", "chrome", "*.kdl")))
# Prints a screenful of known-coloured text, then waits.
-SH = ["/bin/sh", "-c",
- 'printf "\\033]2;p\\007"; '
- 'printf "\\033[38;2;0;255;0mBLOCK\\033[0m\\n"; stty raw -echo; cat']
+SH = [
+ "/bin/sh",
+ "-c",
+ 'printf "\\033]2;p\\007"; '
+ 'printf "\\033[38;2;0;255;0mBLOCK\\033[0m\\n"; stty raw -echo; cat',
+]
# Announcements stack up from the bottom right and are drawn over everything,
# including a pane's bottom rule -- and this test reloads eight times. A toast
# that outlived its reload would be measured as the frame having changed, so
# they are told to expire immediately.
-THEME = ('theme { default_fg "#ffffff" default_bg "#000000" }\n'
- 'toast_ms 1\n')
+THEME = 'theme { default_fg "#ffffff" default_bg "#000000" }\ntoast_ms 1\n'
def frame_cells(snap, pane):
@@ -51,8 +54,11 @@ def frame_cells(snap, pane):
def content_cells(snap, pane):
- return [(snap.style_at(pane["content_x"] + c, pane["content_y"] + r) or {}).get("fg")
- for r in range(pane["content_h"]) for c in range(pane["content_w"])]
+ return [
+ (snap.style_at(pane["content_x"] + c, pane["content_y"] + r) or {}).get("fg")
+ for r in range(pane["content_h"])
+ for c in range(pane["content_w"])
+ ]
def test_every_chrome_preset_moves_the_frame():
@@ -89,8 +95,10 @@ def test_every_chrome_preset_moves_the_frame():
snap = s.snapshot()
pane = s.pane()
seen.update(
- i for i, (a, b) in enumerate(zip(base_frame, frame_cells(snap, pane)))
- if a != b)
+ i
+ for i, (a, b) in enumerate(zip(base_frame, frame_cells(snap, pane)))
+ if a != b
+ )
if content_cells(snap, pane) != base_content:
content_moved = True
# The frame answer can stop the clock; the contents answer is
@@ -100,7 +108,11 @@ def test_every_chrome_preset_moves_the_frame():
time.sleep(0.03)
check(f"{name} colours the frame", len(seen) > 0, "no cell changed")
- check(f"{name} leaves the contents alone", not content_moved, "content changed")
+ check(
+ f"{name} leaves the contents alone",
+ not content_moved,
+ "content changed",
+ )
# ...and taking it out again puts the frame back, so one preset cannot
# leave a mark on the next.
@@ -108,8 +120,11 @@ def test_every_chrome_preset_moves_the_frame():
f.write(THEME)
s.api("reload")
s.settle(40)
- check("removing the last one restores the frame",
- frame_cells(s.snapshot(), s.pane()) == base_frame, "still shaded")
+ check(
+ "removing the last one restores the frame",
+ frame_cells(s.snapshot(), s.pane()) == base_frame,
+ "still shaded",
+ )
os.unlink(cfg.name)
diff --git a/tests/test_chrome_shaders.py b/tests/test_chrome_shaders.py
index 2f46f2c..7bbcfb2 100755
--- a/tests/test_chrome_shaders.py
+++ b/tests/test_chrome_shaders.py
@@ -13,6 +13,7 @@ other's cells; the frame pass has to see the *whole frame's* coordinates or an
effect could not travel round one; and an animated pass has to keep the session
painting, or a pulse freezes the moment you stop typing.
"""
+
import os
import sys
import tempfile
@@ -22,11 +23,14 @@ from harness import Session, check, report
# A pane that paints a known, non-default colour into its own content.
GREEN = "#00ff00"
-FRAME = "#ff5fd7" # the default focused frame colour
+FRAME = "#ff5fd7" # the default focused frame colour
DIM_FRAME = "#45454a" # ...and the colour of a frame you are not in
-SH = ["/bin/sh", "-c",
- 'printf "\\033]2;p\\007"; printf "\\033[38;2;0;255;0mBLOCK\\033[0m\\n"; '
- 'stty raw -echo; cat']
+SH = [
+ "/bin/sh",
+ "-c",
+ 'printf "\\033]2;p\\007"; printf "\\033[38;2;0;255;0mBLOCK\\033[0m\\n"; '
+ "stty raw -echo; cat",
+]
THEME = 'theme { default_fg "#ffffff" default_bg "#000000" }\n'
@@ -55,15 +59,23 @@ def test_a_chrome_pass_colours_the_frame_and_nothing_else():
with Session(SH, cols=60, rows=12, config=conf) as s:
s.until_text("BLOCK")
p, snap = s.pane(), s.snapshot()
- check("the frame takes the chrome colour",
- frame_fg(snap, p) == "#ff0000", str(frame_fg(snap, p)))
- check("all four sides of it, not just the top",
- fg_at(snap, p["x"], p["y"] + 3) == "#ff0000" and
- fg_at(snap, p["x"] + p["w"] - 1, p["y"] + 3) == "#ff0000" and
- fg_at(snap, p["x"] + 5, p["y"] + p["h"] - 1) == "#ff0000",
- snap.screen())
- check("and the pane's own output is untouched",
- content_fg(snap, p) == GREEN, str(content_fg(snap, p)))
+ check(
+ "the frame takes the chrome colour",
+ frame_fg(snap, p) == "#ff0000",
+ str(frame_fg(snap, p)),
+ )
+ check(
+ "all four sides of it, not just the top",
+ fg_at(snap, p["x"], p["y"] + 3) == "#ff0000"
+ and fg_at(snap, p["x"] + p["w"] - 1, p["y"] + 3) == "#ff0000"
+ and fg_at(snap, p["x"] + 5, p["y"] + p["h"] - 1) == "#ff0000",
+ snap.screen(),
+ )
+ check(
+ "and the pane's own output is untouched",
+ content_fg(snap, p) == GREEN,
+ str(content_fg(snap, p)),
+ )
def test_a_content_pass_still_leaves_the_frame_alone():
@@ -73,47 +85,62 @@ def test_a_content_pass_still_leaves_the_frame_alone():
with Session(SH, cols=60, rows=12, config=conf) as s:
s.until_text("BLOCK")
p, snap = s.pane(), s.snapshot()
- check("the contents are tinted", content_fg(snap, p) == "#ff0000",
- str(content_fg(snap, p)))
- check("the frame is not", frame_fg(snap, p) == FRAME,
- str(frame_fg(snap, p)))
+ check(
+ "the contents are tinted",
+ content_fg(snap, p) == "#ff0000",
+ str(content_fg(snap, p)),
+ )
+ check("the frame is not", frame_fg(snap, p) == FRAME, str(frame_fg(snap, p)))
def test_the_two_passes_can_run_together():
- conf = cfg('shaders {\n'
- ' tint color="#0000ff" amount=255\n'
- ' tint color="#ff0000" amount=255 where="chrome"\n'
- '}\n')
+ conf = cfg(
+ "shaders {\n"
+ ' tint color="#0000ff" amount=255\n'
+ ' tint color="#ff0000" amount=255 where="chrome"\n'
+ "}\n"
+ )
with Session(SH, cols=60, rows=12, config=conf) as s:
s.until_text("BLOCK")
p, snap = s.pane(), s.snapshot()
- check("each pass owns its own cells",
- content_fg(snap, p) == "#0000ff" and frame_fg(snap, p) == "#ff0000",
- f"{content_fg(snap, p)} / {frame_fg(snap, p)}")
+ check(
+ "each pass owns its own cells",
+ content_fg(snap, p) == "#0000ff" and frame_fg(snap, p) == "#ff0000",
+ f"{content_fg(snap, p)} / {frame_fg(snap, p)}",
+ )
def test_positions_are_the_whole_frames():
"""`y == 0` is the frame's top row, not the top row of one of its sides:
an effect that travels round a border needs one coordinate space."""
- conf = cfg('shaders {\n'
- ' tint color="#00ff00" amount="(y == 0) * 255" where="chrome"\n'
- '}\n')
+ conf = cfg(
+ "shaders {\n"
+ ' tint color="#00ff00" amount="(y == 0) * 255" where="chrome"\n'
+ "}\n"
+ )
with Session(SH, cols=60, rows=12, config=conf) as s:
s.until_text("BLOCK")
p, snap = s.pane(), s.snapshot()
- check("the top row is the row the expression selected",
- frame_fg(snap, p) == "#00ff00", str(frame_fg(snap, p)))
- check("the sides and the bottom are not",
- fg_at(snap, p["x"], p["y"] + 3) == FRAME and
- fg_at(snap, p["x"] + 5, p["y"] + p["h"] - 1) == FRAME,
- snap.screen())
+ check(
+ "the top row is the row the expression selected",
+ frame_fg(snap, p) == "#00ff00",
+ str(frame_fg(snap, p)),
+ )
+ check(
+ "the sides and the bottom are not",
+ fg_at(snap, p["x"], p["y"] + 3) == FRAME
+ and fg_at(snap, p["x"] + 5, p["y"] + p["h"] - 1) == FRAME,
+ snap.screen(),
+ )
def test_a_state_can_carry_a_chrome_chain():
"""The point of the feature: a border that says what a pane is doing."""
- conf = cfg('states {\n'
- ' unfocused { tint color="#ff0000" amount=255 where="chrome" }\n'
- '}\n')
+ conf = cfg(
+ "states {\n"
+ ' unfocused { tint color="#ff0000" amount=255 where="chrome" }\n'
+ "}\n"
+ )
with Session(SH, cols=90, rows=14, config=conf) as s:
s.until_text("BLOCK")
s.key("\\\\")
@@ -122,18 +149,25 @@ def test_a_state_can_carry_a_chrome_chain():
panes = s.panes()
focused = [p for p in panes if p["focused"]][0]
other = [p for p in panes if not p["focused"]][0]
- check("the pane you are not in wears it",
- frame_fg(snap, other) == "#ff0000", str(frame_fg(snap, other)))
- check("the one you are in does not",
- frame_fg(snap, focused) != "#ff0000", str(frame_fg(snap, focused)))
+ check(
+ "the pane you are not in wears it",
+ frame_fg(snap, other) == "#ff0000",
+ str(frame_fg(snap, other)),
+ )
+ check(
+ "the one you are in does not",
+ frame_fg(snap, focused) != "#ff0000",
+ str(frame_fg(snap, focused)),
+ )
s.key("o") # focus the next pane: the states swap
s.settle()
snap = s.snapshot()
- check("and it follows focus rather than sticking to a pane",
- frame_fg(snap, focused) == "#ff0000" and
- frame_fg(snap, other) != "#ff0000",
- f"{frame_fg(snap, focused)} / {frame_fg(snap, other)}")
+ check(
+ "and it follows focus rather than sticking to a pane",
+ frame_fg(snap, focused) == "#ff0000" and frame_fg(snap, other) != "#ff0000",
+ f"{frame_fg(snap, focused)} / {frame_fg(snap, other)}",
+ )
def test_a_state_block_still_replaces_the_whole_state():
@@ -141,17 +175,22 @@ def test_a_state_block_still_replaces_the_whole_state():
`unfocused` is the one where that is worth knowing: naming it takes the
dim_unfocused knob out of play, chrome chain or not. Documented in
config.kdl, and checked here so the documentation cannot drift from it."""
- conf = cfg('states {\n'
- ' unfocused { tint color="#ff0000" amount=255 where="chrome" }\n'
- '}\n')
+ conf = cfg(
+ "states {\n"
+ ' unfocused { tint color="#ff0000" amount=255 where="chrome" }\n'
+ "}\n"
+ )
with Session(SH, cols=90, rows=14, config=conf) as s:
s.until_text("BLOCK")
s.key("\\\\")
s.settle()
snap = s.snapshot()
other = [p for p in s.panes() if not p["focused"]][0]
- check("the unfocused pane's contents are left alone",
- content_fg(snap, other) == GREEN, str(content_fg(snap, other)))
+ check(
+ "the unfocused pane's contents are left alone",
+ content_fg(snap, other) == GREEN,
+ str(content_fg(snap, other)),
+ )
def test_a_reload_puts_a_chrome_chain_on_and_takes_it_off_again():
@@ -170,21 +209,27 @@ def test_a_reload_puts_a_chrome_chain_on_and_takes_it_off_again():
with Session(SH, cols=60, rows=12, config=path.name) as s:
s.until_text("BLOCK")
- check("the frame starts in its own colour",
- frame_fg(s.snapshot(), s.pane()) == FRAME,
- str(frame_fg(s.snapshot(), s.pane())))
+ check(
+ "the frame starts in its own colour",
+ frame_fg(s.snapshot(), s.pane()) == FRAME,
+ str(frame_fg(s.snapshot(), s.pane())),
+ )
- reload_with(s, 'shaders {\n'
- ' tint color="#ff0000" amount=255 where="chrome"\n'
- '}\n')
- check("saving a chrome pass colours the frame",
- frame_fg(s.snapshot(), s.pane()) == "#ff0000",
- str(frame_fg(s.snapshot(), s.pane())))
+ reload_with(
+ s, 'shaders {\n tint color="#ff0000" amount=255 where="chrome"\n}\n'
+ )
+ check(
+ "saving a chrome pass colours the frame",
+ frame_fg(s.snapshot(), s.pane()) == "#ff0000",
+ str(frame_fg(s.snapshot(), s.pane())),
+ )
reload_with(s, "")
- check("and taking it out of the file takes it off the frame",
- frame_fg(s.snapshot(), s.pane()) == FRAME,
- str(frame_fg(s.snapshot(), s.pane())))
+ check(
+ "and taking it out of the file takes it off the frame",
+ frame_fg(s.snapshot(), s.pane()) == FRAME,
+ str(frame_fg(s.snapshot(), s.pane())),
+ )
os.unlink(path.name)
@@ -200,42 +245,55 @@ def test_a_pass_can_be_told_to_leave_the_background_alone():
terminal's own default — so a border tint painted a dark rectangle behind
the glyphs and left it there. `channel="fg"` is the fix, and the background
has to come back *unset* rather than as our idea of what default means."""
- both = cfg('shaders {\n'
- ' tint color="#ff0000" amount=200 where="chrome"\n'
- '}\n')
- fg_only = cfg('shaders {\n'
- ' tint color="#ff0000" amount=200 where="chrome" channel="fg"\n'
- '}\n')
+ both = cfg('shaders {\n tint color="#ff0000" amount=200 where="chrome"\n}\n')
+ fg_only = cfg(
+ "shaders {\n"
+ ' tint color="#ff0000" amount=200 where="chrome" channel="fg"\n'
+ "}\n"
+ )
with Session(SH, cols=60, rows=12, config=both) as s:
s.until_text("BLOCK")
p, snap = s.pane(), s.snapshot()
- check("without a channel the background is painted too",
- bg_at(snap, p["x"] + 5, p["y"]) is not None,
- str(bg_at(snap, p["x"] + 5, p["y"])))
+ check(
+ "without a channel the background is painted too",
+ bg_at(snap, p["x"] + 5, p["y"]) is not None,
+ str(bg_at(snap, p["x"] + 5, p["y"])),
+ )
with Session(SH, cols=60, rows=12, config=fg_only) as s:
s.until_text("BLOCK")
p, snap = s.pane(), s.snapshot()
- check("channel fg still recolours the border glyphs",
- frame_fg(snap, p) not in (FRAME, None), str(frame_fg(snap, p)))
- check("and the frame keeps the terminal's own background",
- bg_at(snap, p["x"] + 5, p["y"]) is None,
- str(bg_at(snap, p["x"] + 5, p["y"])))
- check("the contents are still untouched",
- content_fg(snap, p) == GREEN, str(content_fg(snap, p)))
+ check(
+ "channel fg still recolours the border glyphs",
+ frame_fg(snap, p) not in (FRAME, None),
+ str(frame_fg(snap, p)),
+ )
+ check(
+ "and the frame keeps the terminal's own background",
+ bg_at(snap, p["x"] + 5, p["y"]) is None,
+ str(bg_at(snap, p["x"] + 5, p["y"])),
+ )
+ check(
+ "the contents are still untouched",
+ content_fg(snap, p) == GREEN,
+ str(content_fg(snap, p)),
+ )
def test_an_unknown_channel_is_refused_not_guessed():
- conf = cfg('shaders {\n'
- ' tint color="#ff0000" amount=255 where="chrome" channel="foreground"\n'
- '}\n')
+ conf = cfg(
+ "shaders {\n"
+ ' tint color="#ff0000" amount=255 where="chrome" channel="foreground"\n'
+ "}\n"
+ )
with Session(SH, cols=60, rows=12, config=conf) as s:
s.until_text("BLOCK")
p, snap = s.pane(), s.snapshot()
- check("the session runs and nothing was shaded",
- s.alive() and frame_fg(snap, p) == FRAME and
- content_fg(snap, p) == GREEN,
- f"{frame_fg(snap, p)} / {content_fg(snap, p)}")
+ check(
+ "the session runs and nothing was shaded",
+ s.alive() and frame_fg(snap, p) == FRAME and content_fg(snap, p) == GREEN,
+ f"{frame_fg(snap, p)} / {content_fg(snap, p)}",
+ )
def ring(s, pane_id, back_to):
@@ -254,10 +312,12 @@ def test_a_bell_can_flash_the_border():
expressible: `t` says what time it is, not how long ago the thing rang."""
# One node per line: a newline ends a KDL node, so a pass split over two
# lines is two broken nodes rather than one pass.
- conf = cfg('anim_ms 20\n'
- 'states {\n'
- ' bell { tint where="chrome" color="#ff0000" amount="(since < 250) * 255" }\n'
- '}\n')
+ conf = cfg(
+ "anim_ms 20\n"
+ "states {\n"
+ ' bell { tint where="chrome" color="#ff0000" amount="(since < 250) * 255" }\n'
+ "}\n"
+ )
with Session(SH, cols=90, rows=14, config=conf) as s:
s.until_text("BLOCK")
s.key("\\\\")
@@ -265,34 +325,45 @@ def test_a_bell_can_flash_the_border():
mine = [p for p in s.panes() if p["focused"]][0]
other = [p for p in s.panes() if not p["focused"]][0]
- check("nothing flashes before anything rings",
- frame_fg(s.snapshot(), other) == DIM_FRAME,
- str(frame_fg(s.snapshot(), other)))
+ check(
+ "nothing flashes before anything rings",
+ frame_fg(s.snapshot(), other) == DIM_FRAME,
+ str(frame_fg(s.snapshot(), other)),
+ )
ring(s, other["id"], mine["id"])
- check("the pane that rang flashes",
- frame_fg(s.snapshot(), other) == "#ff0000",
- str(frame_fg(s.snapshot(), other)))
- check("and asks for the frames to animate it",
- s.deadline() == 20, str(s.deadline()))
- check("the pane you are in does not",
- frame_fg(s.snapshot(), mine) == FRAME,
- str(frame_fg(s.snapshot(), mine)))
+ check(
+ "the pane that rang flashes",
+ frame_fg(s.snapshot(), other) == "#ff0000",
+ str(frame_fg(s.snapshot(), other)),
+ )
+ check(
+ "and asks for the frames to animate it",
+ s.deadline() == 20,
+ str(s.deadline()),
+ )
+ check(
+ "the pane you are in does not",
+ frame_fg(s.snapshot(), mine) == FRAME,
+ str(frame_fg(s.snapshot(), mine)),
+ )
# Real elapsed time: `since` is a clock, and the flash is defined as
# ending on its own.
time.sleep(0.3)
- check("the flash ends by itself, without the bell being answered",
- frame_fg(s.snapshot(), other) == DIM_FRAME,
- str(frame_fg(s.snapshot(), other)))
+ check(
+ "the flash ends by itself, without the bell being answered",
+ frame_fg(s.snapshot(), other) == DIM_FRAME,
+ str(frame_fg(s.snapshot(), other)),
+ )
def test_the_bell_state_ends_when_you_look_at_the_pane():
"""Answering a bell is looking at it, so the colour has to go then too —
a border still shouting about a bell you have read is worse than none."""
- conf = cfg('states {\n'
- ' bell { tint where="chrome" color="#ff0000" amount=255 }\n'
- '}\n')
+ conf = cfg(
+ 'states {\n bell { tint where="chrome" color="#ff0000" amount=255 }\n}\n'
+ )
with Session(SH, cols=90, rows=14, config=conf) as s:
s.until_text("BLOCK")
s.key("\\\\")
@@ -301,38 +372,48 @@ def test_the_bell_state_ends_when_you_look_at_the_pane():
other = [p for p in s.panes() if not p["focused"]][0]
ring(s, other["id"], mine["id"])
- check("it holds while nobody has looked",
- frame_fg(s.snapshot(), other) == "#ff0000",
- str(frame_fg(s.snapshot(), other)))
+ check(
+ "it holds while nobody has looked",
+ frame_fg(s.snapshot(), other) == "#ff0000",
+ str(frame_fg(s.snapshot(), other)),
+ )
s.api("focus", id=other["id"])
s.settle()
- check("and stops the moment you do",
- frame_fg(s.snapshot(), other) == FRAME,
- str(frame_fg(s.snapshot(), other)))
+ check(
+ "and stops the moment you do",
+ frame_fg(s.snapshot(), other) == FRAME,
+ str(frame_fg(s.snapshot(), other)),
+ )
def test_a_rung_pane_is_rung_rather_than_merely_unfocused():
"""States do not stack, and of the two things true of a pane that rang
while you were elsewhere, the ringing is the one you needed telling."""
- conf = cfg('states {\n'
- ' unfocused { dim where="chrome" amount=255 }\n'
- ' bell { tint where="chrome" color="#ff0000" amount=255 }\n'
- '}\n')
+ conf = cfg(
+ "states {\n"
+ ' unfocused { dim where="chrome" amount=255 }\n'
+ ' bell { tint where="chrome" color="#ff0000" amount=255 }\n'
+ "}\n"
+ )
with Session(SH, cols=90, rows=14, config=conf) as s:
s.until_text("BLOCK")
s.key("\\\\")
s.settle()
mine = [p for p in s.panes() if p["focused"]][0]
other = [p for p in s.panes() if not p["focused"]][0]
- check("an unfocused pane wears the unfocused chain",
- frame_fg(s.snapshot(), other) == "#000000",
- str(frame_fg(s.snapshot(), other)))
+ check(
+ "an unfocused pane wears the unfocused chain",
+ frame_fg(s.snapshot(), other) == "#000000",
+ str(frame_fg(s.snapshot(), other)),
+ )
ring(s, other["id"], mine["id"])
- check("and the bell chain replaces it once it rings",
- frame_fg(s.snapshot(), other) == "#ff0000",
- str(frame_fg(s.snapshot(), other)))
+ check(
+ "and the bell chain replaces it once it rings",
+ frame_fg(s.snapshot(), other) == "#ff0000",
+ str(frame_fg(s.snapshot(), other)),
+ )
def test_a_collapsed_pane_is_chrome_all_the_way_through():
@@ -349,9 +430,11 @@ def test_a_collapsed_pane_is_chrome_all_the_way_through():
rows = [p for p in s.panes() if p["h"] == 1]
check("the tab flattened", len(rows) >= 1, str(s.panes()))
snap = s.snapshot()
- check("a collapsed row takes the chrome colour",
- fg_at(snap, rows[0]["x"] + 2, rows[0]["y"]) == "#ff0000",
- snap.screen())
+ check(
+ "a collapsed row takes the chrome colour",
+ fg_at(snap, rows[0]["x"] + 2, rows[0]["y"]) == "#ff0000",
+ snap.screen(),
+ )
def test_an_unknown_where_is_refused_not_guessed():
@@ -362,21 +445,28 @@ def test_an_unknown_where_is_refused_not_guessed():
s.until_text("BLOCK")
p, snap = s.pane(), s.snapshot()
check("the session still runs", s.alive())
- check("and neither the frame nor the contents were shaded",
- frame_fg(snap, p) == FRAME and content_fg(snap, p) == GREEN,
- f"{frame_fg(snap, p)} / {content_fg(snap, p)}")
+ check(
+ "and neither the frame nor the contents were shaded",
+ frame_fg(snap, p) == FRAME and content_fg(snap, p) == GREEN,
+ f"{frame_fg(snap, p)} / {content_fg(snap, p)}",
+ )
def test_an_animated_chrome_pass_asks_for_a_frame_clock():
- conf = cfg('anim_ms 40\n'
- 'shaders {\n'
- ' tint color="#00ff00" amount="((t / 50) % 2) * 255" where="chrome"\n'
- '}\n')
+ conf = cfg(
+ "anim_ms 40\n"
+ "shaders {\n"
+ ' tint color="#00ff00" amount="((t / 50) % 2) * 255" where="chrome"\n'
+ "}\n"
+ )
with Session(SH, cols=60, rows=12, config=conf) as s:
s.until_text("BLOCK")
s.snapshot()
- check("the session wants repainting on the clock it was given",
- s.deadline() == 40, str(s.deadline()))
+ check(
+ "the session wants repainting on the clock it was given",
+ s.deadline() == 40,
+ str(s.deadline()),
+ )
# Real elapsed time: the thing under test is a clock, and the only
# honest way to ask whether a colour moves is to wait and look.
diff --git a/tests/test_clipboard.py b/tests/test_clipboard.py
index b655cc7..9d1fab2 100644
--- a/tests/test_clipboard.py
+++ b/tests/test_clipboard.py
@@ -5,6 +5,7 @@ The clipboard lives on the *client's* machine, so a copy travels to it as
OSC 52; the session keeps its own copy as the primary selection, which is what
middle click pastes.
"""
+
import sys
import time
import unicodedata
@@ -14,13 +15,12 @@ from harness import Session, check, report
# Not an interactive shell: readline treats a typed ESC as a meta prefix and
# eats it, so `printf "<ESC>]52;..."` never reaches printf. A raw read loop
# passes bytes through untouched. (The same trap as the OSC 5577 tests.)
-SH = ["/bin/sh", "-c",
- 'stty raw -echo; while IFS= read -r l; do eval "$l"; done']
+SH = ["/bin/sh", "-c", 'stty raw -echo; while IFS= read -r l; do eval "$l"; done']
def echoing(preamble):
"""A pane that prints something, then echoes whatever is sent to it."""
- return ["/bin/sh", "-c", f'{preamble}; stty raw -echo; cat']
+ return ["/bin/sh", "-c", f"{preamble}; stty raw -echo; cat"]
def emits(*seqs):
@@ -49,16 +49,19 @@ def dbl_click(s, x, y, times=2):
s.send(rf"\e[<0;{x + 1};{y + 1}m")
-LINE = ('src/app.c:1234:5: error in foo_bar(x); see https://a.b/c?d=1 '
- 'or "quoted" \u65e5\u672c\u8a9e\u3067\u3059 end')
+LINE = (
+ "src/app.c:1234:5: error in foo_bar(x); see https://a.b/c?d=1 "
+ 'or "quoted" \u65e5\u672c\u8a9e\u3067\u3059 end'
+)
def column_of(line, index):
"""The screen column a string index lands on. A wide glyph takes two cells, so
every position after one is shifted -- the arithmetic the terminal is doing, and
the reason a test that skips it clicks somewhere else entirely."""
- return sum(2 if unicodedata.east_asian_width(c) in ("W", "F") else 1
- for c in line[:index])
+ return sum(
+ 2 if unicodedata.east_asian_width(c) in ("W", "F") else 1 for c in line[:index]
+ )
def worded(s, needle, offset=0, times=2, line=LINE):
@@ -74,6 +77,7 @@ def a_line(text=LINE):
"""A pane showing one line of text, from a file: a printf of this would hand
the shell backticks and parentheses to interpret."""
import tempfile
+
f = tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False)
f.write(text + "\n")
f.close()
@@ -91,11 +95,17 @@ def test_select_to_copy():
select(s, cx, row, cx + 12, row)
s.settle(80)
clip = s.api("clipboard")["text"]
- check("releasing the drag copies what was under it",
- clip == "hello selecti", repr(clip))
+ check(
+ "releasing the drag copies what was under it",
+ clip == "hello selecti",
+ repr(clip),
+ )
- check("and says so", "copied 13 chars" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-120:]))
+ check(
+ "and says so",
+ "copied 13 chars" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-120:]),
+ )
def test_selection_is_visible():
@@ -113,8 +123,11 @@ def test_selection_is_visible():
runs = [r for r in snap.styles if "inverse" in r["attrs"]]
check("the selection is visible while dragging", runs != [], str(snap.styles))
if runs:
- check("and covers what was dragged over",
- runs[0]["y"] == row and runs[0]["w"] >= 8, str(runs[0]))
+ check(
+ "and covers what was dragged over",
+ runs[0]["y"] == row and runs[0]["w"] >= 8,
+ str(runs[0]),
+ )
s.send(rf"\e[<0;{cx + 9};{row + 1}m")
@@ -131,13 +144,22 @@ def test_double_click_selects_a_word():
with Session(a_line(), cols=110, rows=12) as s:
s.until_text("error")
for label, needle, offset, want in (
- ("an error location comes whole", "app.c", 2, "src/app.c:1234:5:"),
- ("an identifier stops at the bracket", "foo_bar", 3, "foo_bar"),
- ("a URL is one word", "https", 4, "https://a.b/c?d=1"),
- ("quotes separate, so the word inside them is the word",
- "quoted", 2, "quoted"),
- ("a run of wide glyphs is one word", "\u65e5\u672c\u8a9e", 1,
- "\u65e5\u672c\u8a9e\u3067\u3059")):
+ ("an error location comes whole", "app.c", 2, "src/app.c:1234:5:"),
+ ("an identifier stops at the bracket", "foo_bar", 3, "foo_bar"),
+ ("a URL is one word", "https", 4, "https://a.b/c?d=1"),
+ (
+ "quotes separate, so the word inside them is the word",
+ "quoted",
+ 2,
+ "quoted",
+ ),
+ (
+ "a run of wide glyphs is one word",
+ "\u65e5\u672c\u8a9e",
+ 1,
+ "\u65e5\u672c\u8a9e\u3067\u3059",
+ ),
+ ):
got = worded(s, needle, offset)
check(label, got == want, repr(got))
@@ -153,10 +175,16 @@ def test_the_highlight_is_the_copy():
check("exactly one highlighted run", len(runs) == 1, str(snap.styles))
if runs:
p = s.pane()
- check("on the row that was clicked", runs[0]["y"] == p["content_y"],
- str(runs[0]))
- check("as wide as the word that was copied", runs[0]["w"] == len(clip),
- "%s vs %r" % (runs[0], clip))
+ check(
+ "on the row that was clicked",
+ runs[0]["y"] == p["content_y"],
+ str(runs[0]),
+ )
+ check(
+ "as wide as the word that was copied",
+ runs[0]["w"] == len(clip),
+ "%s vs %r" % (runs[0], clip),
+ )
def test_a_click_that_finds_no_word_leaves_the_clipboard_alone():
@@ -169,12 +197,16 @@ def test_a_click_that_finds_no_word_leaves_the_clipboard_alone():
check("something is on the clipboard to lose", first == "foo_bar", repr(first))
on_separator = worded(s, "(x)")
- check("double-clicking a separator copies nothing new",
- on_separator == first, repr(on_separator))
+ check(
+ "double-clicking a separator copies nothing new",
+ on_separator == first,
+ repr(on_separator),
+ )
on_blank = worded(s, " end")
- check("nor does double-clicking a blank cell", on_blank == first,
- repr(on_blank))
+ check(
+ "nor does double-clicking a blank cell", on_blank == first, repr(on_blank)
+ )
single = worded(s, "end", 1, times=1)
check("nor does a single click", single == first, repr(single))
@@ -195,12 +227,18 @@ def test_two_clicks_on_different_words_are_not_a_double_click():
dbl_click(s, x1, row, 1)
dbl_click(s, x2, row, 1)
s.settle(80)
- check("neither click selected anything on its own",
- s.api("clipboard")["text"] in (None, ""), repr(s.api("clipboard")["text"]))
- dbl_click(s, x2, row, 1) # ...and the second cell's pair completes
+ check(
+ "neither click selected anything on its own",
+ s.api("clipboard")["text"] in (None, ""),
+ repr(s.api("clipboard")["text"]),
+ )
+ dbl_click(s, x2, row, 1) # ...and the second cell's pair completes
s.settle(80)
- check("the pair that lands on one cell selects that word",
- s.api("clipboard")["text"] == "quoted", repr(s.api("clipboard")["text"]))
+ check(
+ "the pair that lands on one cell selects that word",
+ s.api("clipboard")["text"] == "quoted",
+ repr(s.api("clipboard")["text"]),
+ )
def test_slow_clicks_are_two_clicks():
@@ -209,11 +247,14 @@ def test_slow_clicks_are_two_clicks():
p = s.pane()
x = p["content_x"] + column_of(LINE, LINE.index("foo_bar") + 3)
dbl_click(s, x, p["content_y"], 1)
- time.sleep(0.5) # longer than double_click_ms (400)
+ time.sleep(0.5) # longer than double_click_ms (400)
dbl_click(s, x, p["content_y"], 1)
s.settle(80)
- check("two slow clicks select no word",
- s.api("clipboard")["text"] in (None, ""), repr(s.api("clipboard")["text"]))
+ check(
+ "two slow clicks select no word",
+ s.api("clipboard")["text"] in (None, ""),
+ repr(s.api("clipboard")["text"]),
+ )
def test_the_separators_are_configurable():
@@ -221,24 +262,31 @@ def test_the_separators_are_configurable():
emptying it makes punctuation part of the word, up to the blanks that always
separate whether they are listed or not."""
import tempfile
+
f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
f.write('word_separators ""\n')
f.close()
with Session(a_line(), cols=110, rows=12, config=f.name) as s:
s.until_text("error")
got = worded(s, "foo_bar", 3)
- check("with no separators, punctuation is part of the word",
- got == "foo_bar(x);", repr(got))
+ check(
+ "with no separators, punctuation is part of the word",
+ got == "foo_bar(x);",
+ repr(got),
+ )
check("but a space still ends it", " " not in got, repr(got))
g = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
- g.write('word_separators "_"\n') # the opposite: split an identifier
+ g.write('word_separators "_"\n') # the opposite: split an identifier
g.close()
with Session(a_line(), cols=110, rows=12, config=g.name) as s:
s.until_text("error")
got = worded(s, "foo_bar", 1)
- check("and a listed character splits what it is listed in", got == "foo",
- repr(got))
+ check(
+ "and a listed character splits what it is listed in",
+ got == "foo",
+ repr(got),
+ )
# The two characters that end or escape a KDL string are also the two a
# double-click most wants to stop at, so the config path has to carry them:
@@ -247,12 +295,15 @@ def test_the_separators_are_configurable():
h = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
h.write("word_separators " + q + bs + q + bs + bs + q + chr(10))
h.close()
- text = 'say ' + q + 'hi' + q + ' and ' + bs + 'esc' + bs + ' ok'
+ text = "say " + q + "hi" + q + " and " + bs + "esc" + bs + " ok"
with Session(a_line(text), cols=80, rows=10, config=h.name) as s:
s.until_text("say")
got = worded(s, "hi", 0, line=text)
- check("a quote can be a separator, escaped through the config",
- got == "hi", repr(got))
+ check(
+ "a quote can be a separator, escaped through the config",
+ got == "hi",
+ repr(got),
+ )
got = worded(s, "esc", 0, line=text)
check("and so can a backslash", got == "esc", repr(got))
@@ -262,11 +313,13 @@ def test_a_word_in_scrollback_is_the_word_on_screen():
scrolled that row is not the row the terminal calls `y`. Same coordinates the
drag selection uses -- viewport, not screen history -- so this is the check that
they stay the same."""
- body = "; ".join("printf 'line%02d unique_%02d\\r\\n'" % (i, i) for i in range(1, 41))
+ body = "; ".join(
+ "printf 'line%02d unique_%02d\\r\\n'" % (i, i) for i in range(1, 41)
+ )
with Session(echoing(body), cols=60, rows=12) as s:
s.until_text("line40")
p = s.pane()
- for _ in range(6): # wheel up into the scrollback
+ for _ in range(6): # wheel up into the scrollback
s.send(rf"\e[<64;{p['content_x'] + 2};{p['content_y'] + 2}M")
s.settle(60)
@@ -276,17 +329,22 @@ def test_a_word_in_scrollback_is_the_word_on_screen():
dbl_click(s, row.index(word), p["content_y"])
s.settle(80)
got = s.api("clipboard")["text"]
- check("and the word copied is the one on screen", got == word,
- "%r from %r" % (got, row.strip()))
+ check(
+ "and the word copied is the one on screen",
+ got == word,
+ "%r from %r" % (got, row.strip()),
+ )
def test_a_mouse_program_keeps_its_double_clicks():
"""The same rule the drag has: a program that asked for the mouse gets the
clicks, and shift is the way to select over one anyway. A word selection that
ignored this would take double-clicks away from every program in every pane."""
- tracking = ["/bin/sh", "-c",
- "printf '\\033[?1002h'; printf 'tracked_word here\\r\\n'; "
- "stty raw -echo; cat"]
+ tracking = [
+ "/bin/sh",
+ "-c",
+ "printf '\\033[?1002h'; printf 'tracked_word here\\r\\n'; stty raw -echo; cat",
+ ]
with Session(tracking, cols=60, rows=10) as s:
s.until_text("tracked_word")
p = s.pane()
@@ -295,16 +353,21 @@ def test_a_mouse_program_keeps_its_double_clicks():
dbl_click(s, col, row)
s.settle(80)
- check("a plain double-click is the program's", s.api("clipboard")["text"] in (None, ""),
- repr(s.api("clipboard")["text"]))
+ check(
+ "a plain double-click is the program's",
+ s.api("clipboard")["text"] in (None, ""),
+ repr(s.api("clipboard")["text"]),
+ )
- for _ in range(2): # the same pair with shift held
+ for _ in range(2): # the same pair with shift held
s.send(rf"\e[<4;{col + 1};{row + 1}M")
s.send(rf"\e[<4;{col + 1};{row + 1}m")
s.settle(80)
- check("shift takes it back and selects the word",
- s.api("clipboard")["text"] == "tracked_word",
- repr(s.api("clipboard")["text"]))
+ check(
+ "shift takes it back and selects the word",
+ s.api("clipboard")["text"] == "tracked_word",
+ repr(s.api("clipboard")["text"]),
+ )
# A line longer than the pane, so the terminal lays it across two rows. The
@@ -317,14 +380,17 @@ def test_a_wrapped_line_is_copied_as_one_line():
"""A row boundary in the grid is not a newline in the text. Copying a wrapped
path or URL used to paste a `\\n` into the middle of it, which is the kind of
wrong that is invisible until it reaches a shell."""
- with Session(["/bin/sh", "-c", 'printf "%%s\\n" "%s"; read x' % WRAPPED],
- cols=40, rows=10) as s:
+ with Session(
+ ["/bin/sh", "-c", 'printf "%%s\\n" "%s"; read x' % WRAPPED], cols=40, rows=10
+ ) as s:
s.settle(200)
p = s.pane()
cx, cy = content(s, p)
- check("the pane really did wrap it",
- s.snapshot().pane_line(p, 1).strip().startswith("DDDD"),
- repr(s.snapshot().pane_line(p, 1)))
+ check(
+ "the pane really did wrap it",
+ s.snapshot().pane_line(p, 1).strip().startswith("DDDD"),
+ repr(s.snapshot().pane_line(p, 1)),
+ )
select(s, cx, cy, cx + 15, cy + 1)
got = s.api("clipboard")["text"]
@@ -335,22 +401,25 @@ def test_a_wrapped_line_is_copied_as_one_line():
def test_real_newlines_are_still_newlines():
"""The other half, and the one a fix for the first could easily break: rows
that are separate *lines* keep the boundaries between them."""
- with Session(["/bin/sh", "-c", 'printf "one\\ntwo\\nthree\\n"; read x'],
- cols=40, rows=10) as s:
+ with Session(
+ ["/bin/sh", "-c", 'printf "one\\ntwo\\nthree\\n"; read x'], cols=40, rows=10
+ ) as s:
s.settle(200)
cx, cy = content(s)
select(s, cx, cy, cx + 4, cy + 2)
got = s.api("clipboard")["text"]
- check("three lines come back as three lines", got == "one\ntwo\nthree",
- repr(got))
+ check(
+ "three lines come back as three lines", got == "one\ntwo\nthree", repr(got)
+ )
def test_a_word_that_straddles_the_wrap_is_still_one_word():
"""Double-clicking the tail of a wrapped word selects the row's run, because a
word selection is a run of cells on one row. What it must not do is paste a
newline, which is the same bug in the other gesture."""
- with Session(["/bin/sh", "-c", 'printf "%%s\\n" "%s"; read x' % WRAPPED],
- cols=40, rows=10) as s:
+ with Session(
+ ["/bin/sh", "-c", 'printf "%%s\\n" "%s"; read x' % WRAPPED], cols=40, rows=10
+ ) as s:
s.settle(200)
cx, cy = content(s)
dbl_click(s, cx + 2, cy)
@@ -367,15 +436,21 @@ def test_middle_click_pastes():
cx, cy = content(s)
select(s, cx, cy, cx + 14, cy)
s.settle(80)
- check("something was copied", s.api("clipboard")["text"] == "paste-me-please",
- repr(s.api("clipboard")["text"]))
+ check(
+ "something was copied",
+ s.api("clipboard")["text"] == "paste-me-please",
+ repr(s.api("clipboard")["text"]),
+ )
s.send(rf"\e[<1;{cx + 1};{cy + 3}M") # middle button
s.send(rf"\e[<1;{cx + 1};{cy + 3}m")
s.settle(150)
text = s.snapshot().pane_text(s.pane())
- check("middle click pastes the selection into the pane",
- text.count("paste-me-please") >= 2, repr(text[:120]))
+ check(
+ "middle click pastes the selection into the pane",
+ text.count("paste-me-please") >= 2,
+ repr(text[:120]),
+ )
def test_a_program_can_copy():
@@ -385,15 +460,18 @@ def test_a_program_can_copy():
# base64 of "from-the-pane"
s.raw(r'printf "\e]52;c;ZnJvbS10aGUtcGFuZQ==\x07"' + "\\n")
s.settle(200)
- check("a pane writing OSC 52 sets the clipboard",
- s.api("clipboard")["text"] == "from-the-pane",
- repr(s.api("clipboard")["text"]))
+ check(
+ "a pane writing OSC 52 sets the clipboard",
+ s.api("clipboard")["text"] == "from-the-pane",
+ repr(s.api("clipboard")["text"]),
+ )
def test_toasts():
# A toast's lifetime is a real duration, so this really does wait for it -
# but the duration is configurable, so it waits 150ms instead of 2500.
import tempfile
+
f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
f.write("toast_ms 150\n")
f.close()
@@ -401,22 +479,32 @@ def test_toasts():
s.settle()
r = s.api("notify", text="build finished")
check("notify is accepted", r["ok"], str(r))
- check("and appears on screen", "build finished" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-120:]))
- check("bottom right, out of the way",
- "build finished" in s.snapshot().text[-1], repr(s.snapshot().text[-1]))
+ check(
+ "and appears on screen",
+ "build finished" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-120:]),
+ )
+ check(
+ "bottom right, out of the way",
+ "build finished" in s.snapshot().text[-1],
+ repr(s.snapshot().text[-1]),
+ )
s.api("notify", text="second thing")
snap = s.snapshot()
- check("several stack up",
- "build finished" in snap.screen() and "second thing" in snap.screen(),
- repr(snap.screen()[-200:]))
+ check(
+ "several stack up",
+ "build finished" in snap.screen() and "second thing" in snap.screen(),
+ repr(snap.screen()[-200:]),
+ )
time.sleep(0.25)
s.settle(20)
- check("and they expire on their own",
- "build finished" not in s.snapshot().screen(),
- repr(s.snapshot().screen()[-120:]))
+ check(
+ "and they expire on their own",
+ "build finished" not in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-120:]),
+ )
def test_pane_notifications_become_toasts():
@@ -424,23 +512,34 @@ def test_pane_notifications_become_toasts():
s.settle()
s.raw(r'printf "\e]9;agent needs you\x07"' + "\\n")
s.settle(200)
- check("OSC 9 from a pane becomes a toast",
- "agent needs you" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-160:]))
+ check(
+ "OSC 9 from a pane becomes a toast",
+ "agent needs you" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-160:]),
+ )
def test_selection_does_not_fight_a_mouse_program():
- tracker = ["/bin/sh", "-c",
- 'stty raw -echo; printf "\\033[?1000h\\033[?1006h"; cat -v']
+ tracker = [
+ "/bin/sh",
+ "-c",
+ 'stty raw -echo; printf "\\033[?1000h\\033[?1006h"; cat -v',
+ ]
with Session(tracker, cols=50, rows=10) as s:
s.settle()
cx, cy = content(s)
select(s, cx + 1, cy + 1, cx + 6, cy + 1)
s.settle(80)
- check("a mouse-tracking program keeps its own clicks",
- s.api("clipboard")["text"] == "", repr(s.api("clipboard")["text"]))
- check("and receives them", "^[[<0;" in s.snapshot().pane_text(s.pane()),
- repr(s.snapshot().pane_text(s.pane())[:100]))
+ check(
+ "a mouse-tracking program keeps its own clicks",
+ s.api("clipboard")["text"] == "",
+ repr(s.api("clipboard")["text"]),
+ )
+ check(
+ "and receives them",
+ "^[[<0;" in s.snapshot().pane_text(s.pane()),
+ repr(s.snapshot().pane_text(s.pane())[:100]),
+ )
if __name__ == "__main__":
diff --git a/tests/test_config.py b/tests/test_config.py
index c8bc524..f0528cc 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -5,6 +5,7 @@ Everything here was a compiled-in constant until now. The important property
is not that settings apply — it is that a broken config costs a warning and
never a terminal.
"""
+
import os
import pathlib
import re
@@ -13,7 +14,7 @@ import sys
import tempfile
import time
-from harness import Session, check, report, BIN
+from harness import BIN, Session, check, report
SH = ["/bin/sh", "-c", 'printf "\\033]2;cfg\\007"; stty raw -echo; cat']
@@ -41,10 +42,16 @@ def test_geometry():
snap, p = s.snapshot(), s.pane()
check("gap is applied", p["x"] == 2 and p["y"] == 3, str(p))
row = snap.line(p["y"])
- check("rounded false gives square corners",
- "┌" in row and "╭" not in row, repr(row))
- check("title_align left puts the title at the left",
- row.index("cfg") < len(row) // 3, repr(row))
+ check(
+ "rounded false gives square corners",
+ "┌" in row and "╭" not in row,
+ repr(row),
+ )
+ check(
+ "title_align left puts the title at the left",
+ row.index("cfg") < len(row) // 3,
+ repr(row),
+ )
os.unlink(path)
@@ -52,10 +59,12 @@ def insets(pane):
"""A pane's content rect as four insets from its frame: top, right, bottom,
left. What `padding` is actually about, and the only way to see it is to
measure both rects."""
- return (pane["content_y"] - pane["y"],
- (pane["x"] + pane["w"]) - (pane["content_x"] + pane["content_w"]),
- (pane["y"] + pane["h"]) - (pane["content_y"] + pane["content_h"]),
- pane["content_x"] - pane["x"])
+ return (
+ pane["content_y"] - pane["y"],
+ (pane["x"] + pane["w"]) - (pane["content_x"] + pane["content_w"]),
+ (pane["y"] + pane["h"]) - (pane["content_y"] + pane["content_h"]),
+ pane["content_x"] - pane["x"],
+ )
def pad_insets(text, cols=60, rows=16):
@@ -71,23 +80,34 @@ def test_padding_takes_one_two_or_four_values():
"""One for every side, two for vertical and horizontal, four in CSS order.
A number means the same thing however many of them were written, which is
why all three of these are the same padding."""
- check("no padding is just the border", pad_insets("padding 0\n") == (1, 1, 1, 1),
- str(pad_insets("padding 0\n")))
+ check(
+ "no padding is just the border",
+ pad_insets("padding 0\n") == (1, 1, 1, 1),
+ str(pad_insets("padding 0\n")),
+ )
one = pad_insets("padding 1\n")
check("one value pads every side", one == (2, 3, 2, 3), str(one))
- check("...and two equal values say the same thing",
- pad_insets("padding 1 1\n") == one, str(pad_insets("padding 1 1\n")))
- check("...as do four", pad_insets("padding 1 1 1 1\n") == one,
- str(pad_insets("padding 1 1 1 1\n")))
+ check(
+ "...and two equal values say the same thing",
+ pad_insets("padding 1 1\n") == one,
+ str(pad_insets("padding 1 1\n")),
+ )
+ check(
+ "...as do four",
+ pad_insets("padding 1 1 1 1\n") == one,
+ str(pad_insets("padding 1 1 1 1\n")),
+ )
# Horizontal padding is aspect-corrected, like the gap: the numbers are rows,
# so a ring that looks square is one number rather than arithmetic.
two = pad_insets("padding 0 2\n")
check("two values are vertical then horizontal", two == (1, 5, 1, 5), str(two))
- check("and gap_aspect 1 makes the units plain cells",
- pad_insets("gap_aspect 1\npadding 0 2\n") == (1, 3, 1, 3),
- str(pad_insets("gap_aspect 1\npadding 0 2\n")))
+ check(
+ "and gap_aspect 1 makes the units plain cells",
+ pad_insets("gap_aspect 1\npadding 0 2\n") == (1, 3, 1, 3),
+ str(pad_insets("gap_aspect 1\npadding 0 2\n")),
+ )
four = pad_insets("padding 2 0 0 1\n")
check("four values are top right bottom left", four == (3, 1, 1, 3), str(four))
@@ -100,12 +120,18 @@ def test_a_padding_nobody_can_read_is_refused():
path = cfg("padding 1 2 3\ngap 2\n")
with Session(SH, cols=60, rows=16, config=path) as s:
s.settle()
- check("the padding was not applied", insets(s.pane()) == (1, 1, 1, 1),
- str(insets(s.pane())))
+ check(
+ "the padding was not applied",
+ insets(s.pane()) == (1, 1, 1, 1),
+ str(insets(s.pane())),
+ )
check("but the rest of the file was", s.pane()["y"] == 3, str(s.pane()))
reply = s.api("reload")
- check("and it says what it wanted", "padding takes 1, 2 or 4" in
- reply.get("warning", ""), str(reply))
+ check(
+ "and it says what it wanted",
+ "padding takes 1, 2 or 4" in reply.get("warning", ""),
+ str(reply),
+ )
os.unlink(path)
@@ -114,8 +140,11 @@ def test_status_bar_off():
with Session(SH, cols=50, rows=10, config=path) as s:
s.settle()
snap, p = s.snapshot(), s.pane()
- check("no status bar means no strip row", "panes" not in snap.screen(),
- repr(snap.line(1)))
+ check(
+ "no status bar means no strip row",
+ "panes" not in snap.screen(),
+ repr(snap.line(1)),
+ )
check("and the panes get the row back", p["y"] == 1, str(p))
os.unlink(path)
@@ -131,8 +160,11 @@ def test_theme():
s.settle()
p = s.pane()
run = s.snapshot().style_at(p["x"], p["y"])
- check("the focused frame takes the configured colour",
- run and run["fg"] == "#00ff00", str(run))
+ check(
+ "the focused frame takes the configured colour",
+ run and run["fg"] == "#00ff00",
+ str(run),
+ )
s.key("\\\\")
s.settle()
@@ -140,8 +172,11 @@ def test_theme():
snap = s.snapshot()
idle = [q for q in panes if not q["focused"]][0]
run = snap.style_at(idle["x"], idle["y"])
- check("an unfocused frame takes the other one",
- run and run["fg"] == "#333333", str(run))
+ check(
+ "an unfocused frame takes the other one",
+ run and run["fg"] == "#333333",
+ str(run),
+ )
os.unlink(path)
@@ -157,18 +192,24 @@ def test_keys():
s.settle()
s.send(r"\x02v") # C-b v
s.settle()
- check("a custom prefix and binding work", len(s.panes()) == 2,
- str(len(s.panes())))
+ check(
+ "a custom prefix and binding work", len(s.panes()) == 2, str(len(s.panes()))
+ )
s.send("\\x02\\\\") # C-b \ -> unbound now
s.settle()
- check("a binding set to none does nothing", len(s.panes()) == 2,
- str(len(s.panes())))
+ check(
+ "a binding set to none does nothing",
+ len(s.panes()) == 2,
+ str(len(s.panes())),
+ )
s.send(r"\x01") # the old prefix is just a keystroke now
s.settle()
- check("the old prefix is passed through to the pane",
- len(s.panes()) == 2 and s.api("alive")["alive"])
+ check(
+ "the old prefix is passed through to the pane",
+ len(s.panes()) == 2 and s.api("alive")["alive"],
+ )
s.send(r"\x02\x02") # prefix twice is still a literal prefix
s.settle()
@@ -197,17 +238,15 @@ def test_a_chord_can_be_written_the_way_the_cheatsheet_prints_it():
# `?` is shift+slash, which is what the key says on the keyboard.
s.send(r"\x02?")
s.settle()
- check("`?` binds the key you press", len(s.panes()) == 2,
- str(len(s.panes())))
+ check("`?` binds the key you press", len(s.panes()) == 2, str(len(s.panes())))
- s.send(r"\x02\e[Z") # C-b shift+tab
+ s.send(r"\x02\e[Z") # C-b shift+tab
s.settle()
check("`S-tab` is shift+tab", len(s.panes()) == 3, str(len(s.panes())))
- s.send(r"\x02\e[C") # C-b right arrow
+ s.send(r"\x02\e[C") # C-b right arrow
s.settle()
- check("an arrow glyph is that arrow", len(s.panes()) == 2,
- str(len(s.panes())))
+ check("an arrow glyph is that arrow", len(s.panes()) == 2, str(len(s.panes())))
os.unlink(path)
# A capital letter is that letter with shift, not that letter: the sheet
@@ -215,14 +254,16 @@ def test_a_chord_can_be_written_the_way_the_cheatsheet_prints_it():
path = cfg('keys {\n bind "H" "split-cols"\n}\n')
with Session(SH, cols=100, rows=30, config=path) as s:
s.settle()
- s.send(r"\x01h") # plain h still moves focus, it does not split
+ s.send(r"\x01h") # plain h still moves focus, it does not split
s.settle()
- check("a plain letter is left alone", len(s.panes()) == 1,
- str(len(s.panes())))
+ check("a plain letter is left alone", len(s.panes()) == 1, str(len(s.panes())))
s.send(r"\x01H")
s.settle()
- check("and the capital is the shifted one", len(s.panes()) == 2,
- str(len(s.panes())))
+ check(
+ "and the capital is the shifted one",
+ len(s.panes()) == 2,
+ str(len(s.panes())),
+ )
os.unlink(path)
@@ -237,14 +278,20 @@ def test_min_pane_drives_collapse():
s.settle()
s.api("split", dir="cols")
s.settle()
- check("a low floor keeps both panes visible",
- not any(p["hidden"] for p in s.panes()), str(s.panes()))
+ check(
+ "a low floor keeps both panes visible",
+ not any(p["hidden"] for p in s.panes()),
+ str(s.panes()),
+ )
with Session(SH, cols=70, rows=16, config=tight) as s:
s.settle()
s.api("split", dir="cols")
s.settle()
- check("a high floor collapses the same split",
- any(p["hidden"] for p in s.panes()), str(s.panes()))
+ check(
+ "a high floor collapses the same split",
+ any(p["hidden"] for p in s.panes()),
+ str(s.panes()),
+ )
os.unlink(roomy)
os.unlink(tight)
@@ -254,17 +301,20 @@ def test_fail_open():
broken = cfg('gap 1\ntheme {\n frame_focus "#00ff00"\n') # unclosed block
with Session(SH, cols=60, rows=14, config=broken) as s:
s.settle()
- check("a session still starts on a broken config",
- s.api("alive")["alive"], "")
+ check("a session still starts on a broken config", s.api("alive")["alive"], "")
p = s.pane()
- check("and uses the compiled-in defaults", p["x"] == 2 and p["y"] == 2,
- str(p))
- proc = subprocess.run([BIN, "--headless", "--cols", "20", "--rows", "3",
- "--", "/bin/echo", "hi"],
- capture_output=True, text=True,
- env=dict(os.environ, SL0PPTY_CONFIG=broken))
- check("the reason is reported on stderr",
- "line" in proc.stderr or "parse" in proc.stderr, repr(proc.stderr))
+ check("and uses the compiled-in defaults", p["x"] == 2 and p["y"] == 2, str(p))
+ proc = subprocess.run(
+ [BIN, "--headless", "--cols", "20", "--rows", "3", "--", "/bin/echo", "hi"],
+ capture_output=True,
+ text=True,
+ env=dict(os.environ, SL0PPTY_CONFIG=broken),
+ )
+ check(
+ "the reason is reported on stderr",
+ "line" in proc.stderr or "parse" in proc.stderr,
+ repr(proc.stderr),
+ )
check("but the run succeeds", proc.returncode == 0, proc.stderr)
os.unlink(broken)
@@ -275,15 +325,20 @@ def test_fail_open():
with Session(SH, cols=60, rows=14, config=partial) as s:
s.settle()
# gap 3, aspect 2 (default) -> 6 columns of margin
- check("a bad binding does not discard the rest of the file",
- s.pane()["x"] == 6, str(s.pane()))
+ check(
+ "a bad binding does not discard the rest of the file",
+ s.pane()["x"] == 6,
+ str(s.pane()),
+ )
os.unlink(partial)
missing = "/nonexistent/definitely/not/here.kdl"
- proc = subprocess.run([BIN, "--headless", "--cols", "20", "--rows", "3",
- "--", "/bin/echo", "hi"],
- capture_output=True, text=True,
- env=dict(os.environ, SL0PPTY_CONFIG=missing))
+ proc = subprocess.run(
+ [BIN, "--headless", "--cols", "20", "--rows", "3", "--", "/bin/echo", "hi"],
+ capture_output=True,
+ text=True,
+ env=dict(os.environ, SL0PPTY_CONFIG=missing),
+ )
check("a missing config is silent", proc.stderr == "", repr(proc.stderr))
@@ -296,16 +351,18 @@ def test_reload():
f.write("gap 3\n")
r = s.api("reload")
s.settle()
- check("reload applies the new file", r["ok"] and s.pane()["x"] == 6,
- f"{r} {s.pane()}")
+ check(
+ "reload applies the new file",
+ r["ok"] and s.pane()["x"] == 6,
+ f"{r} {s.pane()}",
+ )
with open(path, "w") as f:
f.write("gap 5\ntheme {\n") # broken
r = s.api("reload")
s.settle()
check("a broken reload is refused", not r["ok"], str(r))
- check("and the working config is kept", s.pane()["x"] == 6,
- str(s.pane()))
+ check("and the working config is kept", s.pane()["x"] == 6, str(s.pane()))
os.unlink(path)
@@ -317,54 +374,74 @@ def test_status_line_reserves_a_row_at_the_bottom():
with Session(SH, cols=50, rows=12, config=path) as s:
s.settle()
heights[name] = s.pane()["h"]
- check("turning the status line off gives the row back to the panes",
- heights["off"] == heights["on"] + 1, str(heights))
+ check(
+ "turning the status line off gives the row back to the panes",
+ heights["off"] == heights["on"] + 1,
+ str(heights),
+ )
os.unlink(on)
os.unlink(off)
def test_status_line_says_what_you_are_looking_at():
lay = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
- lay.write('layout {\n tab name="api" purpose="project:api.a1" {\n'
- ' pane purpose="agent:main"\n'
- ' pane suspended=true command="top"\n }\n}\n')
+ lay.write(
+ 'layout {\n tab name="api" purpose="project:api.a1" {\n'
+ ' pane purpose="agent:main"\n'
+ ' pane suspended=true command="top"\n }\n}\n'
+ )
lay.close()
with Session(SH, cols=90, rows=18, layout=lay.name) as s:
s.settle(20)
bottom = s.snapshot().text[-2]
- check("it names the tab and the focused pane",
- "api" in bottom and "agent:main" in bottom, repr(bottom))
+ check(
+ "it names the tab and the focused pane",
+ "api" in bottom and "agent:main" in bottom,
+ repr(bottom),
+ )
# The bottom carries a count too, but it is this tab's rather than the
# session's (see test_the_two_pane_counts_answer_different_questions).
# What it must not do is restate the strip itself.
- check("and is not a second copy of the tab strip",
- "pane 1/2" in bottom and "1:api" not in bottom, repr(bottom))
+ check(
+ "and is not a second copy of the tab strip",
+ "pane 1/2" in bottom and "1:api" not in bottom,
+ repr(bottom),
+ )
ids = [p["id"] for p in s.panes()]
s.api("focus", id=ids[1])
s.settle(20)
- check("a pane that has not started says so",
- "not started" in s.snapshot().text[-2],
- repr(s.snapshot().text[-2]))
+ check(
+ "a pane that has not started says so",
+ "not started" in s.snapshot().text[-2],
+ repr(s.snapshot().text[-2]),
+ )
os.unlink(lay.name)
def test_status_line_reports_scrollback():
- noisy = ["/bin/sh", "-c",
- 'printf "\\033]2;p\\007"; i=0; while [ $i -lt 200 ]; do '
- 'echo "line $i"; i=$((i+1)); done; stty raw -echo; cat']
+ noisy = [
+ "/bin/sh",
+ "-c",
+ 'printf "\\033]2;p\\007"; i=0; while [ $i -lt 200 ]; do '
+ 'echo "line $i"; i=$((i+1)); done; stty raw -echo; cat',
+ ]
with Session(noisy, cols=90, rows=18) as s:
s.settle(30)
- check("nothing is said while you are in the present",
- "scrolled" not in s.snapshot().text[-2],
- repr(s.snapshot().text[-2]))
+ check(
+ "nothing is said while you are in the present",
+ "scrolled" not in s.snapshot().text[-2],
+ repr(s.snapshot().text[-2]),
+ )
p = s.pane()
for _ in range(6):
s.send(rf"\e[<64;{p['content_x'] + 2};{p['content_y'] + 2}M")
s.settle(30)
- check("looking at the past is said out loud",
- "scrolled" in s.snapshot().text[-2],
- repr(s.snapshot().text[-2]))
+ check(
+ "looking at the past is said out loud",
+ "scrolled" in s.snapshot().text[-2],
+ repr(s.snapshot().text[-2]),
+ )
def test_the_two_pane_counts_answer_different_questions():
@@ -372,40 +449,53 @@ def test_the_two_pane_counts_answer_different_questions():
tab's panes you are in, which is the question you actually have once a tab
has collapsed into a list and only one of them is open."""
lay = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
- lay.write('layout {\n tab name="api" {\n pane\n pane\n }\n'
- ' tab name="notes" {\n pane\n }\n}\n')
+ lay.write(
+ 'layout {\n tab name="api" {\n pane\n pane\n }\n'
+ ' tab name="notes" {\n pane\n }\n}\n'
+ )
lay.close()
with Session(SH, cols=80, rows=16, layout=lay.name) as s:
s.settle(20)
top, bottom = s.snapshot().line(1), s.snapshot().text[-2]
- check("the strip counts every pane in the session",
- "3 panes" in top, repr(top))
- check("the line says which of this tab's panes is open",
- "pane 1/2" in bottom, repr(bottom))
+ check("the strip counts every pane in the session", "3 panes" in top, repr(top))
+ check(
+ "the line says which of this tab's panes is open",
+ "pane 1/2" in bottom,
+ repr(bottom),
+ )
ids = [p["id"] for p in s.panes() if p["tab"] == 1]
s.api("focus", id=ids[1])
s.settle(20)
- check("the index moves with the focus",
- "pane 2/2" in s.snapshot().text[-2],
- repr(s.snapshot().text[-2]))
- check("while the session count does not",
- "3 panes" in s.snapshot().line(1), repr(s.snapshot().line(1)))
+ check(
+ "the index moves with the focus",
+ "pane 2/2" in s.snapshot().text[-2],
+ repr(s.snapshot().text[-2]),
+ )
+ check(
+ "while the session count does not",
+ "3 panes" in s.snapshot().line(1),
+ repr(s.snapshot().line(1)),
+ )
tabs = s.tabs()
s.api("select-tab", id=tabs[1]["id"])
s.settle(20)
top, bottom = s.snapshot().line(1), s.snapshot().text[-2]
check("a one-pane tab says so", "pane 1/1" in bottom, repr(bottom))
- check("and the session count still counts the session",
- "3 panes" in top, repr(top))
+ check(
+ "and the session count still counts the session",
+ "3 panes" in top,
+ repr(top),
+ )
s.api("split", dir="cols")
s.settle(20)
- check("splitting moves both numbers, each in its own way",
- "pane 2/2" in s.snapshot().text[-2]
- and "4 panes" in s.snapshot().line(1),
- repr(s.snapshot().text[-2]) + " / " + repr(s.snapshot().line(1)))
+ check(
+ "splitting moves both numbers, each in its own way",
+ "pane 2/2" in s.snapshot().text[-2] and "4 panes" in s.snapshot().line(1),
+ repr(s.snapshot().text[-2]) + " / " + repr(s.snapshot().line(1)),
+ )
os.unlink(lay.name)
@@ -419,57 +509,73 @@ def test_status_pad_holds_both_bars_off_the_edge():
with Session(SH, cols=80, rows=16, config=path, layout=lay.name) as s:
s.settle(20)
top, bottom = s.snapshot().line(1), s.snapshot().text[-2]
- seen[pad] = (len(top) - len(top.lstrip()),
- len(top.rstrip()),
- len(bottom) - len(bottom.lstrip()),
- len(bottom.rstrip()))
+ seen[pad] = (
+ len(top) - len(top.lstrip()),
+ len(top.rstrip()),
+ len(bottom) - len(bottom.lstrip()),
+ len(bottom.rstrip()),
+ )
os.unlink(path)
- check("a bigger pad indents the strip further from the left",
- seen[8][0] == seen[2][0] + 6, str(seen))
- check("and holds it further off the right",
- seen[8][1] == seen[2][1] - 6, str(seen))
- check("the line below is padded the same on the left",
- seen[8][2] == seen[2][2] + 6, str(seen))
+ check(
+ "a bigger pad indents the strip further from the left",
+ seen[8][0] == seen[2][0] + 6,
+ str(seen),
+ )
+ check("and holds it further off the right", seen[8][1] == seen[2][1] - 6, str(seen))
+ check(
+ "the line below is padded the same on the left",
+ seen[8][2] == seen[2][2] + 6,
+ str(seen),
+ )
check("and on the right", seen[8][3] == seen[2][3] - 6, str(seen))
os.unlink(lay.name)
-
def test_every_surface_has_its_own_theme_name():
"""The point of splitting six names into thirty-five: recolouring one
surface must not recolour another that happened to share an entry."""
- conf = cfg('theme {\n'
- ' guide "#00ff00"\n'
- ' tab_active_bg "#0000ff"\n'
- ' status "#ff8800"\n'
- ' header "#ff00ff"\n'
- '}\n')
+ conf = cfg(
+ "theme {\n"
+ ' guide "#00ff00"\n'
+ ' tab_active_bg "#0000ff"\n'
+ ' status "#ff8800"\n'
+ ' header "#ff00ff"\n'
+ "}\n"
+ )
with Session(SH, cols=80, rows=16, config=conf) as s:
s.settle(20)
snap = s.snapshot()
tab = [h for h in snap.hits if h["action"].startswith("tab:")][0]
- check("tab_active_bg fills the tab you are in",
- (snap.style_at(tab["x"] + 1, tab["y"]) or {}).get("bg") == "#0000ff",
- str(snap.style_at(tab["x"] + 1, tab["y"])))
- check("status colours the bottom line",
- (snap.style_at(4, snap.rows - 2) or {}).get("fg") == "#ff8800",
- str(snap.style_at(4, snap.rows - 2)))
+ check(
+ "tab_active_bg fills the tab you are in",
+ (snap.style_at(tab["x"] + 1, tab["y"]) or {}).get("bg") == "#0000ff",
+ str(snap.style_at(tab["x"] + 1, tab["y"])),
+ )
+ check(
+ "status colours the bottom line",
+ (snap.style_at(4, snap.rows - 2) or {}).get("fg") == "#ff8800",
+ str(snap.style_at(4, snap.rows - 2)),
+ )
# ...and the frame, which shares a *default* with guide, did not move.
p = s.pane()
- check("the focused frame keeps its own colour",
- (snap.style_at(p["x"], p["y"] + 1) or {}).get("fg") == "#ff5fd7",
- str(snap.style_at(p["x"], p["y"] + 1)))
+ check(
+ "the focused frame keeps its own colour",
+ (snap.style_at(p["x"], p["y"] + 1) or {}).get("fg") == "#ff5fd7",
+ str(snap.style_at(p["x"], p["y"] + 1)),
+ )
# The guide is the one that used to be the same entry as the frame.
hover(s, p["x"], p["y"] + 3)
time.sleep(0.35)
s.settle(40)
snap = s.snapshot()
- check("and the split guide takes the colour named for it",
- (snap.style_at(p["x"], p["y"] + 3) or {}).get("fg") == "#00ff00",
- str(snap.style_at(p["x"], p["y"] + 3)))
+ check(
+ "and the split guide takes the colour named for it",
+ (snap.style_at(p["x"], p["y"] + 3) or {}).get("fg") == "#00ff00",
+ str(snap.style_at(p["x"], p["y"] + 3)),
+ )
os.unlink(conf)
@@ -479,10 +585,11 @@ def test_an_unknown_theme_name_is_refused_not_ignored():
s.settle(20)
check("a bad colour costs a warning, not a terminal", s.alive(), "")
p = s.pane()
- check("and the compiled-in default is kept",
- (s.snapshot().style_at(p["x"], p["y"] + 1) or {}).get("fg")
- == "#ff5fd7",
- str(s.snapshot().style_at(p["x"], p["y"] + 1)))
+ check(
+ "and the compiled-in default is kept",
+ (s.snapshot().style_at(p["x"], p["y"] + 1) or {}).get("fg") == "#ff5fd7",
+ str(s.snapshot().style_at(p["x"], p["y"] + 1)),
+ )
os.unlink(conf)
@@ -493,8 +600,11 @@ def test_a_setting_nobody_knows_is_said_out_loud():
path = cfg("gap 2\nwobble 3\n")
out = subprocess.run([BIN, "--check", path], capture_output=True, text=True)
said = out.stderr + out.stdout
- check("it names the setting it does not know", "unknown setting: wobble" in said,
- repr(said))
+ check(
+ "it names the setting it does not know",
+ "unknown setting: wobble" in said,
+ repr(said),
+ )
check("and the line it is on", ":2:" in said, repr(said))
check("and exits non-zero", out.returncode == 1, str(out.returncode))
os.unlink(path)
@@ -508,8 +618,11 @@ def test_the_two_documents_are_told_apart():
lay = cfg('layout {\n tab name="x" { pane }\n}\n')
out = subprocess.run([BIN, "--check", lay], capture_output=True, text=True)
said = out.stderr + out.stdout
- check("a layout handed to --check is checked as a layout",
- "ok, a layout" in said, repr(said))
+ check(
+ "a layout handed to --check is checked as a layout",
+ "ok, a layout" in said,
+ repr(said),
+ )
check("...and a clean one exits zero", out.returncode == 0, str(out.returncode))
# Held to the layout schema, not the config one: the proof is that it names a
@@ -517,32 +630,73 @@ def test_the_two_documents_are_told_apart():
typo = cfg('layout {\n tab name="x" { pane cmd="htop" }\n}\n')
out1 = subprocess.run([BIN, "--check", typo], capture_output=True, text=True)
said1 = out1.stderr + out1.stdout
- check("a mistyped pane property is named", "unknown pane property: cmd" in said1,
- repr(said1))
+ check(
+ "a mistyped pane property is named",
+ "unknown pane property: cmd" in said1,
+ repr(said1),
+ )
check("and the line it is on", ":2:" in said1, repr(said1))
check("and it exits non-zero", out1.returncode == 1, str(out1.returncode))
- conf = cfg("gap 2\ntheme { frame_focus \"#00ff88\" }\n")
+ conf = cfg('gap 2\ntheme { frame_focus "#00ff88" }\n')
out2 = subprocess.run(
- [BIN, "--script", "--cols", "40", "--rows", "8", "--layout", conf,
- "--", "/bin/sh", "-c", "read x"],
- input="quit\n", capture_output=True, text=True)
+ [
+ BIN,
+ "--script",
+ "--cols",
+ "40",
+ "--rows",
+ "8",
+ "--layout",
+ conf,
+ "--",
+ "/bin/sh",
+ "-c",
+ "read x",
+ ],
+ input="quit\n",
+ capture_output=True,
+ text=True,
+ )
said2 = out2.stderr + out2.stdout
- check("a config handed to --layout says it is a config",
- "this is a config, not a layout" in said2, repr(said2))
- check("...and names the setting that gave it away", "`gap` is a setting" in said2,
- repr(said2))
+ check(
+ "a config handed to --layout says it is a config",
+ "this is a config, not a layout" in said2,
+ repr(said2),
+ )
+ check(
+ "...and names the setting that gave it away",
+ "`gap` is a setting" in said2,
+ repr(said2),
+ )
# A layout whose own tabs are misspelled is a different mistake, and keeps its
# own message rather than being told it is a config.
broken = cfg('layout {\n tabb name="x" { pane }\n}\n')
out3 = subprocess.run(
- [BIN, "--script", "--cols", "40", "--rows", "8", "--layout", broken,
- "--", "/bin/sh", "-c", "read x"],
- input="quit\n", capture_output=True, text=True)
- check("a layout with no tabs still says that",
- "layout declares no tabs" in (out3.stderr + out3.stdout),
- repr(out3.stderr[:120]))
+ [
+ BIN,
+ "--script",
+ "--cols",
+ "40",
+ "--rows",
+ "8",
+ "--layout",
+ broken,
+ "--",
+ "/bin/sh",
+ "-c",
+ "read x",
+ ],
+ input="quit\n",
+ capture_output=True,
+ text=True,
+ )
+ check(
+ "a layout with no tabs still says that",
+ "layout declares no tabs" in (out3.stderr + out3.stdout),
+ repr(out3.stderr[:120]),
+ )
for p in (lay, typo, conf, broken):
os.unlink(p)
@@ -551,32 +705,60 @@ def test_the_shipped_files_are_what_they_claim():
"""The example layout is a layout and the reference config is a config, checked
by the program rather than by their names."""
root = pathlib.Path(__file__).resolve().parent.parent
- conf = subprocess.run([BIN, "--check", str(root / "config" / "config.kdl")],
- capture_output=True, text=True)
- check("config/config.kdl lints clean", conf.returncode == 0,
- repr(conf.stderr[:160]))
+ conf = subprocess.run(
+ [BIN, "--check", str(root / "config" / "config.kdl")],
+ capture_output=True,
+ text=True,
+ )
+ check(
+ "config/config.kdl lints clean", conf.returncode == 0, repr(conf.stderr[:160])
+ )
lay = root / "config" / "example.layout.kdl"
check("the example layout is named `*.layout.kdl`", lay.exists(), str(lay))
exl = subprocess.run([BIN, "--check", str(lay)], capture_output=True, text=True)
- check("and it lints clean as a layout", exl.returncode == 0,
- repr(exl.stderr[:200]))
+ check("and it lints clean as a layout", exl.returncode == 0, repr(exl.stderr[:200]))
out = subprocess.run(
- [BIN, "--script", "--cols", "60", "--rows", "12", "--layout", str(lay),
- "--", "/bin/sh", "-c", "read x"],
- input="tabs\nquit\n", capture_output=True, text=True)
- check("and it loads as one", '"tabs"' in out.stdout or "api" in out.stdout,
- repr(out.stdout[:160]) + repr(out.stderr[:120]))
+ [
+ BIN,
+ "--script",
+ "--cols",
+ "60",
+ "--rows",
+ "12",
+ "--layout",
+ str(lay),
+ "--",
+ "/bin/sh",
+ "-c",
+ "read x",
+ ],
+ input="tabs\nquit\n",
+ capture_output=True,
+ text=True,
+ )
+ check(
+ "and it loads as one",
+ '"tabs"' in out.stdout or "api" in out.stdout,
+ repr(out.stdout[:160]) + repr(out.stderr[:120]),
+ )
def test_the_known_settings_list_cannot_go_stale():
"""`KNOWN_TOP` is what "unknown setting" is measured against, so a setting added
to the loader and not to that list would be reported as unknown -- which is the
same silent-ish wrongness one step along."""
- src = (pathlib.Path(__file__).resolve().parent.parent / "src" / "config.c").read_text()
+ src = (
+ pathlib.Path(__file__).resolve().parent.parent / "src" / "config.c"
+ ).read_text()
asked = set(re.findall(r'kdl_child\(root, "([a-z_]+)"\)', src))
- listed = set(re.findall(r'^ "([a-z_]+)",', src[src.index("KNOWN_TOP[] = {"):], re.M))
- check("every setting the loader reads is in KNOWN_TOP", asked <= listed,
- "missing: %s" % sorted(asked - listed))
+ listed = set(
+ re.findall(r'^ "([a-z_]+)",', src[src.index("KNOWN_TOP[] = {") :], re.M)
+ )
+ check(
+ "every setting the loader reads is in KNOWN_TOP",
+ asked <= listed,
+ "missing: %s" % sorted(asked - listed),
+ )
check("and the list was actually found", len(listed) > 20, str(len(listed)))
diff --git a/tests/test_corner.py b/tests/test_corner.py
index 244f65d..a7af948 100644
--- a/tests/test_corner.py
+++ b/tests/test_corner.py
@@ -6,6 +6,7 @@ boundary begins, because they belong to different splits -- so the corner is
found by adjacency, and it has to move every boundary that meets there or the
line it looks like would come apart.
"""
+
import os
import sys
import tempfile
@@ -16,15 +17,19 @@ from harness import Session, check, report
SH = ["/bin/sh", "-c", 'printf "\\033]2;p\\007"; stty raw -echo; cat']
CROSS, CROSS_ON = "\u253c", "\u256c"
-GRID = ('layout {\n tab name="t" {\n pane split="rows" {\n'
- ' pane split="cols" { pane\n pane }\n'
- ' pane split="cols" { pane\n pane }\n }\n }\n}\n')
+GRID = (
+ 'layout {\n tab name="t" {\n pane split="rows" {\n'
+ ' pane split="cols" { pane\n pane }\n'
+ ' pane split="cols" { pane\n pane }\n }\n }\n}\n'
+)
COLUMNS = 'layout {\n tab name="t" {\n pane\n pane\n }\n}\n'
# The other nesting: a full-height column boundary with row boundaries running
# into it from the sides, rather than the other way round.
-GRID2 = ('layout {\n tab name="t" {\n pane split="cols" {\n'
- ' pane split="rows" { pane\n pane }\n'
- ' pane split="rows" { pane\n pane }\n }\n }\n}\n')
+GRID2 = (
+ 'layout {\n tab name="t" {\n pane split="cols" {\n'
+ ' pane split="rows" { pane\n pane }\n'
+ ' pane split="rows" { pane\n pane }\n }\n }\n}\n'
+)
def lay(text):
@@ -54,13 +59,18 @@ def test_a_grid_has_a_corner_where_the_boundaries_cross():
h_gap = [g for g in gaps if g["h"] == 1][0]
v_gap = [g for g in gaps if g["h"] > 1][0]
check("it sits on the row boundary", c[0]["y"] == h_gap["y"], str(c[0]))
- check("and in the column boundary's columns",
- c[0]["x"] == v_gap["x"] and c[0]["w"] == v_gap["w"], str(c[0]))
+ check(
+ "and in the column boundary's columns",
+ c[0]["x"] == v_gap["x"] and c[0]["w"] == v_gap["w"],
+ str(c[0]),
+ )
# Registered after both gaps, so those cells resolve to the corner.
snap = s.snapshot()
- check("and wins those cells from both of them",
- snap.hit_at(c[0]["x"], c[0]["y"]) == c[0]["action"],
- str(snap.hit_at(c[0]["x"], c[0]["y"])))
+ check(
+ "and wins those cells from both of them",
+ snap.hit_at(c[0]["x"], c[0]["y"]) == c[0]["action"],
+ str(snap.hit_at(c[0]["x"], c[0]["y"])),
+ )
os.unlink(l)
@@ -87,18 +97,24 @@ def test_dragging_it_moves_both_boundaries():
s.settle(40)
after = rects(s)
- check("the column boundary moved right",
- after[top_left][2] > before[top_left][2]
- and after[top_right][2] < before[top_right][2],
- f"{before[top_left]} -> {after[top_left]}")
- check("and the row boundary moved down",
- after[top_left][3] > before[top_left][3],
- f"{before[top_left]} -> {after[top_left]}")
+ check(
+ "the column boundary moved right",
+ after[top_left][2] > before[top_left][2]
+ and after[top_right][2] < before[top_right][2],
+ f"{before[top_left]} -> {after[top_left]}",
+ )
+ check(
+ "and the row boundary moved down",
+ after[top_left][3] > before[top_left][3],
+ f"{before[top_left]} -> {after[top_left]}",
+ )
# The two column boundaries are separate splits that happen to line up.
# Moving one without the other would put a step in one apparent line.
- check("both column boundaries moved together, so the line is still one",
- after[bot_left][2] == after[top_left][2],
- f"top {after[top_left]} bottom {after[bot_left]}")
+ check(
+ "both column boundaries moved together, so the line is still one",
+ after[bot_left][2] == after[top_left][2],
+ f"top {after[top_left]} bottom {after[bot_left]}",
+ )
os.unlink(l)
@@ -108,23 +124,28 @@ def test_resting_on_it_arms_both_boundaries():
s.settle(30)
c = corners(s)[0]
screen = s.snapshot().screen()
- check("nothing armed before the pointer rests",
- CROSS not in screen and "\u250a" not in screen, "")
+ check(
+ "nothing armed before the pointer rests",
+ CROSS not in screen and "\u250a" not in screen,
+ "",
+ )
s.send(rf"\e[<35;{c['x'] + 1};{c['y'] + 1}M")
time.sleep(0.35)
s.settle(60)
snap = s.snapshot()
- check("the crossing is marked", CROSS in snap.screen(),
- repr(snap.line(c["y"])))
+ check("the crossing is marked", CROSS in snap.screen(), repr(snap.line(c["y"])))
check("the column boundary is armed too", "\u250a" in snap.screen(), "")
check("and the row boundary", "\u2508" in snap.screen(), "")
s.send(rf"\e[<0;{c['x'] + 1};{c['y'] + 1}M")
s.send(rf"\e[<32;{c['x'] + 3};{c['y'] + 2}M")
s.settle(40)
- check("and it reads differently once you have hold of it",
- CROSS_ON in s.snapshot().screen(), repr(s.snapshot().screen()[:200]))
+ check(
+ "and it reads differently once you have hold of it",
+ CROSS_ON in s.snapshot().screen(),
+ repr(s.snapshot().screen()[:200]),
+ )
s.send(rf"\e[<0;{c['x'] + 3};{c['y'] + 2}m")
s.settle(20)
os.unlink(l)
@@ -137,8 +158,11 @@ def test_the_hint_says_both_ways():
c = corners(s)[0]
s.send(rf"\e[<35;{c['x'] + 1};{c['y'] + 1}M")
s.settle(30)
- check("the status line says what it would do",
- "both ways" in s.snapshot().text[-2], repr(s.snapshot().text[-2]))
+ check(
+ "the status line says what it would do",
+ "both ways" in s.snapshot().text[-2],
+ repr(s.snapshot().text[-2]),
+ )
os.unlink(l)
@@ -162,7 +186,7 @@ def test_a_drag_keeps_the_boundaries_it_grabbed():
w0 = {p["id"]: p["w"] for p in s.panes()}
c = corners(s)[0]
s.send(rf"\e[<0;{c['x'] + 1};{c['y'] + 1}M")
- for step in range(1, 9): # wander, so the layout keeps changing
+ for step in range(1, 9): # wander, so the layout keeps changing
s.send(rf"\e[<32;{c['x'] + 1 + step};{c['y'] + 1 + (step % 3)}M")
s.settle(15)
s.send(rf"\e[<0;{c['x'] + 9};{c['y'] + 3}m")
@@ -170,12 +194,16 @@ def test_a_drag_keeps_the_boundaries_it_grabbed():
w1 = {p["id"]: p["w"] for p in s.panes()}
top, bottom = ids[:2], ids[2:]
- check("the boundary it grabbed moved",
- [w1[i] for i in top] != [w0[i] for i in top],
- f"{[w0[i] for i in top]} -> {[w1[i] for i in top]}")
- check("and the one out of line with it was never picked up",
- [w1[i] for i in bottom] == [w0[i] for i in bottom],
- f"{[w0[i] for i in bottom]} -> {[w1[i] for i in bottom]}")
+ check(
+ "the boundary it grabbed moved",
+ [w1[i] for i in top] != [w0[i] for i in top],
+ f"{[w0[i] for i in top]} -> {[w1[i] for i in top]}",
+ )
+ check(
+ "and the one out of line with it was never picked up",
+ [w1[i] for i in bottom] == [w0[i] for i in bottom],
+ f"{[w0[i] for i in bottom]} -> {[w1[i] for i in bottom]}",
+ )
os.unlink(l)
@@ -185,11 +213,17 @@ def test_the_crossing_shows_itself_before_you_rest_on_it():
s.settle(30)
c = corners(s)[0]
s.send(rf"\e[<35;{c['x'] + 1};{c['y'] + 1}M")
- s.settle(40) # no dwell
- check("a crossing two cells wide says it is there straight away",
- CROSS in s.snapshot().screen(), repr(s.snapshot().line(c["y"])))
- check("without arming the boundaries yet",
- "\u250a" not in s.snapshot().screen(), "")
+ s.settle(40) # no dwell
+ check(
+ "a crossing two cells wide says it is there straight away",
+ CROSS in s.snapshot().screen(),
+ repr(s.snapshot().line(c["y"])),
+ )
+ check(
+ "without arming the boundaries yet",
+ "\u250a" not in s.snapshot().screen(),
+ "",
+ )
def test_boundaries_out_of_line_are_two_crossings():
@@ -210,12 +244,10 @@ def test_boundaries_out_of_line_are_two_crossings():
s.settle(30)
c = sorted(corners(s), key=lambda h: h["x"])
- check("moving one of them makes a second crossing", len(c) == 2,
- str(c))
+ check("moving one of them makes a second crossing", len(c) == 2, str(c))
if len(c) != 2:
return
- check("at the two different columns",
- c[0]["x"] != c[1]["x"], str(c))
+ check("at the two different columns", c[0]["x"] != c[1]["x"], str(c))
w0 = [p["w"] for p in s.panes()]
s.send(rf"\e[<0;{c[0]['x'] + 1};{c[0]['y'] + 1}M")
@@ -223,8 +255,11 @@ def test_boundaries_out_of_line_are_two_crossings():
s.send(rf"\e[<0;{c[0]['x'] + 5};{c[0]['y'] + 1}m")
s.settle(30)
w1 = [p["w"] for p in s.panes()]
- check("and each moves only the boundary that meets it",
- w1[:2] == w0[:2] and w1[2:] != w0[2:], f"{w0} -> {w1}")
+ check(
+ "and each moves only the boundary that meets it",
+ w1[:2] == w0[:2] and w1[2:] != w0[2:],
+ f"{w0} -> {w1}",
+ )
os.unlink(l)
@@ -249,9 +284,11 @@ def test_it_finds_the_crossing_whichever_way_the_tree_is_nested():
s.send(rf"\e[<0;{x + 5};{y + 3}m")
s.settle(30)
after = rects(s)
- check(f"{name}: dragging it moves both ways",
- all(before[i] != after[i] for i in before),
- f"{before} -> {after}")
+ check(
+ f"{name}: dragging it moves both ways",
+ all(before[i] != after[i] for i in before),
+ f"{before} -> {after}",
+ )
os.unlink(l)
diff --git a/tests/test_dead.py b/tests/test_dead.py
index 6569013..8416879 100644
--- a/tests/test_dead.py
+++ b/tests/test_dead.py
@@ -10,6 +10,7 @@ anything: run it again, or close it.
Buttons are found through the hit list rather than by column, because where
they land is the frame's business and not this test's.
"""
+
import sys
import tempfile
@@ -21,9 +22,11 @@ from harness import Session, check, report
SHELL = ["/bin/sh", "-c", 'printf "\\033]2;p\\007"; read x']
LIVES = SHELL
# The same, having painted a known non-default colour into its own content.
-GREEN = ["/bin/sh", "-c",
- 'printf "\\033]2;p\\007"; printf "\\033[38;2;0;255;0mBLOCK\\033[0m\\n"; '
- 'read x']
+GREEN = [
+ "/bin/sh",
+ "-c",
+ 'printf "\\033]2;p\\007"; printf "\\033[38;2;0;255;0mBLOCK\\033[0m\\n"; read x',
+]
DONE = "q\\n" # the line those two are waiting for
@@ -51,8 +54,7 @@ WAITS_CMD = "printf '\\033]2;w\\007'; read x"
def commanded_and_shell(command):
- return cfg('layout {\n tab {\n pane command="%s"\n pane\n }\n}\n'
- % command)
+ return cfg('layout {\n tab {\n pane command="%s"\n pane\n }\n}\n' % command)
def hit(snap, action):
@@ -82,8 +84,11 @@ def test_it_stays_and_says_why():
check("the pane is still there", len(s.panes()) == 1, str(s.panes()))
check("and the session with it", s.alive())
check("what it printed is still on screen", "mark-one" in snap.screen())
- check("the backlog says it exited",
- "[process exited: status 3]" in snap.screen(), snap.screen())
+ check(
+ "the backlog says it exited",
+ "[process exited: status 3]" in snap.screen(),
+ snap.screen(),
+ )
check("the frame says so too", "exited: status 3" in snap.screen())
check("json calls it dead", pane["alive"] is False, str(pane))
@@ -99,14 +104,20 @@ def test_it_says_what_it_ran():
snap = s.until_text("[process exited")
screen = snap.screen()
check("the backlog names the command", "[ran: " in screen, screen)
- check("and it is the command that was given",
- "echo mark-one; exit 3" in screen, screen)
+ check(
+ "and it is the command that was given",
+ "echo mark-one; exit 3" in screen,
+ screen,
+ )
# Above, not below: the exit line is the last word on the run.
lines = [i for i, l in enumerate(snap.text) if "[ran: " in l]
exits = [i for i, l in enumerate(snap.text) if "[process exited" in l]
- check("it sits above how it ended", lines and exits and lines[0] < exits[0],
- "%s vs %s" % (lines, exits))
+ check(
+ "it sits above how it ended",
+ lines and exits and lines[0] < exits[0],
+ "%s vs %s" % (lines, exits),
+ )
def test_a_pane_with_no_command_has_none_to_name():
@@ -116,10 +127,14 @@ def test_a_pane_with_no_command_has_none_to_name():
s.settle()
s.raw(DONE)
snap = s.until_text("[process exited")
- check("it still says it exited", "[process exited" in snap.screen(),
- snap.screen())
- check("and says nothing about a command", "[ran: " not in snap.screen(),
- snap.screen())
+ check(
+ "it still says it exited", "[process exited" in snap.screen(), snap.screen()
+ )
+ check(
+ "and says nothing about a command",
+ "[ran: " not in snap.screen(),
+ snap.screen(),
+ )
def test_each_run_says_what_it_was():
@@ -134,26 +149,31 @@ def test_each_run_says_what_it_was():
if snap.screen().count("[ran: ") >= 2:
break
s.settle(30)
- check("both runs are named", snap.screen().count("[ran: ") == 2,
- snap.screen())
- check("and both endings are", snap.screen().count("[process exited") == 2,
- snap.screen())
+ check("both runs are named", snap.screen().count("[ran: ") == 2, snap.screen())
+ check(
+ "and both endings are",
+ snap.screen().count("[process exited") == 2,
+ snap.screen(),
+ )
def test_a_clean_exit_is_not_dressed_up_as_a_failure():
with Session(SHELL, cols=60, rows=12, layout=commanded("true")) as s:
snap = s.until_text("[process exited")
- check("no status number for 0",
- "[process exited]" in snap.screen(), snap.screen())
+ check(
+ "no status number for 0", "[process exited]" in snap.screen(), snap.screen()
+ )
check("and the code is reported as 0", s.pane()["exit_code"] == 0)
def test_a_signal_is_named_as_one():
- with Session(SHELL, cols=60, rows=12,
- layout=commanded("kill -9 $$")) as s:
+ with Session(SHELL, cols=60, rows=12, layout=commanded("kill -9 $$")) as s:
snap = s.until_text("[process exited")
- check("the backlog says signal",
- "[process exited: signal 9]" in snap.screen(), snap.screen())
+ check(
+ "the backlog says signal",
+ "[process exited: signal 9]" in snap.screen(),
+ snap.screen(),
+ )
check("and so does json", s.pane()["exit_signal"] == 9, str(s.pane()))
@@ -170,22 +190,28 @@ def test_the_frame_offers_the_two_verbs_that_are_left():
check("re-run is clickable", rerun is not None, str(snap.hits))
check("close is clickable", close is not None, str(snap.hits))
# Rightmost is the one that must survive a narrow frame.
- check("close sits to the right of re-run", close[0] > rerun[0],
- f"{rerun} {close}")
+ check(
+ "close sits to the right of re-run", close[0] > rerun[0], f"{rerun} {close}"
+ )
- check("the button is drawn where its hit is",
- "[re-run]" in snap.line(rerun[1]), snap.line(rerun[1]))
+ check(
+ "the button is drawn where its hit is",
+ "[re-run]" in snap.line(rerun[1]),
+ snap.line(rerun[1]),
+ )
def test_a_live_pane_keeps_its_own_buttons():
"""The dead row replaces the pane's OSC buttons; it must not pre-empt them."""
- argv = ["/bin/sh", "-c",
- 'printf "\\033]5577;1;buttons;go:Go\\033\\\\"; stty raw -echo; cat']
+ argv = [
+ "/bin/sh",
+ "-c",
+ 'printf "\\033]5577;1;buttons;go:Go\\033\\\\"; stty raw -echo; cat',
+ ]
with Session(argv, cols=60, rows=12) as s:
snap = s.until_text("[Go]")
check("a live pane's own button is drawn", "[Go]" in snap.screen())
- check("and no dead buttons are", "[re-run]" not in snap.screen(),
- snap.screen())
+ check("and no dead buttons are", "[re-run]" not in snap.screen(), snap.screen())
def test_rerun_runs_it_again_on_top_of_what_it_left():
@@ -199,14 +225,19 @@ def test_rerun_runs_it_again_on_top_of_what_it_left():
snap = s.until(lambda sn: output_runs(sn) == 2)
check("the command ran again", output_runs(snap) == 2, snap.screen())
- check("the run that ended is still above it",
- "[process exited: status 3]" in snap.screen(), snap.screen())
+ check(
+ "the run that ended is still above it",
+ "[process exited: status 3]" in snap.screen(),
+ snap.screen(),
+ )
check("the pane kept its id", s.pane()["id"] == pane["id"])
snap = s.until(lambda sn: sn.screen().count("[process exited") == 2)
- check("and dying again says so again",
- snap.screen().count("[process exited: status 3]") == 2,
- snap.screen())
+ check(
+ "and dying again says so again",
+ snap.screen().count("[process exited: status 3]") == 2,
+ snap.screen(),
+ )
check("with the buttons back", "[re-run]" in snap.screen())
@@ -228,27 +259,28 @@ def test_the_keyboard_can_do_it_too():
s.until_text("[process exited")
s.key("r")
snap = s.until(lambda sn: output_runs(sn) == 2)
- check("C-a r runs it again", output_runs(snap) == 2,
- snap.screen())
+ check("C-a r runs it again", output_runs(snap) == 2, snap.screen())
def test_a_live_pane_is_not_restarted_under_you():
- """"Run it again" on something still running would mean killing it."""
+ """ "Run it again" on something still running would mean killing it."""
with Session(LIVES, cols=60, rows=12) as s:
s.until_text("p")
s.key("r")
s.settle()
snap = s.snapshot()
- check("it says no rather than doing it", "still running" in snap.screen(),
- snap.screen())
+ check(
+ "it says no rather than doing it",
+ "still running" in snap.screen(),
+ snap.screen(),
+ )
check("and the pane is untouched", s.pane()["alive"] is True)
def test_closing_it_is_what_takes_it_away():
# A commanded pane (its corpse is kept) beside a shell (which keeps the
# session alive once the first one is closed).
- with Session(SHELL, cols=80, rows=16,
- layout=commanded_and_shell(WAITS_CMD)) as s:
+ with Session(SHELL, cols=80, rows=16, layout=commanded_and_shell(WAITS_CMD)) as s:
s.until_text("w")
s.settle()
panes = s.panes()
@@ -279,23 +311,26 @@ def test_a_shell_you_split_off_just_goes_away():
check("two panes", len(s.panes()) == 2, str(s.panes()))
s.raw(DONE)
s.until(lambda sn: len(s.panes()) == 1)
- check("the one you finished with is gone", len(s.panes()) == 1,
- str(s.panes()))
- check("and left no notice behind",
- "[process exited" not in s.snapshot().screen(),
- s.snapshot().screen())
+ check("the one you finished with is gone", len(s.panes()) == 1, str(s.panes()))
+ check(
+ "and left no notice behind",
+ "[process exited" not in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
def test_a_commanded_pane_keeps_its_corpse():
"""The other half of the same rule, in one place so the pair is legible."""
- with Session(SHELL, cols=80, rows=16,
- layout=commanded_and_shell(WAITS_CMD)) as s:
+ with Session(SHELL, cols=80, rows=16, layout=commanded_and_shell(WAITS_CMD)) as s:
s.until_text("w")
s.api("focus", id=s.panes()[0]["id"])
s.raw(DONE)
snap = s.until(lambda sn: "[process exited" in sn.screen())
- check("a pane that was told to run something stays",
- len(s.panes()) == 2, str(s.panes()))
+ check(
+ "a pane that was told to run something stays",
+ len(s.panes()) == 2,
+ str(s.panes()),
+ )
check("and says so", "[process exited" in snap.screen(), snap.screen())
@@ -308,35 +343,41 @@ def test_keep_dead_all_keeps_shells_too():
s.raw(DONE)
snap = s.until(lambda sn: "[process exited" in sn.screen())
check("both panes are still there", len(s.panes()) == 2, str(s.panes()))
- check("the shell's corpse stayed", "[process exited" in snap.screen(),
- snap.screen())
+ check(
+ "the shell's corpse stayed",
+ "[process exited" in snap.screen(),
+ snap.screen(),
+ )
def test_keep_dead_none_keeps_nothing():
conf = cfg('keep_dead "none"\n')
- with Session(SHELL, cols=80, rows=16, config=conf,
- layout=commanded_and_shell(WAITS_CMD)) as s:
+ with Session(
+ SHELL, cols=80, rows=16, config=conf, layout=commanded_and_shell(WAITS_CMD)
+ ) as s:
s.until_text("w")
s.api("focus", id=s.panes()[0]["id"])
s.raw(DONE)
s.until(lambda sn: len(s.panes()) == 1)
- check("even a commanded pane is reaped", len(s.panes()) == 1,
- str(s.panes()))
+ check("even a commanded pane is reaped", len(s.panes()) == 1, str(s.panes()))
def test_true_and_false_still_mean_what_they_used_to():
"""`keep_dead true` shipped once. A config that still says it should keep
working rather than quietly changing behaviour."""
for word, want in (("true", 2), ("false", 1)):
- conf = cfg('keep_dead %s\n' % word)
+ conf = cfg("keep_dead %s\n" % word)
with Session(SHELL, cols=80, rows=16, config=conf) as s:
s.until_text("p")
s.key("\\\\")
s.settle()
s.raw(DONE)
s.until(lambda sn: len(s.panes()) == want)
- check(f"keep_dead {word} leaves {want} pane(s)",
- len(s.panes()) == want, str(s.panes()))
+ check(
+ f"keep_dead {word} leaves {want} pane(s)",
+ len(s.panes()) == want,
+ str(s.panes()),
+ )
def test_dying_recolours_what_the_pane_left_behind():
@@ -346,36 +387,38 @@ def test_dying_recolours_what_the_pane_left_behind():
# Doubled on purpose: this lands in a KDL string, whose unescaping turns
# `\\033` into `\033` for the shell. Written singly the shell gets `033[...`
# and prints it, which looks like the colour "not working".
- green_cmd = (r"printf '\\033]2;p\\007'; "
- r"printf '\\033[38;2;0;255;0mBLOCK\\033[0m\\n'; read x")
- with Session(SHELL, cols=60, rows=12,
- layout=commanded(green_cmd)) as s:
+ green_cmd = (
+ r"printf '\\033]2;p\\007'; "
+ r"printf '\\033[38;2;0;255;0mBLOCK\\033[0m\\n'; read x"
+ )
+ with Session(SHELL, cols=60, rows=12, layout=commanded(green_cmd)) as s:
snap = s.until_text("BLOCK")
x, y = snap.find("BLOCK")
live = snap.style_at(x, y)
- check("a live pane keeps the colour it printed",
- live and live.get("fg") == "#00ff00", str(live))
+ check(
+ "a live pane keeps the colour it printed",
+ live and live.get("fg") == "#00ff00",
+ str(live),
+ )
s.raw(DONE)
snap = s.until(lambda sn: "[process exited" in sn.screen())
x, y = snap.find("BLOCK")
dead = snap.style_at(x, y)
- check("a dead one does not",
- dead and dead.get("fg") != "#00ff00", str(dead))
+ check("a dead one does not", dead and dead.get("fg") != "#00ff00", str(dead))
def test_a_flattened_tab_still_says_which_pane_is_over():
"""A collapsed header is all a flattened tab draws of a pane, and the
shader pass does not reach one — so the words have to carry it."""
- with Session(SHELL, cols=100, rows=20,
- layout=commanded_and_shell(WAITS_CMD)) as s:
+ with Session(SHELL, cols=100, rows=20, layout=commanded_and_shell(WAITS_CMD)) as s:
s.until_text("w")
s.api("focus", id=s.panes()[0]["id"])
- s.raw(DONE) # the commanded one dies
+ s.raw(DONE) # the commanded one dies
s.until(lambda sn: "[process exited" in sn.screen())
- s.api("focus", id=s.panes()[1]["id"]) # focus the live one, so the
- s.settle() # dead one is what collapses
- s.resize(46, 14) # no room for two: the tab flattens
+ s.api("focus", id=s.panes()[1]["id"]) # focus the live one, so the
+ s.settle() # dead one is what collapses
+ s.resize(46, 14) # no room for two: the tab flattens
s.settle()
snap = s.snapshot()
@@ -386,9 +429,11 @@ def test_a_flattened_tab_still_says_which_pane_is_over():
def test_the_status_line_names_the_state():
with Session(SHELL, cols=60, rows=12, layout=commanded(DIES_CMD)) as s:
snap = s.until_text("[process exited")
- check("the focused dead pane is called exited",
- "exited: status 3" in snap.line(snap.rows - 2),
- snap.line(snap.rows - 2))
+ check(
+ "the focused dead pane is called exited",
+ "exited: status 3" in snap.line(snap.rows - 2),
+ snap.line(snap.rows - 2),
+ )
for name, fn in sorted(list(globals().items())):
diff --git a/tests/test_direct_keys.py b/tests/test_direct_keys.py
index f459061..51a2179 100644
--- a/tests/test_direct_keys.py
+++ b/tests/test_direct_keys.py
@@ -10,6 +10,7 @@ the part where that chord no longer reaches anything else.
So the deal is exactly this, and it is what these tests pin: a `direct` block
fires without the prefix, and what it fires on, nothing else sees.
"""
+
import sys
import tempfile
@@ -19,8 +20,8 @@ from harness import Session, check, report
# question the screen can answer.
SH = ["/bin/sh", "-c", 'printf "\\033]2;p\\007"; stty raw -echo; cat -v']
-C_M_RIGHT = r"\e[1;7C" # ctrl+alt+right
-C_M_LEFT = r"\e[1;7D" # ctrl+alt+left, deliberately left unbound
+C_M_RIGHT = r"\e[1;7C" # ctrl+alt+right
+C_M_LEFT = r"\e[1;7D" # ctrl+alt+left, deliberately left unbound
def cfg(text):
@@ -30,11 +31,9 @@ def cfg(text):
return f.name
-DIRECT = cfg('keys {\n'
- ' direct {\n'
- ' bind "ctrl+alt+right" "split-cols"\n'
- ' }\n'
- '}\n')
+DIRECT = cfg(
+ 'keys {\n direct {\n bind "ctrl+alt+right" "split-cols"\n }\n}\n'
+)
def test_it_fires_with_no_leader():
@@ -43,8 +42,9 @@ def test_it_fires_with_no_leader():
check("one pane to start", len(s.panes()) == 1)
s.send(C_M_RIGHT)
s.settle()
- check("the chord split the pane on its own", len(s.panes()) == 2,
- str(s.panes()))
+ check(
+ "the chord split the pane on its own", len(s.panes()) == 2, str(s.panes())
+ )
def test_what_it_fires_on_the_program_never_sees():
@@ -53,8 +53,11 @@ def test_what_it_fires_on_the_program_never_sees():
s.settle()
s.send(C_M_RIGHT)
s.settle(40)
- check("the pane was not sent the keystroke",
- "1;7C" not in s.snapshot().screen(), s.snapshot().screen()[:300])
+ check(
+ "the pane was not sent the keystroke",
+ "1;7C" not in s.snapshot().screen(),
+ s.snapshot().screen()[:300],
+ )
def test_everything_else_still_reaches_the_program():
@@ -62,8 +65,11 @@ def test_everything_else_still_reaches_the_program():
s.settle()
s.send(C_M_LEFT)
snap = s.until(lambda sn: "1;7D" in sn.screen())
- check("an unbound chord goes straight through",
- "1;7D" in snap.screen(), snap.screen()[:300])
+ check(
+ "an unbound chord goes straight through",
+ "1;7D" in snap.screen(),
+ snap.screen()[:300],
+ )
check("and did nothing else", len(s.panes()) == 1, str(s.panes()))
@@ -74,8 +80,11 @@ def test_it_also_works_after_the_leader():
s.settle()
s.send(r"\x01" + C_M_RIGHT)
s.settle()
- check("the leader then the chord does the same thing",
- len(s.panes()) == 2, str(s.panes()))
+ check(
+ "the leader then the chord does the same thing",
+ len(s.panes()) == 2,
+ str(s.panes()),
+ )
def test_prefixed_bindings_are_unaffected():
@@ -89,16 +98,18 @@ def test_prefixed_bindings_are_unaffected():
def test_the_leader_itself_cannot_be_stolen():
"""A direct binding on the prefix key would make the prefix unusable, and
the ordering has to guarantee it cannot."""
- conf = cfg('keys {\n'
- ' prefix "ctrl+a"\n'
- ' direct { bind "ctrl+a" "close-pane" }\n'
- '}\n')
+ conf = cfg(
+ 'keys {\n prefix "ctrl+a"\n direct { bind "ctrl+a" "close-pane" }\n}\n'
+ )
with Session(SH, cols=80, rows=14, config=conf) as s:
s.settle()
s.send(r"\x01")
s.settle()
- check("the prefix is still the prefix", "C-a" in s.snapshot().line(1),
- s.snapshot().line(1))
+ check(
+ "the prefix is still the prefix",
+ "C-a" in s.snapshot().line(1),
+ s.snapshot().line(1),
+ )
check("and the pane was not closed", len(s.panes()) == 1, str(s.panes()))
@@ -106,13 +117,15 @@ def test_an_overlay_still_owns_the_keyboard():
"""While a modal is up, a direct binding must not fire underneath it."""
with Session(SH, cols=92, rows=28, config=DIRECT) as s:
s.settle()
- s.send(r"\x01?") # the cheatsheet
+ s.send(r"\x01?") # the cheatsheet
s.settle()
s.send(C_M_RIGHT)
s.settle()
check("the modal swallowed it", len(s.panes()) == 1, str(s.panes()))
- check("and closed, as any key does",
- "split into columns" not in s.snapshot().screen())
+ check(
+ "and closed, as any key does",
+ "split into columns" not in s.snapshot().screen(),
+ )
def test_the_cheatsheet_lists_them_apart():
@@ -121,25 +134,29 @@ def test_the_cheatsheet_lists_them_apart():
s.send(r"\x01?")
s.settle()
screen = s.snapshot().screen()
- check("under a heading of their own",
- "without the leader" in screen, screen)
+ check("under a heading of their own", "without the leader" in screen, screen)
check("named the way you press them", "C-M-→" in screen, screen)
def test_none_unbinds_a_direct_binding():
- conf = cfg('keys {\n'
- ' direct {\n'
- ' bind "ctrl+alt+right" "split-cols"\n'
- ' bind "ctrl+alt+right" "none"\n'
- ' }\n'
- '}\n')
+ conf = cfg(
+ "keys {\n"
+ " direct {\n"
+ ' bind "ctrl+alt+right" "split-cols"\n'
+ ' bind "ctrl+alt+right" "none"\n'
+ " }\n"
+ "}\n"
+ )
with Session(SH, cols=80, rows=14, config=conf) as s:
s.settle()
s.send(C_M_RIGHT)
snap = s.until(lambda sn: "1;7C" in sn.screen())
check("it does not fire", len(s.panes()) == 1, str(s.panes()))
- check("and the chord goes back to the program", "1;7C" in snap.screen(),
- snap.screen()[:300])
+ check(
+ "and the chord goes back to the program",
+ "1;7C" in snap.screen(),
+ snap.screen()[:300],
+ )
for name, fn in sorted(list(globals().items())):
diff --git a/tests/test_docs.py b/tests/test_docs.py
index 12d208b..5c7c2bb 100755
--- a/tests/test_docs.py
+++ b/tests/test_docs.py
@@ -12,6 +12,7 @@ The Markdown subset: what it understands, it has to understand *exactly* --
notably that markup inside a code span is not markup, because half of these docs
are shader expressions full of asterisks.
"""
+
import html
import os
import re
@@ -31,8 +32,9 @@ def run(outdir, docs=None, expect_ok=True):
env = dict(os.environ)
if docs:
env["SL0PPTY_DOCS"] = docs
- r = subprocess.run([sys.executable, GEN, outdir], capture_output=True,
- text=True, env=env)
+ r = subprocess.run(
+ [sys.executable, GEN, outdir], capture_output=True, text=True, env=env
+ )
if expect_ok and r.returncode != 0:
check("gen-docs succeeds", False, r.stderr.strip() or r.stdout.strip())
return r
@@ -52,21 +54,29 @@ def test_it_renders_every_page_in_the_nav():
out = tempfile.mkdtemp()
run(out)
sections, pages = nav_pages()
- check("docs/nav has sections and pages", sections and len(pages) >= 8,
- f"{sections} / {pages}")
+ check(
+ "docs/nav has sections and pages",
+ sections and len(pages) >= 8,
+ f"{sections} / {pages}",
+ )
for md in pages:
f = os.path.join(out, md[:-3] + ".html")
check(f"{md} was rendered", os.path.exists(f), f)
- check("the stylesheet came with it",
- os.path.exists(os.path.join(out, "docs.css")))
+ check("the stylesheet came with it", os.path.exists(os.path.join(out, "docs.css")))
body = open(os.path.join(out, "keys.html")).read()
for md in pages:
- check(f"keys.html links to {md} in its sidebar",
- 'href="%s"' % (md[:-3] + ".html") in body, md)
- check("the current page is marked in the sidebar",
- 'class="here" href="keys.html"' in body, "no here marker")
+ check(
+ f"keys.html links to {md} in its sidebar",
+ 'href="%s"' % (md[:-3] + ".html") in body,
+ md,
+ )
+ check(
+ "the current page is marked in the sidebar",
+ 'class="here" href="keys.html"' in body,
+ "no here marker",
+ )
check("its section is the crumb", 'class="crumb">Using it' in body, body[:400])
@@ -93,12 +103,21 @@ def test_prev_and_next_chain_the_nav_order():
first = open(os.path.join(out, pages[0][:-3] + ".html")).read()
middle = open(os.path.join(out, pages[1][:-3] + ".html")).read()
last = open(os.path.join(out, pages[-1][:-3] + ".html")).read()
- check("the first page has a next and no prev",
- 'class="next"' in first and 'class="prev"' not in first, "first")
- check("a middle page has both",
- 'class="next"' in middle and 'class="prev"' in middle, "middle")
- check("the last page has a prev and no next",
- 'class="prev"' in last and 'class="next"' not in last, "last")
+ check(
+ "the first page has a next and no prev",
+ 'class="next"' in first and 'class="prev"' not in first,
+ "first",
+ )
+ check(
+ "a middle page has both",
+ 'class="next"' in middle and 'class="prev"' in middle,
+ "middle",
+ )
+ check(
+ "the last page has a prev and no next",
+ 'class="prev"' in last and 'class="next"' not in last,
+ "last",
+ )
def test_a_page_missing_from_the_nav_is_refused():
@@ -113,8 +132,11 @@ def test_a_page_missing_from_the_nav_is_refused():
f.write("# Orphan\n\nnobody links here\n")
r = run(tempfile.mkdtemp(), docs=docs, expect_ok=False)
check("an orphan page fails the build", r.returncode != 0, r.stdout)
- check("and it says which one", "orphan.md" in (r.stderr + r.stdout),
- r.stderr + r.stdout)
+ check(
+ "and it says which one",
+ "orphan.md" in (r.stderr + r.stdout),
+ r.stderr + r.stdout,
+ )
def test_a_nav_entry_with_no_file_is_refused():
@@ -125,8 +147,11 @@ def test_a_nav_entry_with_no_file_is_refused():
f.write("# Start\n\nhello\n")
r = run(tempfile.mkdtemp(), docs=docs, expect_ok=False)
check("a nav entry with no page fails the build", r.returncode != 0, r.stdout)
- check("and it says which one", "ghost.md" in (r.stderr + r.stdout),
- r.stderr + r.stdout)
+ check(
+ "and it says which one",
+ "ghost.md" in (r.stderr + r.stdout),
+ r.stderr + r.stdout,
+ )
def render_fixture(md):
@@ -171,18 +196,16 @@ shaders { dim amount="(y % 2) * 40" }
""")
cases = {
"the h1 is the title": "<title>Title</title>" in got,
- "headings get an id and an anchor":
- '<h2 id="a-heading">' in got and 'href="#a-heading"' in got,
+ "headings get an id and an anchor": '<h2 id="a-heading">' in got
+ and 'href="#a-heading"' in got,
"inline code": "<code>code</code>" in got,
"bold": "<strong>bold</strong>" in got,
"italic": "<em>italic</em>" in got,
- "a link to another page points at its html":
- 'href="keys.html#defaults"' in got,
+ "a link to another page points at its html": 'href="keys.html#defaults"' in got,
"lists nest": "<ul>" in got and "<li>nested</li>" in got,
"tables": "<table>" in got and "<th>a</th>" in got,
"a fenced block keeps its language": 'class="lang-kdl"' in got,
- "and its contents verbatim":
- html.escape('dim amount="(y % 2) * 40"') in got,
+ "and its contents verbatim": html.escape('dim amount="(y % 2) * 40"') in got,
"box drawing is marked as art, not code": 'class="art"' in got,
"blockquotes": "<blockquote>" in got,
"rules": "<hr>" in got,
@@ -195,11 +218,17 @@ def test_markup_inside_a_code_span_is_not_markup():
"""Half of these docs are shader expressions. A rule that formats the
asterisks inside them is a rule that mangles the documentation."""
got = render_fixture('# T\n\n`amount="(x < 10) * 200 ** 2"` and **bold**\n')
- check("asterisks in a code span survive",
- html.escape('(x < 10) * 200 ** 2') in got, got[:400])
+ check(
+ "asterisks in a code span survive",
+ html.escape("(x < 10) * 200 ** 2") in got,
+ got[:400],
+ )
check("bold outside it still works", "<strong>bold</strong>" in got, got[:400])
- check("and the span is not a strong tag",
- "<strong>" in got and got.count("<strong>") == 1, got[:400])
+ check(
+ "and the span is not a strong tag",
+ "<strong>" in got and got.count("<strong>") == 1,
+ got[:400],
+ )
def test_html_in_the_source_is_escaped():
diff --git a/tests/test_drag.py b/tests/test_drag.py
index f3bef62..e7964e4 100644
--- a/tests/test_drag.py
+++ b/tests/test_drag.py
@@ -5,13 +5,16 @@ Sizes are weights, so resizing is not a special case of anything: an even
split is equal weights, and every layout pass is still the same pure function
of the tree and the rect.
"""
+
import sys
import time
from harness import Session, check, report
+
def _cfg(text):
import tempfile
+
f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
f.write(text)
f.close()
@@ -20,8 +23,11 @@ def _cfg(text):
# The hint is gated on a real clock, so the wait is real - just a short one.
FAST = _cfg("hover_delay_ms 20\n")
+
+
def _cfg(text):
import tempfile
+
f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
f.write(text)
f.close()
@@ -44,14 +50,16 @@ def test_keyboard_resize():
s.key("\\\\")
s.settle()
before = widths(s)
- check("an even split starts even", abs(before[0] - before[1]) <= 1,
- str(before))
+ check("an even split starts even", abs(before[0] - before[1]) <= 1, str(before))
s.send(r"\x01L") # C-a shift-L: move the boundary right
s.settle()
after = widths(s)
- check("resize moves the boundary right",
- after[0] > before[0] and after[1] < before[1], f"{before} -> {after}")
+ check(
+ "resize moves the boundary right",
+ after[0] > before[0] and after[1] < before[1],
+ f"{before} -> {after}",
+ )
s.send(r"\x01H")
s.settle()
@@ -64,12 +72,19 @@ def test_keyboard_resize():
# It stops at min_pane rather than being pushed under it. Going under
# would put a pane below the floor, which collapses the whole tab into
# a list -- a drastic answer to "you nudged the divider once more".
- check("a pane is squeezed down to the floor and no further",
- squeezed[1] == 24, str(squeezed))
- check("and nothing collapsed on the way",
- not any(p["hidden"] for p in s.panes()), str(s.panes()))
- check("the total is still the screen",
- sum(squeezed) + 2 + 4 == 80, str(squeezed))
+ check(
+ "a pane is squeezed down to the floor and no further",
+ squeezed[1] == 24,
+ str(squeezed),
+ )
+ check(
+ "and nothing collapsed on the way",
+ not any(p["hidden"] for p in s.panes()),
+ str(s.panes()),
+ )
+ check(
+ "the total is still the screen", sum(squeezed) + 2 + 4 == 80, str(squeezed)
+ )
def test_vertical_resize():
@@ -81,14 +96,15 @@ def test_vertical_resize():
s.send(r"\x01J")
s.settle()
after = [p["h"] for p in s.panes()]
- check("resize works in rows too", after[0] > before[0],
- f"{before} -> {after}")
+ check("resize works in rows too", after[0] > before[0], f"{before} -> {after}")
s.send(r"\x01L") # no column split anywhere above this pane
s.settle()
- check("resizing against a boundary that does not exist is harmless",
- [p["h"] for p in s.panes()] == after and s.api("alive")["alive"],
- str(s.panes()))
+ check(
+ "resizing against a boundary that does not exist is harmless",
+ [p["h"] for p in s.panes()] == after and s.api("alive")["alive"],
+ str(s.panes()),
+ )
def test_resize_survives_a_collapse_cycle():
@@ -101,12 +117,15 @@ def test_resize_survives_a_collapse_cycle():
s.settle()
resized = widths(s)
- s.resize(40, 12) # collapses
+ s.resize(40, 12) # collapses
s.settle()
s.resize(100, 20) # and back
s.settle()
- check("weights survive a collapse/expand cycle", widths(s) == resized,
- f"{resized} -> {widths(s)}")
+ check(
+ "weights survive a collapse/expand cycle",
+ widths(s) == resized,
+ f"{resized} -> {widths(s)}",
+ )
def test_edge_drag():
@@ -120,8 +139,11 @@ def test_edge_drag():
gap_x = panes[0]["x"] + panes[0]["w"]
gap_y = panes[0]["y"] + 2
action = snap.hit_at(gap_x, gap_y)
- check("the gap between panes is a drag target",
- action and action.startswith("edge:"), str(action))
+ check(
+ "the gap between panes is a drag target",
+ action and action.startswith("edge:"),
+ str(action),
+ )
before = widths(s)
# press in the gap, move right, release
@@ -130,17 +152,22 @@ def test_edge_drag():
s.send(rf"\e[<0;{gap_x + 9};{gap_y + 1}m")
s.settle()
after = widths(s)
- check("dragging the gap right widens the left pane",
- after[0] > before[0] and after[1] < before[1],
- f"{before} -> {after}")
+ check(
+ "dragging the gap right widens the left pane",
+ after[0] > before[0] and after[1] < before[1],
+ f"{before} -> {after}",
+ )
# and back
s.send(rf"\e[<0;{after[0] + 3};{gap_y + 1}M")
s.send(rf"\e[<32;{after[0] - 5};{gap_y + 1}M")
s.send(rf"\e[<0;{after[0] - 5};{gap_y + 1}m")
s.settle()
- check("dragging it back narrows it again", widths(s)[0] < after[0],
- f"{after} -> {widths(s)}")
+ check(
+ "dragging it back narrows it again",
+ widths(s)[0] < after[0],
+ f"{after} -> {widths(s)}",
+ )
def test_title_drag_reorders():
@@ -157,9 +184,11 @@ def test_title_drag_reorders():
s.settle()
snap = s.snapshot()
- check("a pane title row is a drag handle",
- snap.hit_at(left["x"] + 4, left["y"]) == f"title:{left['id']}",
- str(snap.hit_at(left["x"] + 4, left["y"])))
+ check(
+ "a pane title row is a drag handle",
+ snap.hit_at(left["x"] + 4, left["y"]) == f"title:{left['id']}",
+ str(snap.hit_at(left["x"] + 4, left["y"])),
+ )
# drag the left pane's title onto the right pane
s.send(rf"\e[<0;{left['x'] + 5};{left['y'] + 1}M")
@@ -169,17 +198,23 @@ def test_title_drag_reorders():
s.settle()
panes = {p["id"]: p for p in s.panes()}
- check("the dragged pane took the target's place",
- panes[left["id"]]["x"] > panes[right["id"]]["x"],
- f"{panes[left['id']]['x']} vs {panes[right['id']]['x']}")
+ check(
+ "the dragged pane took the target's place",
+ panes[left["id"]]["x"] > panes[right["id"]]["x"],
+ f"{panes[left['id']]['x']} vs {panes[right['id']]['x']}",
+ )
snap = s.snapshot()
- check("and its content came with it",
- "LEFTMARK" in snap.pane_text(panes[left["id"]]),
- repr(snap.pane_text(panes[left["id"]])))
- check("the other pane's content did too",
- "RIGHTMARK" in snap.pane_text(panes[right["id"]]),
- repr(snap.pane_text(panes[right["id"]])))
+ check(
+ "and its content came with it",
+ "LEFTMARK" in snap.pane_text(panes[left["id"]]),
+ repr(snap.pane_text(panes[left["id"]])),
+ )
+ check(
+ "the other pane's content did too",
+ "RIGHTMARK" in snap.pane_text(panes[right["id"]]),
+ repr(snap.pane_text(panes[right["id"]])),
+ )
def test_drag_edge_cases():
@@ -198,17 +233,23 @@ def test_drag_edge_cases():
s.send(rf"\e[<32;{left['x'] + 5};{left['y'] + 1}M")
s.send(rf"\e[<0;{left['x'] + 5};{left['y'] + 1}m")
s.settle()
- check("a drag that returns to its origin changes nothing",
- [(p["id"], p["x"]) for p in s.panes()] == before, str(s.panes()))
+ check(
+ "a drag that returns to its origin changes nothing",
+ [(p["id"], p["x"]) for p in s.panes()] == before,
+ str(s.panes()),
+ )
# a drag released over nothing (the gap ring) must not swap or crash
s.send(rf"\e[<0;{left['x'] + 5};{left['y'] + 1}M")
s.send(r"\e[<32;1;1M")
s.send(r"\e[<0;1;1m")
s.settle()
- check("dropping outside any pane is harmless",
- [(p["id"], p["x"]) for p in s.panes()] == before
- and s.api("alive")["alive"], str(s.panes()))
+ check(
+ "dropping outside any pane is harmless",
+ [(p["id"], p["x"]) for p in s.panes()] == before
+ and s.api("alive")["alive"],
+ str(s.panes()),
+ )
# a press whose release never arrives must not wedge the mouse
s.send(rf"\e[<0;{left['x'] + 5};{left['y'] + 1}M")
@@ -219,8 +260,11 @@ def test_drag_edge_cases():
# and a border click still works, which is the proof it is not wedged
s.click(left["x"], left["y"] + 2)
s.settle()
- check("the mouse still works after a drag", len(s.panes()) == 3,
- str(len(s.panes())))
+ check(
+ "the mouse still works after a drag",
+ len(s.panes()) == 3,
+ str(len(s.panes())),
+ )
def test_drop_target_is_visible():
@@ -234,15 +278,18 @@ def test_drop_target_is_visible():
s.settle(60)
snap = s.snapshot()
run = snap.style_at(right["x"], right["y"])
- check("the drop target is highlighted while dragging",
- run and run["fg"] == "#ff5fd7" and "bold" in run["attrs"], str(run))
+ check(
+ "the drop target is highlighted while dragging",
+ run and run["fg"] == "#ff5fd7" and "bold" in run["attrs"],
+ str(run),
+ )
s.send(rf"\e[<0;{right['x'] + 5};{right['y'] + 3}m")
# ---- the resize zone announces itself --------------------------------------
-GRIP_V, GRAB_V = "\u250a", "\u2551" # dotted / doubled, vertical boundary
-GRIP_H, GRAB_H = "\u2508", "\u2550" # dotted / doubled, horizontal boundary
+GRIP_V, GRAB_V = "\u250a", "\u2551" # dotted / doubled, vertical boundary
+GRIP_H, GRAB_H = "\u2508", "\u2550" # dotted / doubled, horizontal boundary
ARROW_V, ARROW_H = "\u21d4", "\u21d5" # which way that boundary travels
@@ -262,19 +309,22 @@ def test_the_gap_says_it_is_a_handle():
# movement there is, so contact alone must not arm it.
s.send(rf"\e[<35;{gx + 1};{gy + 1}M")
s.settle(60)
- check("contact alone does not arm it",
- GRIP_V not in s.snapshot().screen(), "")
+ check("contact alone does not arm it", GRIP_V not in s.snapshot().screen(), "")
time.sleep(0.35)
s.settle(60)
snap = s.snapshot()
check("resting on it says it can be moved", GRIP_V in snap.screen(), "")
- check("and does not yet claim it is being moved",
- GRAB_V not in snap.screen(), "")
+ check(
+ "and does not yet claim it is being moved", GRAB_V not in snap.screen(), ""
+ )
pos = snap.find(GRIP_V)
- check("the hint is in the gap, not in either pane",
- pos and left["x"] + left["w"] <= pos[0] < right["x"], str(pos))
+ check(
+ "the hint is in the gap, not in either pane",
+ pos and left["x"] + left["w"] <= pos[0] < right["x"],
+ str(pos),
+ )
def test_the_hint_changes_while_resizing():
@@ -290,25 +340,34 @@ def test_the_hint_changes_while_resizing():
s.send(rf"\e[<32;{gx + 6};{gy + 1}M")
s.settle(80)
snap = s.snapshot()
- check("a resize in progress reads differently from a hover",
- GRAB_V in snap.screen() and GRIP_V not in snap.screen(), "")
+ check(
+ "a resize in progress reads differently from a hover",
+ GRAB_V in snap.screen() and GRIP_V not in snap.screen(),
+ "",
+ )
s.send(rf"\e[<0;{gx + 6};{gy + 1}m")
s.settle(60)
- check("and the boundary actually moved",
- s.panes()[0]["w"] > before, f'{before} -> {s.panes()[0]["w"]}')
+ check(
+ "and the boundary actually moved",
+ s.panes()[0]["w"] > before,
+ f"{before} -> {s.panes()[0]['w']}",
+ )
# Off the handle: still resting on it would (correctly) re-arm the
# dotted hint, which is the pointer following the mouse, not a leak.
- s.send(rf"\e[<35;{left["x"] + 3};{gy + 1}M")
+ s.send(rf"\e[<35;{left['x'] + 3};{gy + 1}M")
snap = s.snapshot()
- check("both hints are gone once it is dropped and left alone",
- GRAB_V not in snap.screen() and GRIP_V not in snap.screen(), "")
+ check(
+ "both hints are gone once it is dropped and left alone",
+ GRAB_V not in snap.screen() and GRIP_V not in snap.screen(),
+ "",
+ )
def test_a_horizontal_boundary_uses_horizontal_marks():
with Session(SH, cols=120, rows=26, config=FAST) as s:
s.settle()
- s.key("-") # split into rows
+ s.key("-") # split into rows
s.settle(200)
top, bottom = s.panes()
gx, gy = top["x"] + 6, top["y"] + top["h"]
@@ -317,11 +376,17 @@ def test_a_horizontal_boundary_uses_horizontal_marks():
time.sleep(0.06)
s.settle(60)
snap = s.snapshot()
- check("a row boundary hints along its own axis",
- GRIP_H in snap.screen() and GRIP_V not in snap.screen(), "")
+ check(
+ "a row boundary hints along its own axis",
+ GRIP_H in snap.screen() and GRIP_V not in snap.screen(),
+ "",
+ )
pos = snap.find(GRIP_H)
- check("in the gap between the two rows",
- pos and top["y"] + top["h"] <= pos[1] < bottom["y"], str(pos))
+ check(
+ "in the gap between the two rows",
+ pos and top["y"] + top["h"] <= pos[1] < bottom["y"],
+ str(pos),
+ )
def test_no_resize_hint_during_another_drag():
@@ -333,12 +398,15 @@ def test_no_resize_hint_during_another_drag():
left, right = s.panes()
gx, gy = left["x"] + left["w"], left["y"] + 3
- s.send(rf"\e[<0;{left['x'] + 5};{left['y'] + 1}M") # grab the title
- s.send(rf"\e[<32;{gx + 1};{gy + 1}M") # drag over the gap
+ s.send(rf"\e[<0;{left['x'] + 5};{left['y'] + 1}M") # grab the title
+ s.send(rf"\e[<32;{gx + 1};{gy + 1}M") # drag over the gap
time.sleep(0.06)
s.settle(80)
- check("a title drag over a gap arms nothing",
- GRIP_V not in s.snapshot().screen(), "")
+ check(
+ "a title drag over a gap arms nothing",
+ GRIP_V not in s.snapshot().screen(),
+ "",
+ )
s.send(rf"\e[<0;{gx + 1};{gy + 1}m")
s.settle(80)
@@ -356,27 +424,42 @@ def test_the_gap_names_the_verb():
time.sleep(0.06)
s.settle(60)
snap = s.snapshot()
- check("hovering a column boundary shows a sideways arrow",
- ARROW_V in snap.screen(), "")
+ check(
+ "hovering a column boundary shows a sideways arrow",
+ ARROW_V in snap.screen(),
+ "",
+ )
# A divider between columns is drawn vertically and travels sideways;
# the useful half of that is the travelling.
check("and not the up-down one", ARROW_H not in snap.screen(), "")
pos = snap.find(ARROW_V)
- check("the arrow sits in the middle of the boundary",
- pos and pos[1] == left["y"] + left["h"] // 2, str(pos))
- check("on the line itself, inside the gap",
- pos and left["x"] + left["w"] <= pos[0] < right["x"], str(pos))
+ check(
+ "the arrow sits in the middle of the boundary",
+ pos and pos[1] == left["y"] + left["h"] // 2,
+ str(pos),
+ )
+ check(
+ "on the line itself, inside the gap",
+ pos and left["x"] + left["w"] <= pos[0] < right["x"],
+ str(pos),
+ )
s.send(rf"\e[<0;{gx + 1};{gy + 1}M")
s.send(rf"\e[<32;{gx + 5};{gy + 1}M")
s.settle(80)
- check("it stays while the boundary is actually moving",
- ARROW_V in s.snapshot().screen(), "")
+ check(
+ "it stays while the boundary is actually moving",
+ ARROW_V in s.snapshot().screen(),
+ "",
+ )
s.send(rf"\e[<0;{gx + 5};{gy + 1}m")
- s.send(rf"\e[<35;{left["x"] + 3};{gy + 1}M") # and off the handle
- check("and goes with the rest of the hint",
- ARROW_V not in s.snapshot().screen(), "")
+ s.send(rf"\e[<35;{left['x'] + 3};{gy + 1}M") # and off the handle
+ check(
+ "and goes with the rest of the hint",
+ ARROW_V not in s.snapshot().screen(),
+ "",
+ )
def test_a_row_boundary_names_its_own_verb():
@@ -391,11 +474,17 @@ def test_a_row_boundary_names_its_own_verb():
time.sleep(0.06)
s.settle(60)
snap = s.snapshot()
- check("a row boundary shows an up-down arrow",
- ARROW_H in snap.screen() and ARROW_V not in snap.screen(), "")
+ check(
+ "a row boundary shows an up-down arrow",
+ ARROW_H in snap.screen() and ARROW_V not in snap.screen(),
+ "",
+ )
pos = snap.find(ARROW_H)
- check("in the gap between the rows",
- pos and top["y"] + top["h"] <= pos[1] < bottom["y"], str(pos))
+ check(
+ "in the gap between the rows",
+ pos and top["y"] + top["h"] <= pos[1] < bottom["y"],
+ str(pos),
+ )
def test_dragging_a_boundary_also_stops_at_the_floor():
@@ -415,10 +504,16 @@ def test_dragging_a_boundary_also_stops_at_the_floor():
s.settle(60)
panes = s.panes()
- check("the dragged-against pane stops at the floor",
- panes[1]["w"] == 24, str([p["w"] for p in panes]))
- check("and the tab did not collapse into a list",
- not any(p["hidden"] for p in panes), str(panes))
+ check(
+ "the dragged-against pane stops at the floor",
+ panes[1]["w"] == 24,
+ str([p["w"] for p in panes]),
+ )
+ check(
+ "and the tab did not collapse into a list",
+ not any(p["hidden"] for p in panes),
+ str(panes),
+ )
if __name__ == "__main__":
diff --git a/tests/test_dump_config.py b/tests/test_dump_config.py
index b263523..5d38dc1 100644
--- a/tests/test_dump_config.py
+++ b/tests/test_dump_config.py
@@ -11,6 +11,7 @@ knows must appear in it. The second is the one that catches "added a knob,
forgot to render it", which is the failure this whole approach exists to
prevent.
"""
+
import os
import re
import subprocess
@@ -26,8 +27,9 @@ SRC = os.path.join(HERE, "..", "src", "config.c")
def dump(config=None):
env = dict(os.environ)
env["SL0PPTY_CONFIG"] = config or "/nonexistent/sl0ppty.kdl"
- return subprocess.run([BIN, "--dump-config"], capture_output=True,
- text=True, env=env).stdout
+ return subprocess.run(
+ [BIN, "--dump-config"], capture_output=True, text=True, env=env
+ ).stdout
def test_it_dumps_something_that_looks_like_a_config():
@@ -46,9 +48,16 @@ def test_it_parses_back_to_exactly_itself():
f.write(one)
f.close()
two = dump(f.name)
- check("a dump of a dump is the same dump", one == two,
- "\n".join(l for l in __import__("difflib").unified_diff(
- one.splitlines(), two.splitlines(), "first", "second", lineterm=""))[:1500])
+ check(
+ "a dump of a dump is the same dump",
+ one == two,
+ "\n".join(
+ l
+ for l in __import__("difflib").unified_diff(
+ one.splitlines(), two.splitlines(), "first", "second", lineterm=""
+ )
+ )[:1500],
+ )
os.unlink(f.name)
@@ -65,9 +74,14 @@ def test_a_value_with_a_quote_in_it_is_written_escaped():
check("word_separators is rendered", len(line) == 1, str(line))
if line:
value = line[0].split(None, 1)[1]
- check("its quote is escaped", value.startswith('"' + chr(92) + '"'), repr(value))
- check("and the line is one KDL string, not two",
- value.count('"') - value.count(chr(92) + '"') == 2, repr(value))
+ check(
+ "its quote is escaped", value.startswith('"' + chr(92) + '"'), repr(value)
+ )
+ check(
+ "and the line is one KDL string, not two",
+ value.count('"') - value.count(chr(92) + '"') == 2,
+ repr(value),
+ )
def test_every_key_the_parser_knows_is_in_it():
@@ -78,10 +92,14 @@ def test_every_key_the_parser_knows_is_in_it():
# These are containers whose contents are rendered, not scalars.
keys -= {"keys"}
text = dump()
- missing = sorted(k for k in keys
- if not re.search(r'^(// )?%s[ {"]' % re.escape(k), text, re.M))
- check(f"all {len(keys)} settings are rendered", not missing,
- "missing: " + ", ".join(missing))
+ missing = sorted(
+ k for k in keys if not re.search(r'^(// )?%s[ {"]' % re.escape(k), text, re.M)
+ )
+ check(
+ f"all {len(keys)} settings are rendered",
+ not missing,
+ "missing: " + ", ".join(missing),
+ )
def test_every_theme_colour_is_in_it():
@@ -89,8 +107,11 @@ def test_every_theme_colour_is_in_it():
colours = set(re.findall(r'\{"([a-z_]+)", offsetof\(config_t', src))
text = dump()
missing = sorted(c for c in colours if f" {c} " not in text)
- check(f"all {len(colours)} colours are rendered", not missing,
- "missing: " + ", ".join(missing))
+ check(
+ f"all {len(colours)} colours are rendered",
+ not missing,
+ "missing: " + ", ".join(missing),
+ )
check("and there are plenty of them", len(colours) > 40, str(len(colours)))
@@ -107,9 +128,11 @@ def test_the_dump_is_a_working_config():
with Session(sh, cols=70, rows=14) as s:
s.settle(20)
with_none = s.snapshot().screen()
- check("a session run from the dump looks like one run from nothing",
- with_dump == with_none,
- "\n--- from the dump\n%s\n--- from nothing\n%s" % (with_dump, with_none))
+ check(
+ "a session run from the dump looks like one run from nothing",
+ with_dump == with_none,
+ "\n--- from the dump\n%s\n--- from nothing\n%s" % (with_dump, with_none),
+ )
# ...and it loads without a word of complaint, which the screen alone could
# not tell you: for a long time the dump wrote every chord in the
@@ -120,8 +143,11 @@ def test_the_dump_is_a_working_config():
with Session(sh, cols=70, rows=14, config=f.name) as s:
s.settle(20)
reply = s.api("reload")
- check("and it loads with nothing to complain about",
- reply.get("ok") and "warning" not in reply, str(reply))
+ check(
+ "and it loads with nothing to complain about",
+ reply.get("ok") and "warning" not in reply,
+ str(reply),
+ )
os.unlink(f.name)
@@ -131,21 +157,32 @@ def test_edit_config_writes_it_when_there_is_none():
home = tempfile.mkdtemp(prefix="sl0ppty-home-")
sh = ["/bin/sh", "-c", 'printf "\\033]2;p\\007"; read x']
# No config, and no ~/.config either: a fresh container has neither.
- with Session(sh, cols=74, rows=24, env={"HOME": home, "EDITOR": "tail -f"},
- config=os.path.join(home, ".config", "sl0ppty", "config.kdl")) as s:
+ with Session(
+ sh,
+ cols=74,
+ rows=24,
+ env={"HOME": home, "EDITOR": "tail -f"},
+ config=os.path.join(home, ".config", "sl0ppty", "config.kdl"),
+ ) as s:
s.settle()
s.key("e")
# `tail -f` shows the *end* of the file, so wait for something that is
# actually on screen rather than for the first thing in the config.
snap = s.until_text("select-tab-9")
- check("the editor pane has the config in it",
- "select-tab-9" in snap.screen(), snap.screen()[-400:])
+ check(
+ "the editor pane has the config in it",
+ "select-tab-9" in snap.screen(),
+ snap.screen()[-400:],
+ )
path = os.path.join(home, ".config", "sl0ppty", "config.kdl")
check("and it was written, directories and all", os.path.exists(path), path)
if os.path.exists(path):
- check("with the real defaults in it", "theme {" in open(path).read(),
- open(path).read()[:200])
+ check(
+ "with the real defaults in it",
+ "theme {" in open(path).read(),
+ open(path).read()[:200],
+ )
def test_it_never_overwrites_a_config_you_already_have():
@@ -153,13 +190,17 @@ def test_it_never_overwrites_a_config_you_already_have():
path = os.path.join(home, "config.kdl")
open(path, "w").write("// mine\ngap 3\n")
sh = ["/bin/sh", "-c", 'printf "\\033]2;p\\007"; read x']
- with Session(sh, cols=74, rows=24, config=path,
- env={"HOME": home, "EDITOR": "tail -f"}) as s:
+ with Session(
+ sh, cols=74, rows=24, config=path, env={"HOME": home, "EDITOR": "tail -f"}
+ ) as s:
s.settle()
s.key("e")
s.settle(60)
- check("the file is untouched", open(path).read() == "// mine\ngap 3\n",
- open(path).read())
+ check(
+ "the file is untouched",
+ open(path).read() == "// mine\ngap 3\n",
+ open(path).read(),
+ )
for name, fn in sorted(list(globals().items())):
diff --git a/tests/test_dump_layout.py b/tests/test_dump_layout.py
index 5c3a5f3..2d64da2 100644
--- a/tests/test_dump_layout.py
+++ b/tests/test_dump_layout.py
@@ -9,6 +9,7 @@ right and rebuilds a different tree is exactly the failure this would have —
the first version wrote `split=` on the children instead of on the node that
has them, which loaded without complaint and produced the wrong shape.
"""
+
import os
import sys
import tempfile
@@ -42,8 +43,19 @@ def write(text, suffix=".kdl"):
def shape(s):
"""The structure, as something two sessions can be compared on."""
- return [(p["tab"], p["x"], p["y"], p["w"], p["h"], p["purpose"],
- p["suspended"], p["focused"]) for p in s.panes()]
+ return [
+ (
+ p["tab"],
+ p["x"],
+ p["y"],
+ p["w"],
+ p["h"],
+ p["purpose"],
+ p["suspended"],
+ p["focused"],
+ )
+ for p in s.panes()
+ ]
def test_a_dump_reloads_into_the_same_session():
@@ -63,8 +75,11 @@ def test_a_dump_reloads_into_the_same_session():
second.settle(60)
after = shape(second)
- check("the same panes, in the same places", before == after,
- "\n before %s\n after %s" % (before, after))
+ check(
+ "the same panes, in the same places",
+ before == after,
+ "\n before %s\n after %s" % (before, after),
+ )
os.unlink(lay)
os.unlink(again)
@@ -81,8 +96,11 @@ def test_dumping_the_reload_gives_the_same_text():
with Session(SH, cols=90, rows=20, layout=again) as second:
second.settle(60)
two = second.api("dump-layout")["kdl"]
- check("a dump of a dump is the same dump", one == two,
- "\n--- first\n%s\n--- second\n%s" % (one, two))
+ check(
+ "a dump of a dump is the same dump",
+ one == two,
+ "\n--- first\n%s\n--- second\n%s" % (one, two),
+ )
os.unlink(lay)
os.unlink(again)
@@ -103,7 +121,7 @@ def test_proportions_and_focus_come_back():
s.key("\\\\")
s.settle()
for _ in range(3):
- s.send(r"\x01L") # push the boundary well off centre
+ s.send(r"\x01L") # push the boundary well off centre
s.settle()
widths = [p["w"] for p in s.panes()]
focused = [i for i, p in enumerate(s.panes()) if p["focused"]][0]
@@ -114,11 +132,16 @@ def test_proportions_and_focus_come_back():
lay = write(dumped)
with Session(SH, cols=100, rows=20, layout=lay) as s2:
s2.settle(60)
- check("the proportions survive", [p["w"] for p in s2.panes()] == widths,
- f"{[p['w'] for p in s2.panes()]} vs {widths}")
- check("and so does the focus",
- [i for i, p in enumerate(s2.panes()) if p["focused"]] == [focused],
- str([p["focused"] for p in s2.panes()]))
+ check(
+ "the proportions survive",
+ [p["w"] for p in s2.panes()] == widths,
+ f"{[p['w'] for p in s2.panes()]} vs {widths}",
+ )
+ check(
+ "and so does the focus",
+ [i for i, p in enumerate(s2.panes()) if p["focused"]] == [focused],
+ str([p["focused"] for p in s2.panes()]),
+ )
os.unlink(lay)
@@ -126,18 +149,19 @@ def test_the_active_tab_survives():
lay = write(LAYOUT)
with Session(SH, cols=90, rows=20, layout=lay) as s:
s.settle(60)
- s.key(r"\t") # C-a tab: to the second tab
+ s.key(r"\t") # C-a tab: to the second tab
s.settle()
was = [t["index"] for t in s.tabs() if t["active"]]
dumped = s.api("dump-layout")["kdl"]
- check("the dump says which tab was active", "active=true" in dumped,
- dumped)
+ check("the dump says which tab was active", "active=true" in dumped, dumped)
again = write(dumped)
with Session(SH, cols=90, rows=20, layout=again) as s2:
s2.settle(60)
- check("and it comes back on that tab",
- [t["index"] for t in s2.tabs() if t["active"]] == was,
- str(s2.tabs()))
+ check(
+ "and it comes back on that tab",
+ [t["index"] for t in s2.tabs() if t["active"]] == was,
+ str(s2.tabs()),
+ )
os.unlink(lay)
os.unlink(again)
diff --git a/tests/test_edit_config.py b/tests/test_edit_config.py
index 5c216bd..eac30f9 100644
--- a/tests/test_edit_config.py
+++ b/tests/test_edit_config.py
@@ -9,6 +9,7 @@ workflow the config watcher exists for.
The pane is *ephemeral*: quit the editor and it is gone. You asked for an
editor, not for a record of having had one.
"""
+
import os
import sys
import tempfile
@@ -31,15 +32,17 @@ def conf(text='shell "x"\n'):
def test_it_opens_the_config_in_a_pane():
c = conf('shell "a-marker-only-this-test-writes"\n')
- with Session(SH, cols=70, rows=24, config=c,
- env={"EDITOR": SHOWS}) as s:
+ with Session(SH, cols=70, rows=24, config=c, env={"EDITOR": SHOWS}) as s:
s.settle()
check("one pane to start", len(s.panes()) == 1)
s.key("e")
snap = s.until_text("a-marker-only-this-test-writes")
check("a second pane opened", len(s.panes()) == 2, str(s.panes()))
- check("with the config file in it",
- "a-marker-only-this-test-writes" in snap.screen(), snap.screen())
+ check(
+ "with the config file in it",
+ "a-marker-only-this-test-writes" in snap.screen(),
+ snap.screen(),
+ )
os.unlink(c)
@@ -53,9 +56,11 @@ def test_the_editor_pane_goes_when_the_editor_does():
s.raw("q\\n")
s.until(lambda _: len(s.panes()) == 1)
check("quitting it closes the pane", len(s.panes()) == 1, str(s.panes()))
- check("and leaves no corpse to dismiss",
- "[process exited" not in s.snapshot().screen(),
- s.snapshot().screen())
+ check(
+ "and leaves no corpse to dismiss",
+ "[process exited" not in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
os.unlink(c)
@@ -63,13 +68,17 @@ def test_the_config_can_name_the_editor():
"""`editor` beats $EDITOR, for the person whose $EDITOR is something they
do not want a terminal pane to run."""
c = conf('editor "tail -f"\nshell "picked-by-the-config"\n')
- with Session(SH, cols=70, rows=24, config=c,
- env={"EDITOR": "definitely-not-an-editor"}) as s:
+ with Session(
+ SH, cols=70, rows=24, config=c, env={"EDITOR": "definitely-not-an-editor"}
+ ) as s:
s.settle()
s.key("e")
snap = s.until_text("picked-by-the-config")
- check("the config's editor is used",
- "picked-by-the-config" in snap.screen(), snap.screen())
+ check(
+ "the config's editor is used",
+ "picked-by-the-config" in snap.screen(),
+ snap.screen(),
+ )
os.unlink(c)
@@ -84,8 +93,11 @@ def test_it_is_not_written_into_a_dumped_layout():
s.settle(60)
check("the editor is up", len(s.panes()) == 2, str(s.panes()))
dumped = s.api("dump-layout")["kdl"]
- check("the dump does not carry the editor command",
- "tail -f" not in dumped, dumped)
+ check(
+ "the dump does not carry the editor command",
+ "tail -f" not in dumped,
+ dumped,
+ )
os.unlink(c)
@@ -96,8 +108,9 @@ def test_no_room_says_so_rather_than_splitting_anyway():
s.key("e")
s.settle(40)
check("still one pane", len(s.panes()) == 1, str(s.panes()))
- check("and it said why", "no room" in s.snapshot().screen(),
- s.snapshot().screen())
+ check(
+ "and it said why", "no room" in s.snapshot().screen(), s.snapshot().screen()
+ )
os.unlink(c)
diff --git a/tests/test_equalize.py b/tests/test_equalize.py
index 675d468..dca3aa8 100755
--- a/tests/test_equalize.py
+++ b/tests/test_equalize.py
@@ -8,6 +8,7 @@ two is a third of the width and not half of it. Weighting per child would make
"even" mean something different at every depth, which is the layout this action
exists to get away from.
"""
+
import sys
from harness import Session, check, report
@@ -18,8 +19,7 @@ SH = ["/bin/sh", "-c", "stty raw -echo; cat"]
def by_pos(s):
"""Panes on screen, left to right then top to bottom. A minimised pane gets
no rect at all — the strip draws it — so it is not one of these."""
- return sorted((p for p in s.panes() if p["w"] > 1),
- key=lambda p: (p["x"], p["y"]))
+ return sorted((p for p in s.panes() if p["w"] > 1), key=lambda p: (p["x"], p["y"]))
def widths(s):
@@ -33,21 +33,22 @@ def test_a_nudged_row_of_columns_goes_back_to_even():
s.key("\\\\")
s.settle()
even = widths(s)
- check("three columns start out even", max(even) - min(even) <= 1,
- str(even))
+ check("three columns start out even", max(even) - min(even) <= 1, str(even))
for _ in range(4):
s.key("L") # shift+l: move the boundary right
s.settle()
nudged = widths(s)
- check("nudging the boundary makes them uneven",
- max(nudged) - min(nudged) > 2, str(nudged))
+ check(
+ "nudging the boundary makes them uneven",
+ max(nudged) - min(nudged) > 2,
+ str(nudged),
+ )
s.key("=")
s.settle()
back = widths(s)
- check("equalize puts them back to even",
- max(back) - min(back) <= 1, str(back))
+ check("equalize puts them back to even", max(back) - min(back) <= 1, str(back))
check("and it kept every pane", len(back) == 3, str(back))
@@ -55,27 +56,39 @@ def test_it_divides_a_split_between_the_panes_behind_it():
"""A pane beside a stack of two: a third of the width, not half of it."""
with Session(SH, cols=120, rows=30) as s:
s.settle()
- s.key("\\\\") # left | right
- s.key("-") # right becomes right-top / right-bottom
+ s.key("\\\\") # left | right
+ s.key("-") # right becomes right-top / right-bottom
s.settle()
left, top, bottom = by_pos(s)
- check("the split starts out halved per child",
- abs(left["w"] - top["w"]) <= 1,
- f"{left['w']} vs {top['w']}")
+ check(
+ "the split starts out halved per child",
+ abs(left["w"] - top["w"]) <= 1,
+ f"{left['w']} vs {top['w']}",
+ )
s.key("=")
s.settle()
left, top, bottom = by_pos(s)
- check("the stack of two gets twice the lone pane's width",
- abs(top["w"] - 2 * left["w"]) <= 2,
- f"{left['w']} vs {top['w']}")
- check("the two panes sharing that width still match",
- top["w"] == bottom["w"], f"{top['w']} vs {bottom['w']}")
- check("and they still split their column in half",
- abs(top["h"] - bottom["h"]) <= 1,
- f"{top['h']} vs {bottom['h']}")
- check("the lone pane still spans the tab",
- left["h"] > top["h"], f"{left['h']} vs {top['h']}")
+ check(
+ "the stack of two gets twice the lone pane's width",
+ abs(top["w"] - 2 * left["w"]) <= 2,
+ f"{left['w']} vs {top['w']}",
+ )
+ check(
+ "the two panes sharing that width still match",
+ top["w"] == bottom["w"],
+ f"{top['w']} vs {bottom['w']}",
+ )
+ check(
+ "and they still split their column in half",
+ abs(top["h"] - bottom["h"]) <= 1,
+ f"{top['h']} vs {bottom['h']}",
+ )
+ check(
+ "the lone pane still spans the tab",
+ left["h"] > top["h"],
+ f"{left['h']} vs {top['h']}",
+ )
def test_a_minimised_pane_is_not_counted():
@@ -90,14 +103,16 @@ def test_a_minimised_pane_is_not_counted():
s.api("focus", id=top["id"])
s.key("m")
s.settle()
- check("one pane is away in the strip", len(widths(s)) == 2,
- str(widths(s)))
+ check("one pane is away in the strip", len(widths(s)) == 2, str(widths(s)))
s.key("=")
s.settle()
seen = widths(s)
- check("the two on screen share the tab evenly",
- max(seen) - min(seen) <= 1, str(seen))
+ check(
+ "the two on screen share the tab evenly",
+ max(seen) - min(seen) <= 1,
+ str(seen),
+ )
def test_one_pane_has_nothing_to_even_out():
@@ -107,13 +122,18 @@ def test_one_pane_has_nothing_to_even_out():
s.key("=")
s.settle()
snap = s.snapshot()
- check("it says so rather than doing nothing quietly",
- "nothing to even out" in snap.screen(), repr(snap.screen()[-200:]))
+ check(
+ "it says so rather than doing nothing quietly",
+ "nothing to even out" in snap.screen(),
+ repr(snap.screen()[-200:]),
+ )
after = s.pane()
- check("and the pane is untouched",
- (after["x"], after["y"], after["w"], after["h"]) ==
- (before["x"], before["y"], before["w"], before["h"]),
- str((before, after)))
+ check(
+ "and the pane is untouched",
+ (after["x"], after["y"], after["w"], after["h"])
+ == (before["x"], before["y"], before["w"], before["h"]),
+ str((before, after)),
+ )
def test_it_is_in_the_palette_and_the_cheatsheet():
@@ -124,22 +144,32 @@ def test_it_is_in_the_palette_and_the_cheatsheet():
s.key("?")
s.settle()
sheet = s.snapshot().screen()
- check("the cheatsheet lists it under size, with its key",
- "= even out every split" in sheet, repr(sheet[:400]))
+ check(
+ "the cheatsheet lists it under size, with its key",
+ "= even out every split" in sheet,
+ repr(sheet[:400]),
+ )
s.send("q") # any key closes the sheet
s.key("p")
s.send("equalize")
s.settle()
pal = s.snapshot()
- check("the palette finds it by its config name",
- "even out every split" in pal.screen(),
- repr(pal.screen()[:400]))
- check("with the key it would have skipped", "C-a =" in pal.screen(),
- repr(pal.screen()[:400]))
- check("and the row runs it",
- any(h["action"].startswith("run:") for h in pal.hits),
- str([h["action"] for h in pal.hits][:8]))
+ check(
+ "the palette finds it by its config name",
+ "even out every split" in pal.screen(),
+ repr(pal.screen()[:400]),
+ )
+ check(
+ "with the key it would have skipped",
+ "C-a =" in pal.screen(),
+ repr(pal.screen()[:400]),
+ )
+ check(
+ "and the row runs it",
+ any(h["action"].startswith("run:") for h in pal.hits),
+ str([h["action"] for h in pal.hits][:8]),
+ )
if __name__ == "__main__":
diff --git a/tests/test_finder.py b/tests/test_finder.py
index 944597f..6d3e80e 100644
--- a/tests/test_finder.py
+++ b/tests/test_finder.py
@@ -4,6 +4,7 @@
Tabs stop being navigation somewhere around six projects; the finder is what
replaces them at that scale.
"""
+
import sys
from harness import Session, check, report
@@ -23,23 +24,32 @@ def test_status_bar():
btn = [h for h in snap.hits if h["action"] == "newtab"]
check("the bar shows a new-tab button", len(btn) == 1, str(snap.hits))
x = btn[0]["x"] + 1
- check("the new-tab button is clickable", snap.hit_at(x, 1) == "newtab",
- str(snap.hit_at(x, 1)))
+ check(
+ "the new-tab button is clickable",
+ snap.hit_at(x, 1) == "newtab",
+ str(snap.hit_at(x, 1)),
+ )
s.click(x, 1)
s.settle()
check("clicking + makes a tab", len(s.tabs()) == 2, str(s.tabs()))
s.key("\\\\")
s.settle()
- check("the pane count follows", "3 panes" in s.snapshot().line(1),
- repr(s.snapshot().line(1)))
+ check(
+ "the pane count follows",
+ "3 panes" in s.snapshot().line(1),
+ repr(s.snapshot().line(1)),
+ )
with Session(SH, cols=70, rows=14) as s:
s.settle()
s.send(r"\x01") # prefix, and then nothing
s.settle(60)
- check("a held prefix is shown as a mode", "C-a" in s.snapshot().line(1),
- repr(s.snapshot().line(1)))
+ check(
+ "a held prefix is shown as a mode",
+ "C-a" in s.snapshot().line(1),
+ repr(s.snapshot().line(1)),
+ )
s.send("q") # release it without quitting the session... 'q' quits
s.settle(60)
@@ -62,13 +72,15 @@ def setup_panes(s):
# got a title instead of a "find:" prompt.
def is_open(s):
snap = s.snapshot()
- return any(h["action"].startswith("find:") or h["action"] == "closefind"
- for h in snap.hits)
+ return any(
+ h["action"].startswith("find:") or h["action"] == "closefind" for h in snap.hits
+ )
def counter(s):
"""(selected, total) from the footer, or None when it is not shown."""
import re
+
for line in s.snapshot().screen().split("\n"):
m = re.search(r"(\d+) of (\d+)", line)
if m:
@@ -94,24 +106,31 @@ def test_finder_keyboard():
s.settle(80)
snap = s.snapshot()
check("the finder opens", is_open(s), repr(snap.screen()[:200]))
- check("it lists panes from every tab",
- "agent:main" in snap.screen() and "service:web" in snap.screen(),
- repr(snap.screen()))
+ check(
+ "it lists panes from every tab",
+ "agent:main" in snap.screen() and "service:web" in snap.screen(),
+ repr(snap.screen()),
+ )
check("and says how many it found", counter(s) == (1, 2), str(counter(s)))
s.send("web")
s.settle(80)
snap = s.snapshot()
- check("typing filters the list", "service:web" in snap.screen()
- and "agent:main" not in snap.screen(), repr(snap.screen()))
- check("the query is shown as you type", "web" in snap.screen(),
- repr(snap.screen()))
+ check(
+ "typing filters the list",
+ "service:web" in snap.screen() and "agent:main" not in snap.screen(),
+ repr(snap.screen()),
+ )
+ check(
+ "the query is shown as you type",
+ "web" in snap.screen(),
+ repr(snap.screen()),
+ )
check("and the count follows it", counter(s) == (1, 1), str(counter(s)))
s.send(r"\r")
s.settle(80)
- check("enter focuses the match", s.focused()["id"] == b,
- str(s.focused()))
+ check("enter focuses the match", s.focused()["id"] == b, str(s.focused()))
check("and closes the finder", not is_open(s))
with Session(SH, cols=70, rows=16) as s:
@@ -120,13 +139,18 @@ def test_finder_keyboard():
s.send("zzzz")
s.settle(80)
check("a query matching nothing is not fatal", is_open(s))
- check("and says so rather than showing an empty box",
- "no pane matches" in s.snapshot().screen(),
- repr(s.snapshot().screen()[:400]))
+ check(
+ "and says so rather than showing an empty box",
+ "no pane matches" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[:400]),
+ )
s.send(r"\x7f\x7f\x7f\x7f") # backspace back to empty
s.settle(80)
- check("backspace restores the list", "agent:main" in s.snapshot().screen(),
- repr(s.snapshot().screen()[:200]))
+ check(
+ "backspace restores the list",
+ "agent:main" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[:200]),
+ )
s.send(r"\e")
s.settle(120)
check("escape closes it", not is_open(s))
@@ -142,29 +166,40 @@ def test_finder_navigation():
s.settle(80)
check("it starts on the first row", counter(s) == (1, 2), str(counter(s)))
- for keys, want, what in ((r"\e[B", 2, "down"),
- (r"\e[A", 1, "up"),
- (r"\x0e", 2, "C-n"),
- (r"\x10", 1, "C-p"),
- (r"\t", 2, "tab"),
- (r"\e[Z", 1, "shift-tab"),
- (r"\e[F", 2, "end"),
- (r"\e[H", 1, "home")):
+ for keys, want, what in (
+ (r"\e[B", 2, "down"),
+ (r"\e[A", 1, "up"),
+ (r"\x0e", 2, "C-n"),
+ (r"\x10", 1, "C-p"),
+ (r"\t", 2, "tab"),
+ (r"\e[Z", 1, "shift-tab"),
+ (r"\e[F", 2, "end"),
+ (r"\e[H", 1, "home"),
+ ):
s.send(keys)
s.settle(60)
- check(f"{what} moves the selection",
- counter(s) == (want, 2), f"{what}: {counter(s)}")
+ check(
+ f"{what} moves the selection",
+ counter(s) == (want, 2),
+ f"{what}: {counter(s)}",
+ )
# Wrapping: one press to reach the last row, which is most of why a
# short list wraps at all.
s.send(r"\e[A")
s.settle(60)
- check("up from the first row wraps to the last",
- counter(s) == (2, 2), str(counter(s)))
+ check(
+ "up from the first row wraps to the last",
+ counter(s) == (2, 2),
+ str(counter(s)),
+ )
s.send(r"\e[B")
s.settle(60)
- check("and down from the last wraps to the first",
- counter(s) == (1, 2), str(counter(s)))
+ check(
+ "and down from the last wraps to the first",
+ counter(s) == (1, 2),
+ str(counter(s)),
+ )
def test_finder_query_editing():
@@ -182,15 +217,20 @@ def test_finder_query_editing():
# A character, not a byte. The rename editor learned this first; the
# finder was still deleting one byte of a multi-byte character and
# leaving the rest.
- s.send(r"\xc3\xa9") # e-acute, two bytes of UTF-8
+ s.send(r"\xc3\xa9") # e-acute, two bytes of UTF-8
s.settle(60)
- check("a multi-byte character is accepted",
- "\u00e9" in s.snapshot().screen(), repr(s.snapshot().screen()[:300]))
+ check(
+ "a multi-byte character is accepted",
+ "\u00e9" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[:300]),
+ )
s.send(r"\x7f")
s.settle(60)
- check("and one backspace removes all of it",
- "\u00e9" not in s.snapshot().screen() and counter(s) == (1, 2),
- repr(s.snapshot().screen()[:300]))
+ check(
+ "and one backspace removes all of it",
+ "\u00e9" not in s.snapshot().screen() and counter(s) == (1, 2),
+ repr(s.snapshot().screen()[:300]),
+ )
def test_finder_mouse():
@@ -206,12 +246,16 @@ def test_finder_mouse():
if not pos:
return
x, y = pos
- check("finder rows sit on top of the hit list",
- snap.hit_at(x, y) == f"find:{b}", str(snap.hit_at(x, y)))
+ check(
+ "finder rows sit on top of the hit list",
+ snap.hit_at(x, y) == f"find:{b}",
+ str(snap.hit_at(x, y)),
+ )
s.click(x, y)
s.settle(80)
- check("clicking a row focuses that pane", s.focused()["id"] == b,
- str(s.focused()))
+ check(
+ "clicking a row focuses that pane", s.focused()["id"] == b, str(s.focused())
+ )
check("and switches to its tab", s.tabs()[1]["active"], str(s.tabs()))
@@ -227,14 +271,18 @@ def test_finder_dismissal_by_mouse():
s.key("f")
s.settle(80)
close = [h for h in s.snapshot().hits if h["action"] == "closefind"]
- check("it has a close button, like any modal", len(close) == 1,
- str(s.snapshot().hits))
+ check(
+ "it has a close button, like any modal",
+ len(close) == 1,
+ str(s.snapshot().hits),
+ )
if close:
s.click(close[0]["x"], close[0]["y"])
s.settle(80)
check("which closes it", not is_open(s))
- check("without changing the focus", s.focused()["id"] == b,
- str(s.focused()))
+ check(
+ "without changing the focus", s.focused()["id"] == b, str(s.focused())
+ )
s.key("f")
s.settle(80)
@@ -242,8 +290,11 @@ def test_finder_dismissal_by_mouse():
s.click(1, 15) # the corner of the screen, well outside the box
s.settle(80)
check("a click outside dismisses it", not is_open(s))
- check("and does not reach the pane behind it",
- s.focused()["id"] == b, str(s.focused()))
+ check(
+ "and does not reach the pane behind it",
+ s.focused()["id"] == b,
+ str(s.focused()),
+ )
def test_finder_marks_where_you_are():
@@ -254,11 +305,15 @@ def test_finder_marks_where_you_are():
s.key("f")
s.settle(80)
marked = [r for r in rows(s) if "\u2022" in r[2]]
- check("exactly one row is marked as the current pane",
- len(marked) == 1, str(rows(s)))
+ check(
+ "exactly one row is marked as the current pane",
+ len(marked) == 1,
+ str(rows(s)),
+ )
if marked:
- check("and it is the pane we are in", marked[0][0] == f"find:{b}",
- str(marked))
+ check(
+ "and it is the pane we are in", marked[0][0] == f"find:{b}", str(marked)
+ )
def test_finder_over_a_collapsed_layout():
@@ -270,22 +325,31 @@ def test_finder_over_a_collapsed_layout():
s.key("\\\\")
s.settle()
target = s.panes()[0]["id"]
- s.api("set-purpose", target="pane", id=target, purpose="agent:findme",
- declared=True)
+ s.api(
+ "set-purpose",
+ target="pane",
+ id=target,
+ purpose="agent:findme",
+ declared=True,
+ )
s.api("focus", id=s.panes()[-1]["id"])
s.resize(50, 14)
s.settle()
- check("the target is collapsed away",
- [p for p in s.panes() if p["id"] == target][0]["hidden"])
+ check(
+ "the target is collapsed away",
+ [p for p in s.panes() if p["id"] == target][0]["hidden"],
+ )
s.key("f")
s.send("findme")
s.settle(80)
s.send(r"\r")
s.settle(80)
- check("the finder reaches a collapsed pane and expands it",
- not [p for p in s.panes() if p["id"] == target][0]["hidden"],
- str(s.panes()))
+ check(
+ "the finder reaches a collapsed pane and expands it",
+ not [p for p in s.panes() if p["id"] == target][0]["hidden"],
+ str(s.panes()),
+ )
if __name__ == "__main__":
diff --git a/tests/test_graphics.py b/tests/test_graphics.py
index 90f7117..805eaeb 100644
--- a/tests/test_graphics.py
+++ b/tests/test_graphics.py
@@ -6,6 +6,7 @@ has to add is re-emitting them to the client at the right place, with ids that
cannot collide between panes. That collision is the whole reason tmux and
zellij drop images instead, and it is the first thing tested here.
"""
+
import base64
import struct
import sys
@@ -14,7 +15,9 @@ import zlib
from harness import Session, check, report
# a 4x2 RGB image
-PX = base64.b64encode(bytes([255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0] * 2)).decode()
+PX = base64.b64encode(
+ bytes([255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 0] * 2)
+).decode()
def png(w=4, h=2, color_type=2):
@@ -23,16 +26,19 @@ def png(w=4, h=2, color_type=2):
`color_type` 2 is RGB and 6 is RGBA; both are worth sending, because the
decoder is asked to normalise whatever the file is into RGBA.
"""
+
def chunk(tag, body):
c = tag + body
return struct.pack(">I", len(body)) + c + struct.pack(">I", zlib.crc32(c))
px = [255, 0, 0] if color_type == 2 else [255, 0, 0, 255]
raw = b"".join(b"\x00" + bytes(px * w) for _ in range(h))
- return (b"\x89PNG\r\n\x1a\n" +
- chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, color_type, 0, 0, 0)) +
- chunk(b"IDAT", zlib.compress(raw)) +
- chunk(b"IEND", b""))
+ return (
+ b"\x89PNG\r\n\x1a\n"
+ + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, color_type, 0, 0, 0))
+ + chunk(b"IDAT", zlib.compress(raw))
+ + chunk(b"IEND", b"")
+ )
def sends_image(image_id=7, cols=6, rows=2, after="sleep 5"):
@@ -49,9 +55,11 @@ def transmits_then_places(place_keys, before_place="", image_id=7):
with `a=p`. Everything a program that draws repeatedly does."""
t = f"\\033_Ga=t,f=24,s=4,v=2,i={image_id},q=2;{PX}\\033\\\\"
p = f"\\033_Ga={place_keys}\\033\\\\"
- return ["/bin/sh", "-c",
- f'stty raw -echo; printf "{t}"; printf "{before_place}"; '
- f'printf "{p}"; sleep 5']
+ return [
+ "/bin/sh",
+ "-c",
+ f'stty raw -echo; printf "{t}"; printf "{before_place}"; printf "{p}"; sleep 5',
+ ]
def test_a_pane_image_reaches_the_screen():
@@ -62,11 +70,16 @@ def test_a_pane_image_reaches_the_screen():
check("the image is placed", len(pl) == 1, str(pl))
if not pl:
return
- check("at the pane's content origin, in screen cells",
- (pl[0]["x"], pl[0]["y"]) == (p["content_x"], p["content_y"]),
- f"{pl[0]} vs content {p['content_x']},{p['content_y']}")
- check("with the size the program asked for",
- (pl[0]["cols"], pl[0]["rows"]) == (6, 2), str(pl[0]))
+ check(
+ "at the pane's content origin, in screen cells",
+ (pl[0]["x"], pl[0]["y"]) == (p["content_x"], p["content_y"]),
+ f"{pl[0]} vs content {p['content_x']},{p['content_y']}",
+ )
+ check(
+ "with the size the program asked for",
+ (pl[0]["cols"], pl[0]["rows"]) == (6, 2),
+ str(pl[0]),
+ )
def test_a_placement_that_does_not_say_how_big_it_is():
@@ -82,8 +95,11 @@ def test_a_placement_that_does_not_say_how_big_it_is():
if not pl:
return
# 4x2 pixels at the default 8x16 cell: one cell each way.
- check("sized from its pixels and the cell size",
- (pl[0]["cols"], pl[0]["rows"]) == (1, 1), str(pl[0]))
+ check(
+ "sized from its pixels and the cell size",
+ (pl[0]["cols"], pl[0]["rows"]) == (1, 1),
+ str(pl[0]),
+ )
def test_the_cell_size_a_client_reports_is_what_sizes_an_image():
@@ -93,24 +109,32 @@ def test_the_cell_size_a_client_reports_is_what_sizes_an_image():
s.settle(150)
pl = places(s)
# The same 4x2 image against a 2x1 cell is 2 cells wide and 2 tall.
- check("a different cell means a different number of cells",
- pl and (pl[0]["cols"], pl[0]["rows"]) == (2, 2), str(pl))
+ check(
+ "a different cell means a different number of cells",
+ pl and (pl[0]["cols"], pl[0]["rows"]) == (2, 2),
+ str(pl),
+ )
def test_a_program_can_read_the_pixel_size_from_its_pty():
"""TIOCGWINSZ has pixel fields, and a program that draws images reads
them. Zeroes there are why dvd.py fell back to guessing 8x16."""
- prog = ("import fcntl, termios, struct, sys, time;"
- "r, c, xp, yp = struct.unpack('HHHH', "
- "fcntl.ioctl(0, termios.TIOCGWINSZ, b'\\0' * 8));"
- "print('WS', c, r, xp, yp); sys.stdout.flush(); time.sleep(5)")
+ prog = (
+ "import fcntl, termios, struct, sys, time;"
+ "r, c, xp, yp = struct.unpack('HHHH', "
+ "fcntl.ioctl(0, termios.TIOCGWINSZ, b'\\0' * 8));"
+ "print('WS', c, r, xp, yp); sys.stdout.flush(); time.sleep(5)"
+ )
with Session(["python3", "-c", prog], cols=50, rows=10) as s:
snap = s.until_text("WS ")
line = [l for l in snap.text if "WS " in l][0].strip()
cols, rows, xp, yp = (int(v) for v in line.split()[1:5])
check("the pty carries pixel dimensions", xp and yp, line)
- check("consistent with the cell size we were told",
- xp == cols * 8 and yp == rows * 16, line)
+ check(
+ "consistent with the cell size we were told",
+ xp == cols * 8 and yp == rows * 16,
+ line,
+ )
def asks_its_size():
@@ -142,7 +166,8 @@ def asks_its_size():
"for m in re.finditer(rb'\\033\\[([0-9;]+)t', buf):\n"
" sys.stdout.write('R' + m.group(1).decode() + '\\r\\n')\n"
"sys.stdout.write('END\\r\\n'); sys.stdout.flush()\n"
- "time.sleep(5)\n")
+ "time.sleep(5)\n"
+ )
return ["python3", "-c", prog]
@@ -185,8 +210,7 @@ def test_a_program_can_ask_how_big_a_cell_is():
check("the pixel size query is answered (CSI 14 t)", 4 in r, str(r))
if 8 in r and 4 in r:
rows, cols = r[8]
- check("and the three answers agree",
- r[4] == [rows * 16, cols * 8], str(r))
+ check("and the three answers agree", r[4] == [rows * 16, cols * 8], str(r))
def test_the_cell_size_reported_is_the_client_s():
@@ -197,8 +221,7 @@ def test_the_cell_size_reported_is_the_client_s():
s.send("x")
s.until_text("END")
r = size_replies(s)
- check("a different cell means a different answer", r.get(6) == [22, 9],
- str(r))
+ check("a different cell means a different answer", r.get(6) == [22, 9], str(r))
def test_an_image_outlives_a_screen_clear():
@@ -206,26 +229,37 @@ def test_an_image_outlives_a_screen_clear():
programs clear the screen. libghostty-vt freed the image data on ED(2),
so every placement after the first clear drew nothing -- see
vendor/patches. The placements go, the image stays."""
- with Session(transmits_then_places("p,i=7,p=1,q=2,c=6,r=2",
- before_place="\\033[2J"),
- cols=44, rows=10) as s:
+ with Session(
+ transmits_then_places("p,i=7,p=1,q=2,c=6,r=2", before_place="\\033[2J"),
+ cols=44,
+ rows=10,
+ ) as s:
s.settle(200)
- check("a clear before the placement does not lose the image",
- len(places(s)) == 1, str(places(s)))
+ check(
+ "a clear before the placement does not lose the image",
+ len(places(s)) == 1,
+ str(places(s)),
+ )
def test_a_screen_clear_still_removes_what_is_on_screen():
- argv = ["/bin/sh", "-c",
- f'stty raw -echo; printf "\\033_Ga=T,f=24,s=4,v=2,i=7,q=2,c=6,r=2;{PX}\\033\\\\"; '
- f'sleep 0.2; printf "\\033[2J"; printf CLEARED; sleep 5']
+ argv = [
+ "/bin/sh",
+ "-c",
+ f'stty raw -echo; printf "\\033_Ga=T,f=24,s=4,v=2,i=7,q=2,c=6,r=2;{PX}\\033\\\\"; '
+ f'sleep 0.2; printf "\\033[2J"; printf CLEARED; sleep 5',
+ ]
with Session(argv, cols=44, rows=10) as s:
# Both waits are on something observable: the image arrives, then the
# clear that follows it. A settle would race the sleep between them.
s.until(lambda _: len(places(s)) == 1)
check("placed to begin with", len(places(s)) == 1, str(places(s)))
s.until_text("CLEARED")
- check("the placement is gone with the screen it was on",
- len(places(s)) == 0, str(places(s)))
+ check(
+ "the placement is gone with the screen it was on",
+ len(places(s)) == 0,
+ str(places(s)),
+ )
def test_sub_cell_offsets_survive_to_the_client():
@@ -233,29 +267,38 @@ def test_sub_cell_offsets_survive_to_the_client():
with X=/Y=. We tracked the placement and dropped the offsets, so anything
moving jumped a whole cell at a time -- invisible in a still picture and
the first thing you see when it moves."""
- with Session(transmits_then_places("p,i=7,p=1,q=2,c=6,r=2,X=3,Y=5"),
- cols=44, rows=10) as s:
+ with Session(
+ transmits_then_places("p,i=7,p=1,q=2,c=6,r=2,X=3,Y=5"), cols=44, rows=10
+ ) as s:
s.settle(200)
pl = places(s)
- check("the offsets are tracked", pl and (pl[0]["x_off"], pl[0]["y_off"]) == (3, 5),
- str(pl))
+ check(
+ "the offsets are tracked",
+ pl and (pl[0]["x_off"], pl[0]["y_off"]) == (3, 5),
+ str(pl),
+ )
# And, separately, that they reach the terminal: the model being right
# is not the same as the bytes being right.
raw = s.api("graphics", format="bytes")["bytes"]
place = [c for c in raw.split("\x1b") if c.startswith("_Ga=p")]
- check("and emitted to the client", place and "X=3" in place[0] and
- "Y=5" in place[0], str(place))
+ check(
+ "and emitted to the client",
+ place and "X=3" in place[0] and "Y=5" in place[0],
+ str(place),
+ )
def test_a_placement_with_no_offset_emits_none():
- with Session(transmits_then_places("p,i=7,p=1,q=2,c=6,r=2"),
- cols=44, rows=10) as s:
+ with Session(transmits_then_places("p,i=7,p=1,q=2,c=6,r=2"), cols=44, rows=10) as s:
s.settle(200)
raw = s.api("graphics", format="bytes")["bytes"]
place = [c for c in raw.split("\x1b") if c.startswith("_Ga=p")]
- check("nothing is invented", place and "X=" not in place[0] and
- "Y=" not in place[0], str(place))
+ check(
+ "nothing is invented",
+ place and "X=" not in place[0] and "Y=" not in place[0],
+ str(place),
+ )
def test_a_natural_image_is_never_rescaled_as_it_moves():
@@ -263,24 +306,27 @@ def test_a_natural_image_is_never_rescaled_as_it_moves():
natural-size image happens to cover is invisible in a still picture and
wrong the moment it moves: the count goes up by one whenever the image
straddles one more cell boundary, so the picture changes size."""
- with Session(transmits_then_places("p,i=7,p=1,q=2,X=3,Y=5"),
- cols=44, rows=10) as s:
+ with Session(transmits_then_places("p,i=7,p=1,q=2,X=3,Y=5"), cols=44, rows=10) as s:
s.settle(200)
raw = s.api("graphics", format="bytes")["bytes"]
place = [c for c in raw.split("\x1b") if c.startswith("_Ga=p")]
- check("no cell count is sent for a natural placement",
- place and "c=" not in place[0] and "r=" not in place[0],
- str(place))
+ check(
+ "no cell count is sent for a natural placement",
+ place and "c=" not in place[0] and "r=" not in place[0],
+ str(place),
+ )
def test_a_scaled_image_keeps_the_cell_count_it_asked_for():
- with Session(transmits_then_places("p,i=7,p=1,q=2,c=6,r=2"),
- cols=44, rows=10) as s:
+ with Session(transmits_then_places("p,i=7,p=1,q=2,c=6,r=2"), cols=44, rows=10) as s:
s.settle(200)
raw = s.api("graphics", format="bytes")["bytes"]
place = [c for c in raw.split("\x1b") if c.startswith("_Ga=p")]
- check("what the program asked for is passed on",
- place and "c=6" in place[0] and "r=2" in place[0], str(place))
+ check(
+ "what the program asked for is passed on",
+ place and "c=6" in place[0] and "r=2" in place[0],
+ str(place),
+ )
def test_the_graphics_stream_leaves_the_cursor_alone():
@@ -292,8 +338,11 @@ def test_the_graphics_stream_leaves_the_cursor_alone():
with Session(sends_image(), cols=44, rows=10) as s:
s.settle(200)
raw = s.api("graphics", format="bytes")["bytes"]
- check("it moves the cursor at all (or this proves nothing)",
- "H" in raw, repr(raw[:80]))
+ check(
+ "it moves the cursor at all (or this proves nothing)",
+ "H" in raw,
+ repr(raw[:80]),
+ )
check("saved first", raw.startswith("\x1b7"), repr(raw[:8]))
check("and restored last", raw.endswith("\x1b8"), repr(raw[-8:]))
@@ -302,8 +351,11 @@ def test_a_frame_with_no_images_emits_nothing():
"""The save/restore must not become a cost every idle frame pays."""
with Session(["/bin/sh", "-c", "stty raw -echo; cat"], cols=44, rows=10) as s:
s.settle(150)
- check("not one byte", s.api("graphics", format="bytes")["bytes"] == "",
- repr(s.api("graphics", format="bytes")["bytes"]))
+ check(
+ "not one byte",
+ s.api("graphics", format="bytes")["bytes"] == "",
+ repr(s.api("graphics", format="bytes")["bytes"]),
+ )
def test_ids_cannot_collide_between_panes():
@@ -316,23 +368,26 @@ def test_ids_cannot_collide_between_panes():
check("both panes place an image", len(pl) == 2, str(pl))
if len(pl) != 2:
return
- check("and they were given different ids for the client",
- pl[0]["image"] != pl[1]["image"], str(pl))
- check("side by side, where their panes are",
- pl[0]["x"] != pl[1]["x"], str(pl))
+ check(
+ "and they were given different ids for the client",
+ pl[0]["image"] != pl[1]["image"],
+ str(pl),
+ )
+ check("side by side, where their panes are", pl[0]["x"] != pl[1]["x"], str(pl))
def test_placement_follows_the_layout():
with Session(sends_image(), cols=90, rows=12) as s:
s.settle(200)
before = places(s)[0]
- s.key("\\\\") # the pane with the image is now the left half
+ s.key("\\\\") # the pane with the image is now the left half
s.settle(250)
s.api("focus", id=s.panes()[0]["id"])
s.settle(80)
after = [q for q in places(s) if q["image"] == before["image"]]
- check("the placement is still tracked after a split", after != [],
- str(places(s)))
+ check(
+ "the placement is still tracked after a split", after != [], str(places(s))
+ )
def test_cropped_at_the_pane_edge():
@@ -343,10 +398,16 @@ def test_cropped_at_the_pane_edge():
check("an oversized image is placed", len(pl) == 1, str(pl))
if not pl:
return
- check("cropped to the columns the pane has",
- pl[0]["cols"] <= p["content_w"], f"{pl[0]} vs {p['content_w']}")
- check("and to its rows",
- pl[0]["rows"] <= p["content_h"], f"{pl[0]} vs {p['content_h']}")
+ check(
+ "cropped to the columns the pane has",
+ pl[0]["cols"] <= p["content_w"],
+ f"{pl[0]} vs {p['content_w']}",
+ )
+ check(
+ "and to its rows",
+ pl[0]["rows"] <= p["content_h"],
+ f"{pl[0]} vs {p['content_h']}",
+ )
def test_hidden_panes_place_nothing():
@@ -361,8 +422,11 @@ def test_hidden_panes_place_nothing():
s.settle(200)
hidden = [q for q in s.panes() if q["hidden"]]
check("something collapsed", hidden != [], str(s.panes()))
- check("a collapsed pane places nothing",
- len(places(s)) == len(s.panes()) - len(hidden), str(places(s)))
+ check(
+ "a collapsed pane places nothing",
+ len(places(s)) == len(s.panes()) - len(hidden),
+ str(places(s)),
+ )
def test_placement_goes_away_with_its_pane():
@@ -373,8 +437,9 @@ def test_placement_goes_away_with_its_pane():
check("two placements", len(places(s)) == 2, str(places(s)))
s.key("x") # close the focused pane
s.settle(200)
- check("closing a pane removes its placement", len(places(s)) == 1,
- str(places(s)))
+ check(
+ "closing a pane removes its placement", len(places(s)) == 1, str(places(s))
+ )
def test_partial_visibility_crops_rather_than_squashes():
@@ -387,12 +452,14 @@ def test_partial_visibility_crops_rather_than_squashes():
s.settle(250)
p = s.pane()
pl = places(s)
- check("a taller-than-the-pane image is still placed", len(pl) == 1,
- str(pl))
+ check("a taller-than-the-pane image is still placed", len(pl) == 1, str(pl))
if not pl:
return
- check("clipped to the pane's rows", pl[0]["rows"] <= p["content_h"],
- f"{pl[0]} vs {p['content_h']}")
+ check(
+ "clipped to the pane's rows",
+ pl[0]["rows"] <= p["content_h"],
+ f"{pl[0]} vs {p['content_h']}",
+ )
def sends_png(data, place="c=6,r=2", image_id=7):
@@ -405,12 +472,14 @@ def sends_png(data, place="c=6,r=2", image_id=7):
bytes you wrote. That cost an afternoon and a wrong diagnosis.
"""
b64 = base64.b64encode(data).decode()
- prog = ("import sys, time\n"
- f"sys.stdout.write('\\033_Ga=t,f=100,t=d,i={image_id},q=2;{b64}\\033\\\\')\n"
- f"sys.stdout.write('\\033_Ga=p,i={image_id},p=1,{place},q=2\\033\\\\')\n"
- "sys.stdout.write('DONE')\n"
- "sys.stdout.flush()\n"
- "time.sleep(5)\n")
+ prog = (
+ "import sys, time\n"
+ f"sys.stdout.write('\\033_Ga=t,f=100,t=d,i={image_id},q=2;{b64}\\033\\\\')\n"
+ f"sys.stdout.write('\\033_Ga=p,i={image_id},p=1,{place},q=2\\033\\\\')\n"
+ "sys.stdout.write('DONE')\n"
+ "sys.stdout.flush()\n"
+ "time.sleep(5)\n"
+ )
return ["python3", "-c", prog]
@@ -427,8 +496,11 @@ def test_a_png_is_decoded_and_placed():
pl = places(s)
check(f"a {what} png is placed", len(pl) == 1, str(pl))
if pl:
- check("at the size it asked for",
- (pl[0]["cols"], pl[0]["rows"]) == (6, 2), str(pl[0]))
+ check(
+ "at the size it asked for",
+ (pl[0]["cols"], pl[0]["rows"]) == (6, 2),
+ str(pl[0]),
+ )
def test_a_decoded_png_reaches_the_client_as_raw_pixels():
@@ -441,8 +513,11 @@ def test_a_decoded_png_reaches_the_client_as_raw_pixels():
tx = [c for c in raw.split("\x1b") if c.startswith("_Ga=t")]
check("the image is transmitted to the client", tx != [], repr(raw[:120]))
if tx:
- check("as RGBA, not as the png we were given",
- "f=32" in tx[0] and "f=100" not in tx[0], tx[0][:80])
+ check(
+ "as RGBA, not as the png we were given",
+ "f=32" in tx[0] and "f=100" not in tx[0],
+ tx[0][:80],
+ )
def test_a_png_that_is_not_one_places_nothing():
@@ -458,13 +533,18 @@ def test_a_png_that_is_not_one_places_nothing():
def test_scrolled_away_placements_are_dropped():
- prog = ("stty raw -echo; "
- f'printf "\\033_Ga=T,f=24,s=4,v=2,i=7,q=2,c=6,r=2;{PX}\\033\\\\"; '
- "seq 1 200; cat")
+ prog = (
+ "stty raw -echo; "
+ f'printf "\\033_Ga=T,f=24,s=4,v=2,i=7,q=2,c=6,r=2;{PX}\\033\\\\"; '
+ "seq 1 200; cat"
+ )
with Session(["/bin/sh", "-c", prog], cols=44, rows=10) as s:
s.settle(400)
- check("an image scrolled out of the viewport is not placed",
- places(s) == [], str(places(s)))
+ check(
+ "an image scrolled out of the viewport is not placed",
+ places(s) == [],
+ str(places(s)),
+ )
if __name__ == "__main__":
diff --git a/tests/test_help.py b/tests/test_help.py
index d442500..e845530 100644
--- a/tests/test_help.py
+++ b/tests/test_help.py
@@ -5,6 +5,7 @@ It is built from the bindings the config actually has, not from a list of what
the defaults are — a cheatsheet that can disagree with the keyboard is worse
than none, and it would disagree the first time somebody rebound a key.
"""
+
import re
import subprocess
import sys
@@ -26,8 +27,7 @@ def close_mark():
"""The mark the close button is drawn with, read from the binary rather than
written down here: `--dump-config` is generated from the code, so changing
the default cannot leave this test asserting a glyph nothing draws."""
- out = subprocess.run([BIN, "--dump-config"], capture_output=True,
- text=True).stdout
+ out = subprocess.run([BIN, "--dump-config"], capture_output=True, text=True).stdout
m = re.search(r'close_mark "(.+)"', out)
return m.group(1) if m else "x"
@@ -47,8 +47,10 @@ def test_it_opens_and_lists_the_bindings():
# should not fail this one by pushing a heading off the bottom.
with Session(SH, cols=92, rows=36) as s:
s.settle()
- check("nothing is up to begin with", "split into columns" not in
- s.snapshot().screen())
+ check(
+ "nothing is up to begin with",
+ "split into columns" not in s.snapshot().screen(),
+ )
snap = open_help(s)
screen = snap.screen()
@@ -56,12 +58,22 @@ def test_it_opens_and_lists_the_bindings():
check("it says what the prefix is", "C-a then:" in screen, screen)
# Found by shape rather than by row: where it lands is centring, and a
# test that pins the row is testing the terminal size.
- check("it is titled like a pane, in its own top border",
- any("keys" in l and "\u256d" in l for l in snap.text), screen)
- check("groups its bindings", all(g in screen for g in
- ("panes", "focus", "size", "tabs", "scroll", "session")), screen)
- check("names keys the way you type them",
- "\\" in screen and "?" in screen, screen)
+ check(
+ "it is titled like a pane, in its own top border",
+ any("keys" in l and "\u256d" in l for l in snap.text),
+ screen,
+ )
+ check(
+ "groups its bindings",
+ all(
+ g in screen
+ for g in ("panes", "focus", "size", "tabs", "scroll", "session")
+ ),
+ screen,
+ )
+ check(
+ "names keys the way you type them", "\\" in screen and "?" in screen, screen
+ )
check("merges the nine tab digits into one row", "1…9" in screen, screen)
check("shows arrows as arrows", "←" in screen and "→" in screen, screen)
@@ -97,15 +109,20 @@ def test_sections_are_separated_by_a_blank_line():
for heading in ("focus", "size", "tabs", "scroll", "session"):
y = row_of(" " + heading + " ")
- check(f"a blank line above {heading}",
- y is not None and heading not in snap.text[y - 1] and
- not snap.text[y - 1].strip("│╭╮╰╯─ "),
- repr(snap.text[y - 1]) if y else "not found")
+ check(
+ f"a blank line above {heading}",
+ y is not None
+ and heading not in snap.text[y - 1]
+ and not snap.text[y - 1].strip("│╭╮╰╯─ "),
+ repr(snap.text[y - 1]) if y else "not found",
+ )
first = row_of(" panes ")
- check("but not above the first, which the caption already spaces",
- first is not None and "C-a then:" in snap.text[first - 2],
- repr(snap.text[first - 2]) if first else "not found")
+ check(
+ "but not above the first, which the caption already spaces",
+ first is not None and "C-a then:" in snap.text[first - 2],
+ repr(snap.text[first - 2]) if first else "not found",
+ )
def test_the_box_is_not_taller_than_its_contents():
@@ -119,14 +136,16 @@ def test_the_box_is_not_taller_than_its_contents():
for i, l in enumerate(snap.text):
if "any key closes this" in l:
bottom = i
- check("the footer is on the bottom border", bottom is not None,
- snap.screen())
+ check("the footer is on the bottom border", bottom is not None, snap.screen())
above = snap.text[bottom - 1]
- check("exactly one blank line above it",
- not above.strip("│╭╮╰╯─ "), repr(above))
- check("and content on the line above that",
- snap.text[bottom - 2].strip("│╭╮╰╯─ ") != "",
- repr(snap.text[bottom - 2]))
+ check(
+ "exactly one blank line above it", not above.strip("│╭╮╰╯─ "), repr(above)
+ )
+ check(
+ "and content on the line above that",
+ snap.text[bottom - 2].strip("│╭╮╰╯─ ") != "",
+ repr(snap.text[bottom - 2]),
+ )
def test_the_close_button_can_be_seen_without_hovering_it():
@@ -139,10 +158,14 @@ def test_the_close_button_can_be_seen_without_hovering_it():
check("the modal has a top border", y is not None, snap.screen())
x = line.rindex(MARK)
fg, bg = style(snap, x, y)
- check("the close button is not its own background", fg != bg,
- f"fg {fg} bg {bg}")
- check("and it is registered as a target",
- snap.hit_at(x, y) == "closehelp", str(snap.hit_at(x, y)))
+ check(
+ "the close button is not its own background", fg != bg, f"fg {fg} bg {bg}"
+ )
+ check(
+ "and it is registered as a target",
+ snap.hit_at(x, y) == "closehelp",
+ str(snap.hit_at(x, y)),
+ )
def test_the_close_button_closes_it():
@@ -152,8 +175,10 @@ def test_the_close_button_closes_it():
y, line = modal_top(snap)
s.click(line.rindex(MARK), y)
s.settle()
- check("clicking the button puts it away",
- "split into columns" not in s.snapshot().screen())
+ check(
+ "clicking the button puts it away",
+ "split into columns" not in s.snapshot().screen(),
+ )
def test_what_is_behind_is_pushed_back():
@@ -167,8 +192,9 @@ def test_what_is_behind_is_pushed_back():
after = open_help(s)
dimmed = style(after, x, y)
- check("the screen behind the modal is dimmed", dimmed != lit,
- f"{lit} -> {dimmed}")
+ check(
+ "the screen behind the modal is dimmed", dimmed != lit, f"{lit} -> {dimmed}"
+ )
# And the modal itself is not: it is drawn after the scrim, which is
# the whole point of the ordering. Sampled on the title rather than on
@@ -176,8 +202,11 @@ def test_what_is_behind_is_pushed_back():
# and then the line has two of those.
my, line = modal_top(after)
tx = line.index("keys")
- check("the modal is not dimmed with it",
- style(after, tx, my)[0] == "#ffffff", str(style(after, tx, my)))
+ check(
+ "the modal is not dimmed with it",
+ style(after, tx, my)[0] == "#ffffff",
+ str(style(after, tx, my)),
+ )
def test_the_scrim_can_be_turned_off():
@@ -188,8 +217,11 @@ def test_the_scrim_can_be_turned_off():
x, y = before.find("pane 1/1")[0] + 1, before.find("pane 1/1")[1]
lit = style(before, x, y)
after = open_help(s)
- check("nothing behind it changes", style(after, x, y) == lit,
- f"{lit} -> {style(after, x, y)}")
+ check(
+ "nothing behind it changes",
+ style(after, x, y) == lit,
+ f"{lit} -> {style(after, x, y)}",
+ )
check("but the modal is still up", "split into columns" in after.screen())
@@ -202,11 +234,14 @@ def test_any_key_dismisses_it_and_does_nothing_else():
# `\` is the split binding; while the sheet is up it must only close it.
s.send(r"\\")
s.settle()
- check("a keystroke closes it",
- "split into columns" not in s.snapshot().screen(),
- s.snapshot().screen())
- check("and is swallowed rather than run", len(s.panes()) == panes,
- str(s.panes()))
+ check(
+ "a keystroke closes it",
+ "split into columns" not in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
+ check(
+ "and is swallowed rather than run", len(s.panes()) == panes, str(s.panes())
+ )
def test_a_click_dismisses_it_and_hits_nothing_underneath():
@@ -214,12 +249,14 @@ def test_a_click_dismisses_it_and_hits_nothing_underneath():
s.settle()
panes = len(s.panes())
open_help(s)
- s.click(4, 3) # over the pane border, under the modal
+ s.click(4, 3) # over the pane border, under the modal
s.settle()
- check("a click closes it",
- "split into columns" not in s.snapshot().screen())
- check("and does not reach the border it landed on",
- len(s.panes()) == panes, str(s.panes()))
+ check("a click closes it", "split into columns" not in s.snapshot().screen())
+ check(
+ "and does not reach the border it landed on",
+ len(s.panes()) == panes,
+ str(s.panes()),
+ )
def test_the_prefix_key_is_toggled_off_again():
@@ -228,31 +265,40 @@ def test_the_prefix_key_is_toggled_off_again():
open_help(s)
s.send(r"\x01?")
s.settle()
- check("pressing it again puts it away",
- "split into columns" not in s.snapshot().screen())
+ check(
+ "pressing it again puts it away",
+ "split into columns" not in s.snapshot().screen(),
+ )
def test_it_reports_the_bindings_the_config_has():
"""Rebound keys show up rebound, and a removed one is not advertised."""
# A config's keys block *adds to* the defaults, so unbinding is how you
# take something away -- and the sheet has to follow both.
- conf = cfg('keys {\n'
- ' prefix "ctrl+b"\n'
- ' bind "v" "split-cols"\n'
- ' bind "-" "none"\n'
- ' bind "?" "help"\n'
- '}\n')
+ conf = cfg(
+ "keys {\n"
+ ' prefix "ctrl+b"\n'
+ ' bind "v" "split-cols"\n'
+ ' bind "-" "none"\n'
+ ' bind "?" "help"\n'
+ "}\n"
+ )
with Session(SH, cols=92, rows=28, config=conf) as s:
s.settle()
- s.send(r"\x02?") # C-b, the prefix this config asked for
+ s.send(r"\x02?") # C-b, the prefix this config asked for
s.settle()
screen = s.snapshot().screen()
- check("the rebound key is the one shown",
- "v" in screen and "split into columns" in screen, screen)
- check("the prefix shown is the configured one", "C-b then:" in screen,
- screen)
- check("an unbound action is not advertised",
- "split into rows" not in screen, screen)
+ check(
+ "the rebound key is the one shown",
+ "v" in screen and "split into columns" in screen,
+ screen,
+ )
+ check("the prefix shown is the configured one", "C-b then:" in screen, screen)
+ check(
+ "an unbound action is not advertised",
+ "split into rows" not in screen,
+ screen,
+ )
check("but the ones still bound are", "next tab" in screen, screen)
@@ -265,8 +311,11 @@ def test_it_fits_in_a_small_terminal_without_writing_over_its_own_frame():
# Every row of the modal ends with its own border: a label that ran on
# would have overwritten it.
body = [l.rstrip() for l in snap.text if "C-a then:" in l or "panes" in l]
- check("the frame survives the contents",
- all(l.endswith("│") for l in body), str(body))
+ check(
+ "the frame survives the contents",
+ all(l.endswith("│") for l in body),
+ str(body),
+ )
for name, fn in sorted(list(globals().items())):
diff --git a/tests/test_hints.py b/tests/test_hints.py
index bf00d87..fd85711 100644
--- a/tests/test_hints.py
+++ b/tests/test_hints.py
@@ -6,6 +6,7 @@ when clicked looks exactly like a border. The hint is read off the hit list
rather than tracked, so anything that registers a hit gets one by being named
in the table and nothing has to remember to raise it.
"""
+
import os
import sys
import tempfile
@@ -39,12 +40,17 @@ def test_each_button_says_what_it_does():
s.settle(20)
s.api("split", dir="cols")
s.settle(20)
- check("nothing is said before the pointer is on anything",
- "minimise" not in bar(s) and "close" not in bar(s), repr(bar(s)))
+ check(
+ "nothing is said before the pointer is on anything",
+ "minimise" not in bar(s) and "close" not in bar(s),
+ repr(bar(s)),
+ )
- for prefix, word in (("minimize:", "minimise"),
- ("zoom:", "fill the tab"),
- ("close:", "close this pane")):
+ for prefix, word in (
+ ("minimize:", "minimise"),
+ ("zoom:", "fill the tab"),
+ ("close:", "close this pane"),
+ ):
h = hit(s, prefix)
hover(s, h["x"], h["y"])
check(f"{prefix[:-1]} says its word", word in bar(s), repr(bar(s)))
@@ -61,10 +67,13 @@ def test_the_zoom_hint_says_which_way_it_goes():
s.send(r"\x01z")
s.settle(20)
- h = hit(s, "zoom:") # the button moved with the pane
+ h = hit(s, "zoom:") # the button moved with the pane
hover(s, h["x"], h["y"])
- check("and to undo it once it is zoomed",
- "back to the layout" in bar(s), repr(bar(s)))
+ check(
+ "and to undo it once it is zoomed",
+ "back to the layout" in bar(s),
+ repr(bar(s)),
+ )
def test_a_border_names_the_direction_it_would_split():
@@ -72,9 +81,10 @@ def test_a_border_names_the_direction_it_would_split():
s.settle(20)
p = s.pane()
for x, y, word in (
- (p["x"], p["y"] + 2, "split left"),
- (p["x"] + p["w"] - 1, p["y"] + 2, "split right"),
- (p["x"] + 6, p["y"] + p["h"] - 1, "split down")):
+ (p["x"], p["y"] + 2, "split left"),
+ (p["x"] + p["w"] - 1, p["y"] + 2, "split right"),
+ (p["x"] + 6, p["y"] + p["h"] - 1, "split down"),
+ ):
hover(s, x, y)
check(f"the border says {word}", word in bar(s), repr(bar(s)))
@@ -86,13 +96,19 @@ def test_the_gap_and_the_strip_and_the_bar():
s.settle(20)
left = s.panes()[0]
hover(s, left["x"] + left["w"], left["y"] + 3)
- check("the gap between panes offers a resize",
- "drag to resize" in bar(s), repr(bar(s)))
+ check(
+ "the gap between panes offers a resize",
+ "drag to resize" in bar(s),
+ repr(bar(s)),
+ )
t = hit(s, "tab:")
hover(s, t["x"] + 1, t["y"])
- check("a tab offers the two things you would not guess",
- "rename" in bar(s) and "reorder" in bar(s), repr(bar(s)))
+ check(
+ "a tab offers the two things you would not guess",
+ "rename" in bar(s) and "reorder" in bar(s),
+ repr(bar(s)),
+ )
n = hit(s, "newtab")
hover(s, n["x"] + 1, n["y"])
check("the new-tab button offers a new one", "new tab" in bar(s), repr(bar(s)))
@@ -100,11 +116,17 @@ def test_the_gap_and_the_strip_and_the_bar():
s.send(r"\x01m")
s.settle(20)
gone = [p for p in s.panes() if p["w"] <= 1][0]["id"]
- e = [h for h in s.snapshot().hits
- if h["action"] == f"focus:{gone}" and h["h"] == 1][0]
+ e = [
+ h
+ for h in s.snapshot().hits
+ if h["action"] == f"focus:{gone}" and h["h"] == 1
+ ][0]
hover(s, e["x"] + 1, e["y"])
- check("a pane put away offers to come back",
- "open this pane" in bar(s), repr(bar(s)))
+ check(
+ "a pane put away offers to come back",
+ "open this pane" in bar(s),
+ repr(bar(s)),
+ )
def test_the_middle_is_centred_on_the_row_not_on_the_gap():
@@ -121,8 +143,11 @@ def test_the_middle_is_centred_on_the_row_not_on_the_gap():
row = bar(s)
at = row.index("close this pane")
want = (96 - len("close this pane")) // 2
- check("the hint is centred on the terminal", abs(at - want) <= 1,
- f"at {at}, expected about {want}: {row!r}")
+ check(
+ "the hint is centred on the terminal",
+ abs(at - want) <= 1,
+ f"at {at}, expected about {want}: {row!r}",
+ )
def test_it_stays_put_when_the_ends_change_length():
@@ -135,8 +160,7 @@ def test_it_stays_put_when_the_ends_change_length():
for name in ("s", "medium-name", "a-really-quite-long-session-name"):
with Session(SH, cols=120, rows=10) as s:
s.settle(20)
- s.api("set-purpose", target="tab", id=s.tabs()[0]["id"],
- purpose=name)
+ s.api("set-purpose", target="tab", id=s.tabs()[0]["id"], purpose=name)
s.settle(20)
row = bar(s)
check(f"the banner is up with {name!r}", "sl0ppty 0." in row, row)
@@ -149,19 +173,26 @@ def test_a_full_line_drops_the_middle_rather_than_overlapping():
say which session and pane you are in, and the middle is a hint you can
get again by hovering or a version you can read a moment later. So the
middle is the part that disappears."""
- wordy = ["/bin/sh", "-c",
- 'printf "\\033]2;a-very-long-pane-title-indeed\\007"; '
- 'stty raw -echo; cat']
+ wordy = [
+ "/bin/sh",
+ "-c",
+ 'printf "\\033]2;a-very-long-pane-title-indeed\\007"; stty raw -echo; cat',
+ ]
with Session(wordy, cols=60, rows=18) as s:
s.settle(20)
h = hit(s, "close:")
hover(s, h["x"], h["y"])
row = bar(s)
- check("the hint is dropped when it would not fit clear of the ends",
- "close this pane" not in row, repr(row))
- check("and both ends survive intact",
- "a-very-long-pane-title-indeed" in row and "pane 1/1" in row,
- repr(row))
+ check(
+ "the hint is dropped when it would not fit clear of the ends",
+ "close this pane" not in row,
+ repr(row),
+ )
+ check(
+ "and both ends survive intact",
+ "a-very-long-pane-title-indeed" in row and "pane 1/1" in row,
+ repr(row),
+ )
def test_it_keeps_a_blank_column_on_each_side():
@@ -171,8 +202,8 @@ def test_it_keeps_a_blank_column_on_each_side():
row = bar(s)
at = row.index("sl0ppty 0.")
end = at + len(row[at:].split(" ")[0])
- check("a gap before it", row[at - 1] == " ", repr(row[at - 4:at + 4]))
- check("a gap after it", row[end] == " ", repr(row[end - 4:end + 4]))
+ check("a gap before it", row[at - 1] == " ", repr(row[at - 4 : at + 4]))
+ check("a gap after it", row[end] == " ", repr(row[end - 4 : end + 4]))
def test_the_version_sits_in_the_slot_when_no_hint_does():
@@ -191,18 +222,20 @@ def test_the_version_sits_in_the_slot_when_no_hint_does():
check("a hint takes the slot back", "close this pane" in bar(s), bar(s))
check("and the banner gives way", "sl0ppty 0." not in bar(s), bar(s))
- hover(s, h["x"], h["y"] + 4) # off the button, into the pane
+ hover(s, h["x"], h["y"] + 4) # off the button, into the pane
s.settle(20)
- check("the banner returns when the pointer moves off",
- "sl0ppty 0." in bar(s), bar(s))
+ check(
+ "the banner returns when the pointer moves off",
+ "sl0ppty 0." in bar(s),
+ bar(s),
+ )
def test_the_banner_and_the_hints_are_separate_knobs():
off = cfg("version_banner false\n")
with Session(SH, cols=96, rows=18, config=off) as s:
s.settle(20)
- check("version_banner false says nothing", "sl0ppty" not in bar(s),
- bar(s))
+ check("version_banner false says nothing", "sl0ppty" not in bar(s), bar(s))
s.api("split", dir="cols")
s.settle(20)
h = hit(s, "close:")
@@ -213,8 +246,7 @@ def test_the_banner_and_the_hints_are_separate_knobs():
nohints = cfg("hints false\n")
with Session(SH, cols=96, rows=18, config=nohints) as s:
s.settle(20)
- check("hints false leaves the banner alone", "sl0ppty 0." in bar(s),
- bar(s))
+ check("hints false leaves the banner alone", "sl0ppty 0." in bar(s), bar(s))
s.api("split", dir="cols")
s.settle(20)
h = hit(s, "close:")
@@ -231,8 +263,7 @@ def test_hints_can_be_turned_off():
s.settle(20)
h = hit(s, "close:")
hover(s, h["x"], h["y"])
- check("hints false says nothing", "close this pane" not in bar(s),
- repr(bar(s)))
+ check("hints false says nothing", "close this pane" not in bar(s), repr(bar(s)))
os.unlink(conf)
diff --git a/tests/test_hover.py b/tests/test_hover.py
index e515661..2dae5c2 100644
--- a/tests/test_hover.py
+++ b/tests/test_hover.py
@@ -5,6 +5,7 @@ The feature is three lines. The reason it is not obnoxious is the guards: it
must not steal focus mid-chord, reach past an open finder, interrupt a drag,
or expand a collapsed pane just because the pointer crossed its header.
"""
+
import os
import sys
import tempfile
@@ -41,8 +42,7 @@ def test_hover_focuses():
hover(s, left["content_x"] + 2, left["content_y"] + 1)
s.settle(60)
- check("hovering a pane focuses it", focused_id(s) == left["id"],
- str(s.panes()))
+ check("hovering a pane focuses it", focused_id(s) == left["id"], str(s.panes()))
hover(s, right["content_x"] + 2, right["content_y"] + 1)
s.settle(60)
@@ -68,8 +68,11 @@ def test_guards():
s.send(r"\x01")
hover(s, left["content_x"] + 2, left["content_y"] + 1)
s.settle(60)
- check("hover does not steal focus mid-chord",
- focused_id(s) == right["id"], str(s.panes()))
+ check(
+ "hover does not steal focus mid-chord",
+ focused_id(s) == right["id"],
+ str(s.panes()),
+ )
s.send("g") # release the prefix harmlessly
# with the finder open, the pane underneath must not take focus
@@ -77,17 +80,19 @@ def test_guards():
s.settle(60)
hover(s, left["content_x"] + 2, left["content_y"] + 1)
s.settle(60)
- check("hover does not reach past the finder",
- focused_id(s) == right["id"], str(s.panes()))
+ check(
+ "hover does not reach past the finder",
+ focused_id(s) == right["id"],
+ str(s.panes()),
+ )
s.send(r"\e")
s.settle(60)
# during a drag, the mouse belongs to the drag
- s.send(rf"\e[<0;{right['x'] + 5};{right['y'] + 1}M") # press a title
+ s.send(rf"\e[<0;{right['x'] + 5};{right['y'] + 1}M") # press a title
hover(s, left["content_x"] + 2, left["content_y"] + 1) # (button held)
s.settle(60)
- check("a drag owns the mouse", focused_id(s) == right["id"],
- str(s.panes()))
+ check("a drag owns the mouse", focused_id(s) == right["id"], str(s.panes()))
s.send(r"\x01g") # any key ends the drag
@@ -108,15 +113,19 @@ def test_hover_does_not_expand_collapsed_panes():
before = focused_id(s)
hover(s, h["x"] + 3, h["y"])
s.settle(60)
- check("hovering a collapsed header does not expand it",
- [p for p in s.panes() if p["id"] == h["id"]][0]["hidden"],
- str(s.panes()))
+ check(
+ "hovering a collapsed header does not expand it",
+ [p for p in s.panes() if p["id"] == h["id"]][0]["hidden"],
+ str(s.panes()),
+ )
check("nor move focus", focused_id(s) == before, str(s.panes()))
# clicking it still does
s.click(h["x"] + 3, h["y"])
s.settle()
- check("clicking it still expands it",
- not [p for p in s.panes() if p["id"] == h["id"]][0]["hidden"])
+ check(
+ "clicking it still expands it",
+ not [p for p in s.panes() if p["id"] == h["id"]][0]["hidden"],
+ )
def test_can_be_turned_off():
@@ -128,38 +137,50 @@ def test_can_be_turned_off():
left, right = s.panes()
hover(s, left["content_x"] + 2, left["content_y"] + 1)
s.settle(60)
- check("with the option off, hovering does nothing",
- focused_id(s) == right["id"], str(s.panes()))
+ check(
+ "with the option off, hovering does nothing",
+ focused_id(s) == right["id"],
+ str(s.panes()),
+ )
s.click(left["content_x"] + 2, left["content_y"] + 1)
s.settle(60)
- check("but clicking still focuses", focused_id(s) == left["id"],
- str(s.panes()))
+ check("but clicking still focuses", focused_id(s) == left["id"], str(s.panes()))
os.unlink(path)
def test_panes_still_get_motion():
"""?1003 also fixes something: a pane that tracks hover can now see it."""
- tracker = ["/bin/sh", "-c",
- 'stty raw -echo; printf "\\033[?1003h\\033[?1006h"; cat -v']
+ tracker = [
+ "/bin/sh",
+ "-c",
+ 'stty raw -echo; printf "\\033[?1003h\\033[?1006h"; cat -v',
+ ]
with Session(tracker, cols=70, rows=12) as s:
s.settle()
p = s.pane()
hover(s, p["content_x"] + 4, p["content_y"] + 2)
s.settle()
out = s.snapshot().pane_text(p)
- check("motion reaches a pane that asked for any-event tracking",
- "^[[<35;5;3M" in out, repr(out[:120]))
+ check(
+ "motion reaches a pane that asked for any-event tracking",
+ "^[[<35;5;3M" in out,
+ repr(out[:120]),
+ )
- quiet = ["/bin/sh", "-c",
- 'stty raw -echo; printf "\\033[?1000h\\033[?1006h"; cat -v']
+ quiet = [
+ "/bin/sh",
+ "-c",
+ 'stty raw -echo; printf "\\033[?1000h\\033[?1006h"; cat -v',
+ ]
with Session(quiet, cols=70, rows=12) as s:
s.settle()
p = s.pane()
hover(s, p["content_x"] + 4, p["content_y"] + 2)
s.settle()
out = s.snapshot().pane_text(p)
- check("but not one that only asked for clicks", "[<" not in out,
- repr(out[:120]))
+ check(
+ "but not one that only asked for clicks", "[<" not in out, repr(out[:120])
+ )
if __name__ == "__main__":
diff --git a/tests/test_inband_shaders.py b/tests/test_inband_shaders.py
index 41c23aa..4a5bc6c 100755
--- a/tests/test_inband_shaders.py
+++ b/tests/test_inband_shaders.py
@@ -9,6 +9,7 @@ colour pass and editing a file to guess at one.
The chain is the config's own syntax, and that is the property worth testing:
what you arrive at by typing has to be a line you can paste.
"""
+
import os
import sys
import tempfile
@@ -20,13 +21,13 @@ ST = "\\033\\\\"
# A pane's command as a layout file writes it. KDL strings understand \e and have
# no \033, and every quote inside one is escaped -- twice over for the ones that
# have to survive as far as the chain parser.
-LAYOUT = r'''layout {
+LAYOUT = r"""layout {
tab name="t" {
pane command="/bin/sh -c \"stty raw -echo; printf '\\e]5577;1;shader;chrome;tint color=\\\"#ff0033\\\" amount=255\\x07'; exec cat\""
pane command="/bin/sh -c \"stty raw -echo; exec cat\""
}
}
-'''
+"""
def cfg(text):
@@ -50,8 +51,11 @@ def pane(*seqs, body="hello"):
before I looked at what the pane was actually running.
"""
emit = "".join("printf '%s';" % s.replace("%", "%%") for s in seqs)
- return ['/bin/sh', '-c',
- "printf '%s\\n'; stty raw -echo; %s exec cat -v" % (body, emit)]
+ return [
+ "/bin/sh",
+ "-c",
+ "printf '%s\\n'; stty raw -echo; %s exec cat -v" % (body, emit),
+ ]
def staged(*stages, body="hello"):
@@ -71,11 +75,12 @@ def staged(*stages, body="hello"):
for i, (seq, mark) in enumerate(stages):
if i:
parts.append("read -r _;")
- parts.append("printf '%s'; printf '%s\\n';"
- % (seq.replace("%", "%%"), mark))
- return ['/bin/sh', '-c',
- "printf '%s\\n'; stty raw -echo; %s exec cat -v"
- % (body, " ".join(parts))]
+ parts.append("printf '%s'; printf '%s\\n';" % (seq.replace("%", "%%"), mark))
+ return [
+ "/bin/sh",
+ "-c",
+ "printf '%s\\n'; stty raw -echo; %s exec cat -v" % (body, " ".join(parts)),
+ ]
def frame(snap, p):
@@ -88,16 +93,25 @@ def replies(snap, p):
def test_a_program_can_paint_its_own_frame():
path = cfg("in_band_shaders true\n")
- with Session(pane(osc('1;shader;chrome;tint color="#ff0033" amount=255')),
- cols=60, rows=9, config=path) as s:
+ with Session(
+ pane(osc('1;shader;chrome;tint color="#ff0033" amount=255')),
+ cols=60,
+ rows=9,
+ config=path,
+ ) as s:
s.until_text("shader-reply")
snap, p = s.snapshot(), s.pane()
st = frame(snap, p)
- check("the frame takes the colour the pane asked for",
- st["fg"] == "#ff0033", str(st))
- check("and the pane is told it was done",
- "shader-reply;ok" in replies(snap, p),
- repr(replies(snap, p)[:120]))
+ check(
+ "the frame takes the colour the pane asked for",
+ st["fg"] == "#ff0033",
+ str(st),
+ )
+ check(
+ "and the pane is told it was done",
+ "shader-reply;ok" in replies(snap, p),
+ repr(replies(snap, p)[:120]),
+ )
os.unlink(path)
@@ -106,19 +120,32 @@ def test_the_contents_are_a_separate_chain():
document: what it says replaces both, the way naming `shaders { }` in a config
replaces the block rather than adding to it."""
path = cfg("in_band_shaders true\n")
- with Session(pane(osc('1;shader;chrome;'
- 'tint where="content" color="#00ff00" amount=255; '
- 'tint where="chrome" color="#ff0000" amount=255')),
- cols=60, rows=9, config=path) as s:
+ with Session(
+ pane(
+ osc(
+ "1;shader;chrome;"
+ 'tint where="content" color="#00ff00" amount=255; '
+ 'tint where="chrome" color="#ff0000" amount=255'
+ )
+ ),
+ cols=60,
+ rows=9,
+ config=path,
+ ) as s:
s.until_text("shader-reply")
snap, p = s.snapshot(), s.pane()
body = snap.style_at(p["x"] + 1, p["y"] + 1)
- check("the contents take the content entry",
- body["fg"] == "#00ff00", str(body))
- check("and the frame takes the chrome one",
- frame(snap, p)["fg"] == "#ff0000", str(frame(snap, p)))
- check("and both are counted", "1 chrome, 1 content" in replies(snap, p),
- repr(replies(snap, p)[:160]))
+ check("the contents take the content entry", body["fg"] == "#00ff00", str(body))
+ check(
+ "and the frame takes the chrome one",
+ frame(snap, p)["fg"] == "#ff0000",
+ str(frame(snap, p)),
+ )
+ check(
+ "and both are counted",
+ "1 chrome, 1 content" in replies(snap, p),
+ repr(replies(snap, p)[:160]),
+ )
os.unlink(path)
@@ -127,25 +154,44 @@ def test_a_document_replaces_what_it_does_not_mention():
takes the contents pass away, because a document says everything this pane
wears. It is what a config does, and the counts say it out loud."""
path = cfg("in_band_shaders true\n")
- with Session(staged((osc('1;shader;chrome;'
- 'tint where="content" channel="bg" color="#00ff88" amount=255; '
- 'tint where="chrome" color="#ff0033" amount=255'), "both"),
- (osc('1;shader;chrome;tint color="#ff0033" amount=255'), "one")),
- cols=64, rows=10, config=path) as s:
+ with Session(
+ staged(
+ (
+ osc(
+ "1;shader;chrome;"
+ 'tint where="content" channel="bg" color="#00ff88" amount=255; '
+ 'tint where="chrome" color="#ff0033" amount=255'
+ ),
+ "both",
+ ),
+ (osc('1;shader;chrome;tint color="#ff0033" amount=255'), "one"),
+ ),
+ cols=64,
+ rows=10,
+ config=path,
+ ) as s:
s.until_text("both")
snap, p = s.snapshot(), s.pane()
- check("both rects to start with",
- (snap.style_at(p["content_x"] + 1, p["content_y"] + 1) or {}).get("bg")
- == "#00ff88",
- str(snap.style_at(p["content_x"] + 1, p["content_y"] + 1)))
+ check(
+ "both rects to start with",
+ (snap.style_at(p["content_x"] + 1, p["content_y"] + 1) or {}).get("bg")
+ == "#00ff88",
+ str(snap.style_at(p["content_x"] + 1, p["content_y"] + 1)),
+ )
s.raw("\\n")
s.until_text("one")
snap = s.snapshot()
body = snap.style_at(p["content_x"] + 1, p["content_y"] + 1)
- check("a document with only a frame pass takes the other one off",
- not body or body.get("bg") != "#00ff88", str(body))
- check("and the frame still has its own", frame(snap, p)["fg"] == "#ff0033",
- str(frame(snap, p)))
+ check(
+ "a document with only a frame pass takes the other one off",
+ not body or body.get("bg") != "#00ff88",
+ str(body),
+ )
+ check(
+ "and the frame still has its own",
+ frame(snap, p)["fg"] == "#ff0033",
+ str(frame(snap, p)),
+ )
os.unlink(path)
@@ -155,29 +201,54 @@ def test_a_line_from_a_config_means_what_it_meant_there():
pasted back. So a chrome-aimed request carrying a content entry sets the content
chain, and says so."""
path = cfg("in_band_shaders true\n")
- with Session(pane(osc('1;shader;chrome;tint where="chrome" '
- 'color="#0000ff" amount=255')),
- cols=64, rows=9, config=path) as s:
+ with Session(
+ pane(osc('1;shader;chrome;tint where="chrome" color="#0000ff" amount=255')),
+ cols=64,
+ rows=9,
+ config=path,
+ ) as s:
s.until_text("shader-reply")
snap, p = s.snapshot(), s.pane()
- check("a pasted `where=chrome` entry lands on the chrome",
- frame(snap, p)["fg"] == "#0000ff", str(frame(snap, p)))
- check("and the count says so", "1 chrome, 0 content" in replies(snap, p),
- repr(replies(snap, p)[:160]))
+ check(
+ "a pasted `where=chrome` entry lands on the chrome",
+ frame(snap, p)["fg"] == "#0000ff",
+ str(frame(snap, p)),
+ )
+ check(
+ "and the count says so",
+ "1 chrome, 0 content" in replies(snap, p),
+ repr(replies(snap, p)[:160]),
+ )
- with Session(pane(osc('1;shader;chrome;tint where="content" channel="bg" '
- 'color="#00ff88" amount=255')),
- cols=64, rows=9, config=path) as s:
+ with Session(
+ pane(
+ osc(
+ '1;shader;chrome;tint where="content" channel="bg" '
+ 'color="#00ff88" amount=255'
+ )
+ ),
+ cols=64,
+ rows=9,
+ config=path,
+ ) as s:
s.until_text("shader-reply")
snap, p = s.snapshot(), s.pane()
- check("one naming the other rect goes there, not nowhere",
- (snap.style_at(p["content_x"] + 1, p["content_y"] + 1) or {}).get("bg")
- == "#00ff88",
- str(snap.style_at(p["content_x"] + 1, p["content_y"] + 1)))
- check("...and the frame is left alone",
- frame(snap, p)["fg"] != "#00ff88", str(frame(snap, p)))
- check("...and the count says where it went",
- "0 chrome, 1 content" in replies(snap, p), repr(replies(snap, p)[:160]))
+ check(
+ "one naming the other rect goes there, not nowhere",
+ (snap.style_at(p["content_x"] + 1, p["content_y"] + 1) or {}).get("bg")
+ == "#00ff88",
+ str(snap.style_at(p["content_x"] + 1, p["content_y"] + 1)),
+ )
+ check(
+ "...and the frame is left alone",
+ frame(snap, p)["fg"] != "#00ff88",
+ str(frame(snap, p)),
+ )
+ check(
+ "...and the count says where it went",
+ "0 chrome, 1 content" in replies(snap, p),
+ repr(replies(snap, p)[:160]),
+ )
os.unlink(path)
@@ -189,19 +260,28 @@ def test_the_text_is_a_document_not_just_a_chain():
cases = [
('tint color="#ff0033" amount=255', "1 chrome, 0 content"),
('shaders { tint color="#ff0033" amount=255 }', "1 chrome, 0 content"),
- ('shaders { tint where="chrome" color="#ff0033" amount=255; '
- 'tint where="content" channel="bg" color="#00ff88" amount=255 }',
- "1 chrome, 1 content"),
+ (
+ 'shaders { tint where="chrome" color="#ff0033" amount=255; '
+ 'tint where="content" channel="bg" color="#00ff88" amount=255 }',
+ "1 chrome, 1 content",
+ ),
]
for text, want in cases:
- with Session(pane(osc("1;shader;chrome;" + text)),
- cols=76, rows=9, config=path) as s:
+ with Session(
+ pane(osc("1;shader;chrome;" + text)), cols=76, rows=9, config=path
+ ) as s:
s.until_text("shader-reply")
snap, p = s.snapshot(), s.pane()
- check("accepted: %s" % text[:34], want in replies(snap, p),
- repr(replies(snap, p)[:160]))
- check("...and the frame took it",
- frame(snap, p)["fg"] == "#ff0033", str(frame(snap, p)))
+ check(
+ "accepted: %s" % text[:34],
+ want in replies(snap, p),
+ repr(replies(snap, p)[:160]),
+ )
+ check(
+ "...and the frame took it",
+ frame(snap, p)["fg"] == "#ff0033",
+ str(frame(snap, p)),
+ )
os.unlink(path)
@@ -209,34 +289,50 @@ def test_several_entries_in_one_chain():
"""`;` separates entries, which is why the chain is the last field of the
payload and is taken verbatim rather than split on."""
path = cfg("in_band_shaders true\n")
- with Session(pane(osc('1;shader;chrome;tint color="#ff0033" amount=255; '
- 'dim amount=120')),
- cols=60, rows=9, config=path) as s:
+ with Session(
+ pane(osc('1;shader;chrome;tint color="#ff0033" amount=255; dim amount=120')),
+ cols=60,
+ rows=9,
+ config=path,
+ ) as s:
s.until_text("shader-reply")
snap, p = s.snapshot(), s.pane()
- check("a chain of two entries is accepted whole",
- "shader-reply;ok" in replies(snap, p),
- repr(replies(snap, p)[:120]))
+ check(
+ "a chain of two entries is accepted whole",
+ "shader-reply;ok" in replies(snap, p),
+ repr(replies(snap, p)[:120]),
+ )
st = frame(snap, p)
- check("and both ran: tinted, then dimmed off the tint",
- st["fg"] != "#ff0033" and st["fg"] != "#ffffff", str(st))
+ check(
+ "and both ran: tinted, then dimmed off the tint",
+ st["fg"] != "#ff0033" and st["fg"] != "#ffffff",
+ str(st),
+ )
os.unlink(path)
def test_an_empty_chain_puts_the_pane_back():
path = cfg("in_band_shaders true\n")
- with Session(staged((osc('1;shader;chrome;tint color="#ff0033" amount=255'),
- "on"),
- (osc("1;shader;chrome;"), "off")),
- cols=60, rows=9, config=path) as s:
+ with Session(
+ staged(
+ (osc('1;shader;chrome;tint color="#ff0033" amount=255'), "on"),
+ (osc("1;shader;chrome;"), "off"),
+ ),
+ cols=60,
+ rows=9,
+ config=path,
+ ) as s:
s.until_text("on")
painted = frame(s.snapshot(), s.pane())["fg"]
s.raw("\\n") # the cue for the next stage
s.until_text("off")
after = frame(s.snapshot(), s.pane())["fg"]
check("the pane was painted", painted == "#ff0033", painted)
- check("and an empty chain gives the frame back", after != painted,
- "%s -> %s" % (painted, after))
+ check(
+ "and an empty chain gives the frame back",
+ after != painted,
+ "%s -> %s" % (painted, after),
+ )
os.unlink(path)
@@ -246,10 +342,10 @@ def test_a_chain_that_does_not_read_is_refused_with_a_reason():
rather than watching nothing happen."""
path = cfg("in_band_shaders true\n")
cases = [
- ('1;shader;chrome;bloom amount=200', "unknown shader"),
+ ("1;shader;chrome;bloom amount=200", "unknown shader"),
('1;shader;chrome;tint amount="y +"', "bad amount"),
('1;shader;chrome;tint channel="blue"', "bad channel"),
- ('1;shader;sideways;tint amount=200', "content or chrome"),
+ ("1;shader;sideways;tint amount=200", "content or chrome"),
# A `;` left out: KDL reads the second name as a spare word on the first
# entry, which is the one mistake this syntax makes easy to make.
('1;shader;chrome;tint color="#000000" spotlight', "unexpected argument"),
@@ -258,25 +354,32 @@ def test_a_chain_that_does_not_read_is_refused_with_a_reason():
with Session(pane(osc(payload)), cols=76, rows=9, config=path) as s:
s.until_text("shader-reply")
out = replies(s.snapshot(), s.pane())
- check("refused, and says why: %s" % want, want in out,
- repr(out[:200]))
+ check("refused, and says why: %s" % want, want in out, repr(out[:200]))
os.unlink(path)
def test_a_refused_chain_leaves_the_last_good_one_standing():
path = cfg("in_band_shaders true\n")
- with Session(staged((osc('1;shader;chrome;tint color="#ff0033" amount=255'),
- "good"),
- (osc("1;shader;chrome;nonesuch amount=10"), "bad")),
- cols=60, rows=9, config=path) as s:
+ with Session(
+ staged(
+ (osc('1;shader;chrome;tint color="#ff0033" amount=255'), "good"),
+ (osc("1;shader;chrome;nonesuch amount=10"), "bad"),
+ ),
+ cols=60,
+ rows=9,
+ config=path,
+ ) as s:
s.until_text("good")
before = frame(s.snapshot(), s.pane())["fg"]
s.raw("\\n")
s.until_text("bad")
after = frame(s.snapshot(), s.pane())["fg"]
check("the good chain ran", before == "#ff0033", before)
- check("and a bad one does not undo it", before == after,
- "%s -> %s" % (before, after))
+ check(
+ "and a bad one does not undo it",
+ before == after,
+ "%s -> %s" % (before, after),
+ )
os.unlink(path)
@@ -284,26 +387,44 @@ def test_naming_no_rect_clears_both():
"""`shader;` with no rect is every rect: one exchange puts the whole pane
back, rather than two that can leave it half done."""
path = cfg("in_band_shaders true\n")
- with Session(staged((osc('1;shader;chrome;'
- 'tint where="chrome" color="#ff0033" amount=255; '
- 'tint where="content" color="#ff0033" amount=255'),
- "painted"),
- (osc("1;shader;"), "bare")),
- cols=64, rows=10, config=path) as s:
+ with Session(
+ staged(
+ (
+ osc(
+ "1;shader;chrome;"
+ 'tint where="chrome" color="#ff0033" amount=255; '
+ 'tint where="content" color="#ff0033" amount=255'
+ ),
+ "painted",
+ ),
+ (osc("1;shader;"), "bare"),
+ ),
+ cols=64,
+ rows=10,
+ config=path,
+ ) as s:
s.until_text("painted")
snap, p = s.snapshot(), s.pane()
- check("both chains ran",
- frame(snap, p)["fg"] == "#ff0033"
- and snap.style_at(p["x"] + 1, p["y"] + 1)["fg"] == "#ff0033",
- str(frame(snap, p)))
+ check(
+ "both chains ran",
+ frame(snap, p)["fg"] == "#ff0033"
+ and snap.style_at(p["x"] + 1, p["y"] + 1)["fg"] == "#ff0033",
+ str(frame(snap, p)),
+ )
s.raw("\\n")
s.until_text("bare")
snap = s.snapshot()
body = snap.style_at(p["x"] + 1, p["y"] + 1)
- check("naming no rect takes the frame back",
- frame(snap, p)["fg"] != "#ff0033", str(frame(snap, p)))
- check("...and the contents with it", not body or body["fg"] != "#ff0033",
- str(body))
+ check(
+ "naming no rect takes the frame back",
+ frame(snap, p)["fg"] != "#ff0033",
+ str(frame(snap, p)),
+ )
+ check(
+ "...and the contents with it",
+ not body or body["fg"] != "#ff0033",
+ str(body),
+ )
os.unlink(path)
@@ -313,20 +434,29 @@ def test_it_can_be_cleared_from_outside_the_pane():
anything there, because asking for a clean pane and getting one is not an
error."""
path = cfg("in_band_shaders true\n")
- with Session(pane(osc('1;shader;chrome;tint color="#ff0033" amount=255')),
- cols=64, rows=10, config=path) as s:
+ with Session(
+ pane(osc('1;shader;chrome;tint color="#ff0033" amount=255')),
+ cols=64,
+ rows=10,
+ config=path,
+ ) as s:
s.until_text("shader-reply")
p = s.pane()
check("painted", frame(s.snapshot(), p)["fg"] == "#ff0033", "")
first = s.api("clear-shaders", id=p["id"])
s.settle(60)
check("the control API clears it", first.get("cleared") == 1, str(first))
- check("and the frame is the session's again",
- frame(s.snapshot(), p)["fg"] != "#ff0033",
- str(frame(s.snapshot(), p)))
+ check(
+ "and the frame is the session's again",
+ frame(s.snapshot(), p)["fg"] != "#ff0033",
+ str(frame(s.snapshot(), p)),
+ )
again = s.api("clear-shaders", id=p["id"])
- check("a second call says there was nothing to clear",
- again.get("ok") and again.get("cleared") == 0, str(again))
+ check(
+ "a second call says there was nothing to clear",
+ again.get("ok") and again.get("cleared") == 0,
+ str(again),
+ )
os.unlink(path)
@@ -335,23 +465,33 @@ def test_a_key_can_undo_what_a_pane_did_to_itself():
but it has to work on a key, because a pane you cannot read is not a pane you
want to go looking through a menu for."""
path = cfg('in_band_shaders true\nkeys {\n bind "z" "clear-shaders"\n}\n')
- with Session(pane(osc('1;shader;chrome;tint color="#ff0033" amount=255')),
- cols=64, rows=10, config=path) as s:
+ with Session(
+ pane(osc('1;shader;chrome;tint color="#ff0033" amount=255')),
+ cols=64,
+ rows=10,
+ config=path,
+ ) as s:
s.until_text("shader-reply")
p = s.pane()
check("painted", frame(s.snapshot(), p)["fg"] == "#ff0033", "")
s.key("z")
s.settle(80)
snap = s.snapshot()
- check("the key clears it", frame(snap, p)["fg"] != "#ff0033",
- str(frame(snap, p)))
- check("and says so", "shaders cleared" in snap.screen(),
- repr(snap.screen()[-160:]))
+ check(
+ "the key clears it", frame(snap, p)["fg"] != "#ff0033", str(frame(snap, p))
+ )
+ check(
+ "and says so",
+ "shaders cleared" in snap.screen(),
+ repr(snap.screen()[-160:]),
+ )
s.key("z")
s.settle(80)
- check("pressing it again says there was nothing to undo",
- "no shaders on this pane" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-160:]))
+ check(
+ "pressing it again says there was nothing to undo",
+ "no shaders on this pane" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-160:]),
+ )
os.unlink(path)
@@ -360,38 +500,57 @@ def test_clearing_works_even_with_the_setting_off():
was on, and the session reloaded with it off. The way out must not be the
thing the setting controls."""
path = cfg('in_band_shaders true\nkeys {\n bind "z" "clear-shaders"\n}\n')
- with Session(pane(osc('1;shader;chrome;tint color="#ff0033" amount=255')),
- cols=64, rows=10, config=path) as s:
+ with Session(
+ pane(osc('1;shader;chrome;tint color="#ff0033" amount=255')),
+ cols=64,
+ rows=10,
+ config=path,
+ ) as s:
s.until_text("shader-reply")
p = s.pane()
- check("painted while it was allowed",
- frame(s.snapshot(), p)["fg"] == "#ff0033", "")
- open(path, "w").write('in_band_shaders false\nkeys {\n bind "z" "clear-shaders"\n}\n')
+ check(
+ "painted while it was allowed",
+ frame(s.snapshot(), p)["fg"] == "#ff0033",
+ "",
+ )
+ open(path, "w").write(
+ 'in_band_shaders false\nkeys {\n bind "z" "clear-shaders"\n}\n'
+ )
s.api("reload")
s.settle(60)
- check("the paint survives the reload, being the pane's and not the file's",
- frame(s.snapshot(), p)["fg"] == "#ff0033",
- str(frame(s.snapshot(), p)))
+ check(
+ "the paint survives the reload, being the pane's and not the file's",
+ frame(s.snapshot(), p)["fg"] == "#ff0033",
+ str(frame(s.snapshot(), p)),
+ )
s.key("z")
s.settle(80)
- check("and it can still be cleared",
- frame(s.snapshot(), p)["fg"] != "#ff0033",
- str(frame(s.snapshot(), p)))
+ check(
+ "and it can still be cleared",
+ frame(s.snapshot(), p)["fg"] != "#ff0033",
+ str(frame(s.snapshot(), p)),
+ )
os.unlink(path)
def test_it_is_off_unless_the_config_says_otherwise():
"""The D13 default. A program that tries gets told no, which is the part
that matters: silence would look like a build without the feature."""
- with Session(pane(osc('1;shader;chrome;tint color="#ff0033" amount=255')),
- cols=70, rows=9) as s:
+ with Session(
+ pane(osc('1;shader;chrome;tint color="#ff0033" amount=255')), cols=70, rows=9
+ ) as s:
s.until_text("shader-reply")
snap, p = s.snapshot(), s.pane()
- check("the frame is untouched", frame(snap, p)["fg"] != "#ff0033",
- str(frame(snap, p)))
- check("and the pane is told the session will not do it",
- "in_band_shaders is off" in replies(snap, p),
- repr(replies(snap, p)[:200]))
+ check(
+ "the frame is untouched",
+ frame(snap, p)["fg"] != "#ff0033",
+ str(frame(snap, p)),
+ )
+ check(
+ "and the pane is told the session will not do it",
+ "in_band_shaders is off" in replies(snap, p),
+ repr(replies(snap, p)[:200]),
+ )
def test_a_pane_can_only_paint_itself():
@@ -404,18 +563,28 @@ def test_a_pane_can_only_paint_itself():
"""
path = cfg("in_band_shaders true\n")
lay = cfg(LAYOUT)
- with Session(["/bin/sh", "-c", "exec cat"], cols=90, rows=12,
- config=path, layout=lay) as s:
- s.until(lambda snap: any(
- snap.style_at(q["x"], q["y"])["fg"] == "#ff0033" for q in s.panes()))
+ with Session(
+ ["/bin/sh", "-c", "exec cat"], cols=90, rows=12, config=path, layout=lay
+ ) as s:
+ s.until(
+ lambda snap: any(
+ snap.style_at(q["x"], q["y"])["fg"] == "#ff0033" for q in s.panes()
+ )
+ )
snap = s.snapshot()
panes = sorted(s.panes(), key=lambda q: q["x"])
check("the layout gave two panes", len(panes) == 2, str(len(panes)))
shouty, quiet = panes[0], panes[1]
- check("the one that asked is painted",
- frame(snap, shouty)["fg"] == "#ff0033", str(frame(snap, shouty)))
- check("and the one beside it is untouched",
- frame(snap, quiet)["fg"] != "#ff0033", str(frame(snap, quiet)))
+ check(
+ "the one that asked is painted",
+ frame(snap, shouty)["fg"] == "#ff0033",
+ str(frame(snap, shouty)),
+ )
+ check(
+ "and the one beside it is untouched",
+ frame(snap, quiet)["fg"] != "#ff0033",
+ str(frame(snap, quiet)),
+ )
os.unlink(path)
os.unlink(lay)
@@ -425,15 +594,19 @@ def test_an_expression_animates():
the clock keeps the session painting without anything else happening."""
path = cfg("in_band_shaders true\n")
chain = 'tint color="#ff0033" amount="abs(t / 4 % 510 - 255)"'
- with Session(pane(osc("1;shader;chrome;" + chain)),
- cols=60, rows=9, config=path) as s:
+ with Session(
+ pane(osc("1;shader;chrome;" + chain)), cols=60, rows=9, config=path
+ ) as s:
s.until_text("shader-reply")
seen = set()
for _ in range(14):
seen.add(frame(s.snapshot(), s.pane())["fg"])
s.settle(60)
- check("a time expression keeps the frame moving", len(seen) > 2,
- str(sorted(seen)[:6]))
+ check(
+ "a time expression keeps the frame moving",
+ len(seen) > 2,
+ str(sorted(seen)[:6]),
+ )
os.unlink(path)
diff --git a/tests/test_include.py b/tests/test_include.py
index b2f5200..15f6fd6 100755
--- a/tests/test_include.py
+++ b/tests/test_include.py
@@ -11,6 +11,7 @@ what you include is the base, so the file that includes it wins. The loader
reads a document by asking it for the keys it knows rather than walking it in
order, so "here" is not a position it could honour anyway.
"""
+
import os
import re
import sys
@@ -43,82 +44,118 @@ BLUE = 'theme { frame_focus "#0000ff" }\n'
def test_an_included_theme_applies():
- root = tree({"themes/green.kdl": GREEN, "config.kdl": 'include "themes/green.kdl"\n'})
+ root = tree(
+ {"themes/green.kdl": GREEN, "config.kdl": 'include "themes/green.kdl"\n'}
+ )
with Session(SH, cols=60, rows=14, config=root + "/config.kdl") as s:
s.settle()
- check("the frame wears the included colour", frame_fg(s) == "#00ff00",
- str(frame_fg(s)))
+ check(
+ "the frame wears the included colour",
+ frame_fg(s) == "#00ff00",
+ str(frame_fg(s)),
+ )
def test_the_file_doing_the_including_wins():
"""So a theme is a base you can put two lines on top of."""
- root = tree({"themes/green.kdl": GREEN,
- "config.kdl": 'include "themes/green.kdl"\n' + BLUE})
+ root = tree(
+ {"themes/green.kdl": GREEN, "config.kdl": 'include "themes/green.kdl"\n' + BLUE}
+ )
with Session(SH, cols=60, rows=14, config=root + "/config.kdl") as s:
s.settle()
- check("the outer file's colour is the one on screen",
- frame_fg(s) == "#0000ff", str(frame_fg(s)))
+ check(
+ "the outer file's colour is the one on screen",
+ frame_fg(s) == "#0000ff",
+ str(frame_fg(s)),
+ )
# ...and the order of the two lines does not change that, because an include
# is applied before the file it appears in, wherever it appears.
- root = tree({"themes/green.kdl": GREEN,
- "config.kdl": BLUE + 'include "themes/green.kdl"\n'})
+ root = tree(
+ {"themes/green.kdl": GREEN, "config.kdl": BLUE + 'include "themes/green.kdl"\n'}
+ )
with Session(SH, cols=60, rows=14, config=root + "/config.kdl") as s:
s.settle()
- check("even with the include written last", frame_fg(s) == "#0000ff",
- str(frame_fg(s)))
+ check(
+ "even with the include written last",
+ frame_fg(s) == "#0000ff",
+ str(frame_fg(s)),
+ )
def test_later_includes_win_over_earlier_ones():
- root = tree({"a.kdl": GREEN, "b.kdl": BLUE,
- "config.kdl": 'include "a.kdl"\ninclude "b.kdl"\n'})
+ root = tree(
+ {
+ "a.kdl": GREEN,
+ "b.kdl": BLUE,
+ "config.kdl": 'include "a.kdl"\ninclude "b.kdl"\n',
+ }
+ )
with Session(SH, cols=60, rows=14, config=root + "/config.kdl") as s:
s.settle()
- check("the last one applied is the one that shows",
- frame_fg(s) == "#0000ff", str(frame_fg(s)))
+ check(
+ "the last one applied is the one that shows",
+ frame_fg(s) == "#0000ff",
+ str(frame_fg(s)),
+ )
def test_one_include_can_take_several_files():
- root = tree({"a.kdl": GREEN, "b.kdl": 'gap 2\n',
- "config.kdl": 'include "a.kdl" "b.kdl"\n'})
+ root = tree(
+ {"a.kdl": GREEN, "b.kdl": "gap 2\n", "config.kdl": 'include "a.kdl" "b.kdl"\n'}
+ )
with Session(SH, cols=60, rows=14, config=root + "/config.kdl") as s:
s.settle()
- check("both were applied",
- frame_fg(s) == "#00ff00" and s.pane()["x"] == 4,
- f"{frame_fg(s)} / x={s.pane()['x']}")
+ check(
+ "both were applied",
+ frame_fg(s) == "#00ff00" and s.pane()["x"] == 4,
+ f"{frame_fg(s)} / x={s.pane()['x']}",
+ )
def test_a_path_is_relative_to_the_file_that_wrote_it():
"""The nested file says `include "base/core.kdl"`, which only resolves if it
is read relative to the theme file rather than to the top config."""
- root = tree({"themes/base/core.kdl": 'theme { frame_focus "#ff00ff" }\n',
- "themes/nested.kdl": 'include "base/core.kdl"\n',
- "config.kdl": 'include "themes/nested.kdl"\n'})
+ root = tree(
+ {
+ "themes/base/core.kdl": 'theme { frame_focus "#ff00ff" }\n',
+ "themes/nested.kdl": 'include "base/core.kdl"\n',
+ "config.kdl": 'include "themes/nested.kdl"\n',
+ }
+ )
with Session(SH, cols=60, rows=20, config=root + "/config.kdl") as s:
s.settle()
- check("the nested include resolved", frame_fg(s) == "#ff00ff",
- str(frame_fg(s)))
+ check("the nested include resolved", frame_fg(s) == "#ff00ff", str(frame_fg(s)))
def test_a_leading_tilde_is_a_home_directory():
home = tree({"mytheme.kdl": 'theme { frame_focus "#abcdef" }\n'})
root = tree({"config.kdl": 'include "~/mytheme.kdl"\n'})
- with Session(SH, cols=60, rows=14, config=root + "/config.kdl",
- env={"HOME": home}) as s:
+ with Session(
+ SH, cols=60, rows=14, config=root + "/config.kdl", env={"HOME": home}
+ ) as s:
s.settle()
- check("~ expanded to the home directory", frame_fg(s) == "#abcdef",
- str(frame_fg(s)))
+ check(
+ "~ expanded to the home directory",
+ frame_fg(s) == "#abcdef",
+ str(frame_fg(s)),
+ )
def test_anything_can_be_included_not_only_a_theme():
- root = tree({"keys.kdl": 'keys {\n bind "v" "split-cols"\n}\n',
- "config.kdl": 'include "keys.kdl"\n'})
+ root = tree(
+ {
+ "keys.kdl": 'keys {\n bind "v" "split-cols"\n}\n',
+ "config.kdl": 'include "keys.kdl"\n',
+ }
+ )
with Session(SH, cols=90, rows=14, config=root + "/config.kdl") as s:
s.settle()
s.key("v")
s.settle()
- check("a binding from an included file works", len(s.panes()) == 2,
- str(s.panes()))
+ check(
+ "a binding from an included file works", len(s.panes()) == 2, str(s.panes())
+ )
def test_a_missing_include_costs_a_line_and_no_more():
@@ -129,16 +166,22 @@ def test_a_missing_include_costs_a_line_and_no_more():
with Session(SH, cols=60, rows=14, config=root + "/config.kdl") as s:
s.settle()
check("the session is running", s.alive())
- check("and the rest of the file applied anyway", s.pane()["x"] == 4,
- str(s.pane()))
+ check(
+ "and the rest of the file applied anyway", s.pane()["x"] == 4, str(s.pane())
+ )
reply = s.api("reload")
check("the reload succeeds", reply.get("ok"), str(reply))
- check("...and says which line it could not honour",
- "nope.kdl" in reply.get("warning", ""), str(reply))
- check("the session says it out loud too",
- "nope.kdl" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-200:]))
+ check(
+ "...and says which line it could not honour",
+ "nope.kdl" in reply.get("warning", ""),
+ str(reply),
+ )
+ check(
+ "the session says it out loud too",
+ "nope.kdl" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-200:]),
+ )
def test_a_cycle_says_so():
@@ -146,20 +189,30 @@ def test_a_cycle_says_so():
with Session(SH, cols=70, rows=14, config=root + "/a.kdl") as s:
s.settle()
reply = s.api("reload")
- check("a cycle is reported rather than followed",
- "too deep" in reply.get("warning", ""), str(reply))
+ check(
+ "a cycle is reported rather than followed",
+ "too deep" in reply.get("warning", ""),
+ str(reply),
+ )
def test_a_cycle_stops_rather_than_spinning():
"""A config that includes itself is a mistake, and the depth limit is what
turns it into a message instead of a session that never starts."""
- root = tree({"a.kdl": 'include "b.kdl"\ntheme { frame_focus "#00ff00" }\n',
- "b.kdl": 'include "a.kdl"\n'})
+ root = tree(
+ {
+ "a.kdl": 'include "b.kdl"\ntheme { frame_focus "#00ff00" }\n',
+ "b.kdl": 'include "a.kdl"\n',
+ }
+ )
with Session(SH, cols=60, rows=14, config=root + "/a.kdl") as s:
s.settle()
check("the session started at all", s.alive())
- check("and the file that included itself still applied",
- frame_fg(s) == "#00ff00", str(frame_fg(s)))
+ check(
+ "and the file that included itself still applied",
+ frame_fg(s) == "#00ff00",
+ str(frame_fg(s)),
+ )
def test_an_include_is_picked_up_by_a_reload():
@@ -175,8 +228,11 @@ def test_an_include_is_picked_up_by_a_reload():
f.write('include "themes/other.kdl"\n')
s.api("reload")
s.settle(30)
- check("and blue after the include line changed",
- frame_fg(s) == "#0000ff", str(frame_fg(s)))
+ check(
+ "and blue after the include line changed",
+ frame_fg(s) == "#0000ff",
+ str(frame_fg(s)),
+ )
def test_every_contrib_theme_can_be_included():
@@ -199,9 +255,11 @@ def test_every_contrib_theme_can_be_included():
f.write('include "%s"\n' % path)
s.api("reload")
s.settle(30)
- check(f"{name} applies as an include",
- want and frame_fg(s) == want.group(1).lower(),
- f"{name}: wanted {want and want.group(1)}, got {frame_fg(s)}")
+ check(
+ f"{name} applies as an include",
+ want and frame_fg(s) == want.group(1).lower(),
+ f"{name}: wanted {want and want.group(1)}, got {frame_fg(s)}",
+ )
if __name__ == "__main__":
diff --git a/tests/test_layout.py b/tests/test_layout.py
index 5992168..c2413af 100644
--- a/tests/test_layout.py
+++ b/tests/test_layout.py
@@ -5,6 +5,7 @@ The layout is a pure function of the tree and the rect, so these assertions are
about *invariants* (no overlap, no drift, everything on screen) rather than
remembered geometry. That is the whole point of D6.
"""
+
import sys
from harness import Session, check, report
@@ -31,8 +32,10 @@ def check_invariants(name, s, snap=None):
if overlaps(rs[i], rs[j]):
check(f"{name}: panes do not overlap", False, f"{rs[i]} vs {rs[j]}")
return panes
- on_screen = all(x >= 0 and y >= 0 and x + w <= snap.cols and y + h <= snap.rows
- for x, y, w, h in rs)
+ on_screen = all(
+ x >= 0 and y >= 0 and x + w <= snap.cols and y + h <= snap.rows
+ for x, y, w, h in rs
+ )
check(f"{name}: {len(rs)} panes, no overlap, all on screen", on_screen, str(rs))
return panes
@@ -47,17 +50,23 @@ def test_split_and_close():
s.key("\\\\") # C-a \ -> split into columns
s.settle()
panes = check_invariants("after vsplit", s)
- check("vsplit makes two columns",
- len(panes) == 2 and panes[0]["y"] == panes[1]["y"]
- and panes[0]["x"] != panes[1]["x"], str(rects(panes)))
+ check(
+ "vsplit makes two columns",
+ len(panes) == 2
+ and panes[0]["y"] == panes[1]["y"]
+ and panes[0]["x"] != panes[1]["x"],
+ str(rects(panes)),
+ )
check("the new pane takes focus", panes[1]["focused"])
s.key("-") # split the focused pane into rows
s.settle()
panes = check_invariants("after hsplit", s)
- check("hsplit stacks inside the right column",
- panes[1]["x"] == panes[2]["x"] and panes[1]["y"] != panes[2]["y"],
- str(rects(panes)))
+ check(
+ "hsplit stacks inside the right column",
+ panes[1]["x"] == panes[2]["x"] and panes[1]["y"] != panes[2]["y"],
+ str(rects(panes)),
+ )
s.key("x") # close the focused pane
s.settle()
@@ -67,8 +76,11 @@ def test_split_and_close():
s.key("x")
s.settle()
panes = check_invariants("after second close", s)
- check("closing collapses the split back to one pane",
- len(panes) == 1 and panes[0]["w"] > 40, str(rects(panes)))
+ check(
+ "closing collapses the split back to one pane",
+ len(panes) == 1 and panes[0]["w"] > 40,
+ str(rects(panes)),
+ )
def test_three_way_split_is_even():
@@ -81,8 +93,11 @@ def test_three_way_split_is_even():
s.settle()
panes = check_invariants("three columns", s)
widths = sorted(p["w"] for p in panes)
- check("three splits give three even columns",
- len(panes) == 3 and widths[-1] - widths[0] <= 1, str(widths))
+ check(
+ "three splits give three even columns",
+ len(panes) == 3 and widths[-1] - widths[0] <= 1,
+ str(widths),
+ )
def test_focus_movement():
@@ -94,8 +109,11 @@ def test_focus_movement():
s.key("h") # focus left
s.settle()
left = s.focused()
- check("focus moves left", left["id"] != right["id"] and left["x"] < right["x"],
- f"{left['id']} vs {right['id']}")
+ check(
+ "focus moves left",
+ left["id"] != right["id"] and left["x"] < right["x"],
+ f"{left['id']} vs {right['id']}",
+ )
s.key("l")
s.settle()
@@ -115,12 +133,17 @@ def test_frames_and_gap():
s.settle()
snap = s.snapshot()
pane = s.pane()
- check("frame is drawn", "╭" in snap.line(pane["y"]) and "╯" in snap.line(
- pane["y"] + pane["h"] - 1), repr(snap.line(pane["y"])))
+ check(
+ "frame is drawn",
+ "╭" in snap.line(pane["y"]) and "╯" in snap.line(pane["y"] + pane["h"] - 1),
+ repr(snap.line(pane["y"])),
+ )
check("gap ring is blank", snap.line(0).strip() == "", repr(snap.line(0)))
- check("content sits inside the frame",
- pane["content_x"] == pane["x"] + 1 and pane["content_y"] == pane["y"] + 1,
- str(pane))
+ check(
+ "content sits inside the frame",
+ pane["content_x"] == pane["x"] + 1 and pane["content_y"] == pane["y"] + 1,
+ str(pane),
+ )
with Session(SH, cols=76, rows=12) as s:
s.settle()
@@ -140,9 +163,11 @@ def test_focus_is_visible():
a, b = s.panes()
style_a = snap.style_at(a["x"], a["y"])
style_b = snap.style_at(b["x"], b["y"])
- check("focused and unfocused frames differ",
- style_a and style_b and style_a["fg"] != style_b["fg"],
- f"{style_a} vs {style_b}")
+ check(
+ "focused and unfocused frames differ",
+ style_a and style_b and style_a["fg"] != style_b["fg"],
+ f"{style_a} vs {style_b}",
+ )
def test_hit_list():
@@ -151,22 +176,30 @@ def test_hit_list():
snap = s.snapshot()
pane = s.pane()
- check("pane body registers a hit",
- snap.hit_at(pane["content_x"], pane["content_y"]) == f"pane:{pane['id']}",
- str(snap.hit_at(pane["content_x"], pane["content_y"])))
+ check(
+ "pane body registers a hit",
+ snap.hit_at(pane["content_x"], pane["content_y"]) == f"pane:{pane['id']}",
+ str(snap.hit_at(pane["content_x"], pane["content_y"])),
+ )
# the border is the split target now; the top row is also the drag
# handle, which is why it reports title: rather than border:
- check("the top border is the drag handle",
- snap.hit_at(pane["x"] + 4, pane["y"]) == f"title:{pane['id']}",
- str(snap.hit_at(pane["x"] + 4, pane["y"])))
- check("the side border is a split target",
- snap.hit_at(pane["x"], pane["y"] + 2) == f"border:{pane['id']}:l",
- str(snap.hit_at(pane["x"], pane["y"] + 2)))
- check("the bottom border too",
- snap.hit_at(pane["x"] + 4, pane["y"] + pane["h"] - 1)
- == f"border:{pane['id']}:b",
- str(snap.hit_at(pane["x"] + 4, pane["y"] + pane["h"] - 1)))
+ check(
+ "the top border is the drag handle",
+ snap.hit_at(pane["x"] + 4, pane["y"]) == f"title:{pane['id']}",
+ str(snap.hit_at(pane["x"] + 4, pane["y"])),
+ )
+ check(
+ "the side border is a split target",
+ snap.hit_at(pane["x"], pane["y"] + 2) == f"border:{pane['id']}:l",
+ str(snap.hit_at(pane["x"], pane["y"] + 2)),
+ )
+ check(
+ "the bottom border too",
+ snap.hit_at(pane["x"] + 4, pane["y"] + pane["h"] - 1)
+ == f"border:{pane['id']}:b",
+ str(snap.hit_at(pane["x"] + 4, pane["y"] + pane["h"] - 1)),
+ )
def test_click_focuses_and_forwards():
@@ -181,17 +214,22 @@ def test_click_focuses_and_forwards():
s.settle()
check("clicking a pane focuses it", s.focused()["id"] == left["id"])
- mouse = ["/bin/sh", "-c",
- 'stty raw -echo; printf "\\033[?1000h\\033[?1006h"; cat -v']
+ mouse = [
+ "/bin/sh",
+ "-c",
+ 'stty raw -echo; printf "\\033[?1000h\\033[?1006h"; cat -v',
+ ]
with Session(mouse, cols=60, rows=12) as s:
s.settle()
pane = s.pane()
s.click(pane["content_x"] + 3, pane["content_y"] + 2)
s.settle()
# the pane must see coordinates relative to itself, not the screen
- check("mouse is translated into pane-local coordinates",
- "^[[<0;4;3M" in s.snapshot().pane_text(pane),
- repr(s.snapshot().pane_text(pane)[:60]))
+ check(
+ "mouse is translated into pane-local coordinates",
+ "^[[<0;4;3M" in s.snapshot().pane_text(pane),
+ repr(s.snapshot().pane_text(pane)[:60]),
+ )
def test_resize_keeps_invariants():
diff --git a/tests/test_layout_files.py b/tests/test_layout_files.py
index f396b07..236fcbd 100644
--- a/tests/test_layout_files.py
+++ b/tests/test_layout_files.py
@@ -5,6 +5,7 @@ A layout declares tabs, purposes and panes; the purposes it declares are
locked, and commands can be suspended so that opening twelve projects does not
start twelve servers.
"""
+
import os
import sys
import tempfile
@@ -47,29 +48,45 @@ def test_structure():
with session_with(LAYOUT, cols=78, rows=18) as s:
s.settle()
tabs = s.tabs()
- check("a layout builds its tabs", [t["name"] for t in tabs] == ["api", "notes"],
- str(tabs))
- check("and only its tabs (the default one is replaced)", len(tabs) == 2,
- str(tabs))
- check("tab purposes are declared, so nothing can override them",
- tabs[0]["purpose"] == "project:api.a1b2" and tabs[0]["purpose_declared"],
- str(tabs[0]))
+ check(
+ "a layout builds its tabs",
+ [t["name"] for t in tabs] == ["api", "notes"],
+ str(tabs),
+ )
+ check(
+ "and only its tabs (the default one is replaced)", len(tabs) == 2, str(tabs)
+ )
+ check(
+ "tab purposes are declared, so nothing can override them",
+ tabs[0]["purpose"] == "project:api.a1b2" and tabs[0]["purpose_declared"],
+ str(tabs[0]),
+ )
panes = s.panes()
check("panes are built in every tab", len(panes) == 4, str(len(panes)))
purposes = [p["purpose"] for p in panes if p["tab"] == 1]
- check("pane purposes come from the layout",
- purposes == ["agent:main", "service:web", "logs:tail"], str(purposes))
- check("pane purposes are declared too",
- all(p["purpose_declared"] for p in panes if p["purpose"]), str(panes))
+ check(
+ "pane purposes come from the layout",
+ purposes == ["agent:main", "service:web", "logs:tail"],
+ str(purposes),
+ )
+ check(
+ "pane purposes are declared too",
+ all(p["purpose_declared"] for p in panes if p["purpose"]),
+ str(panes),
+ )
first, web, logs = panes[0], panes[1], panes[2]
- check("the tab body splits into columns by default",
- first["x"] < web["x"] and first["y"] == web["y"],
- f"{first['x']},{first['y']} vs {web['x']},{web['y']}")
- check("a nested pane with split=rows stacks its children",
- web["x"] == logs["x"] and web["y"] < logs["y"],
- f"{web['y']} vs {logs['y']}")
+ check(
+ "the tab body splits into columns by default",
+ first["x"] < web["x"] and first["y"] == web["y"],
+ f"{first['x']},{first['y']} vs {web['x']},{web['y']}",
+ )
+ check(
+ "a nested pane with split=rows stacks its children",
+ web["x"] == logs["x"] and web["y"] < logs["y"],
+ f"{web['y']} vs {logs['y']}",
+ )
os.unlink(s._layout_path)
@@ -79,27 +96,38 @@ def test_suspended():
panes = s.panes()
web = [p for p in panes if p["purpose"] == "service:web"][0]
check("a suspended pane is marked as such", web["suspended"], str(web))
- check("others are not",
- not [p for p in panes if p["purpose"] == "agent:main"][0]["suspended"])
+ check(
+ "others are not",
+ not [p for p in panes if p["purpose"] == "agent:main"][0]["suspended"],
+ )
snap = s.snapshot()
- check("a suspended pane says what it would run",
- "press a key to run:" in snap.screen(), repr(snap.screen()[:400]))
- check("but has not run it", "web-running" not in snap.pane_text(web),
- repr(snap.pane_text(web)))
+ check(
+ "a suspended pane says what it would run",
+ "press a key to run:" in snap.screen(),
+ repr(snap.screen()[:400]),
+ )
+ check(
+ "but has not run it",
+ "web-running" not in snap.pane_text(web),
+ repr(snap.pane_text(web)),
+ )
s.api("focus", id=web["id"])
s.send("x")
s.settle()
now = [p for p in s.panes() if p["id"] == web["id"]]
- check("a keystroke starts it", now and not now[0]["suspended"],
- str(s.panes()))
- check("and the command actually runs",
- "web-running" in s.snapshot().pane_text(now[0]),
- repr(s.snapshot().pane_text(now[0])))
- check("the keystroke that started it is not also sent to it",
- "x" not in s.snapshot().pane_text(now[0]).replace("web-running", ""),
- repr(s.snapshot().pane_text(now[0])))
+ check("a keystroke starts it", now and not now[0]["suspended"], str(s.panes()))
+ check(
+ "and the command actually runs",
+ "web-running" in s.snapshot().pane_text(now[0]),
+ repr(s.snapshot().pane_text(now[0])),
+ )
+ check(
+ "the keystroke that started it is not also sent to it",
+ "x" not in s.snapshot().pane_text(now[0]).replace("web-running", ""),
+ repr(s.snapshot().pane_text(now[0])),
+ )
check("the session is unharmed", s.api("alive")["ok"])
os.unlink(s._layout_path)
@@ -108,16 +136,31 @@ def test_declared_purposes_are_locked():
with session_with(LAYOUT, cols=78, rows=18) as s:
s.settle()
agent = [p for p in s.panes() if p["purpose"] == "agent:main"][0]
- r = s.api("set-purpose", target="pane", id=agent["id"],
- purpose="evil:relabelled", declared=False)
- check("a layout-declared pane purpose cannot be taken in-band",
- not r["ok"], str(r))
- check("it is unchanged",
- [p for p in s.panes() if p["id"] == agent["id"]][0]["purpose"]
- == "agent:main")
+ r = s.api(
+ "set-purpose",
+ target="pane",
+ id=agent["id"],
+ purpose="evil:relabelled",
+ declared=False,
+ )
+ check(
+ "a layout-declared pane purpose cannot be taken in-band",
+ not r["ok"],
+ str(r),
+ )
+ check(
+ "it is unchanged",
+ [p for p in s.panes() if p["id"] == agent["id"]][0]["purpose"]
+ == "agent:main",
+ )
- r = s.api("set-purpose", target="tab", id=s.tabs()[0]["id"],
- purpose="evil:tab", declared=False)
+ r = s.api(
+ "set-purpose",
+ target="tab",
+ id=s.tabs()[0]["id"],
+ purpose="evil:tab",
+ declared=False,
+ )
check("nor a tab purpose", not r["ok"], str(r))
os.unlink(s._layout_path)
@@ -135,10 +178,16 @@ def test_cwd():
s.settle()
panes = s.panes()
snap = s.snapshot()
- check("a tab's cwd is inherited by its panes",
- "/tmp" in snap.pane_text(panes[0]), repr(snap.pane_text(panes[0])))
- check("a pane can override it",
- "/usr" in snap.pane_text(panes[1]), repr(snap.pane_text(panes[1])))
+ check(
+ "a tab's cwd is inherited by its panes",
+ "/tmp" in snap.pane_text(panes[0]),
+ repr(snap.pane_text(panes[0])),
+ )
+ check(
+ "a pane can override it",
+ "/usr" in snap.pane_text(panes[1]),
+ repr(snap.pane_text(panes[1])),
+ )
os.unlink(s._layout_path)
@@ -162,11 +211,16 @@ def test_cwd_expands_a_tilde():
s.settle()
panes = s.panes()
snap = s.snapshot()
- check("~ is the home directory",
- home in snap.pane_text(panes[0]), repr(snap.pane_text(panes[0])))
- check("and ~/sub is under it",
- os.path.join(home, "sub") in snap.pane_text(panes[1]),
- repr(snap.pane_text(panes[1])))
+ check(
+ "~ is the home directory",
+ home in snap.pane_text(panes[0]),
+ repr(snap.pane_text(panes[0])),
+ )
+ check(
+ "and ~/sub is under it",
+ os.path.join(home, "sub") in snap.pane_text(panes[1]),
+ repr(snap.pane_text(panes[1])),
+ )
os.unlink(s._layout_path)
@@ -176,39 +230,57 @@ def test_apply_over_the_api():
s.settle()
check("one tab to start", len(s.tabs()) == 1)
- frag = ('layout { tab name="added" purpose="project:x.deadbeef" '
- '{ pane purpose="agent:main" } }')
+ frag = (
+ 'layout { tab name="added" purpose="project:x.deadbeef" '
+ '{ pane purpose="agent:main" } }'
+ )
r = s.api("apply-layout", kdl=frag)
s.settle()
- check("apply-layout answers with the tab list", r["ok"] and "tabs" in r,
- str(r)[:100])
- check("the tab is added, not replacing what was there",
- len(s.tabs()) == 2 and s.tabs()[1]["name"] == "added", str(s.tabs()))
- check("its purpose is declared",
- s.tabs()[1]["purpose"] == "project:x.deadbeef"
- and s.tabs()[1]["purpose_declared"], str(s.tabs()[1]))
+ check(
+ "apply-layout answers with the tab list",
+ r["ok"] and "tabs" in r,
+ str(r)[:100],
+ )
+ check(
+ "the tab is added, not replacing what was there",
+ len(s.tabs()) == 2 and s.tabs()[1]["name"] == "added",
+ str(s.tabs()),
+ )
+ check(
+ "its purpose is declared",
+ s.tabs()[1]["purpose"] == "project:x.deadbeef"
+ and s.tabs()[1]["purpose_declared"],
+ str(s.tabs()[1]),
+ )
# idempotency belongs to the caller: it diffs, then adds what is missing
have = {t["purpose"] for t in s.tabs()}
if "project:x.deadbeef" not in have:
s.api("apply-layout", kdl=frag)
- check("a caller that diffs first does not duplicate", len(s.tabs()) == 2,
- str(s.tabs()))
+ check(
+ "a caller that diffs first does not duplicate",
+ len(s.tabs()) == 2,
+ str(s.tabs()),
+ )
r = s.api("close-tab", id=s.tabs()[1]["id"])
check("close-tab removes it", r["ok"] and len(s.tabs()) == 1, str(s.tabs()))
- r = s.api("apply-layout", kdl='layout { tab name="only" { pane } }',
- replace=True)
+ r = s.api(
+ "apply-layout", kdl='layout { tab name="only" { pane } }', replace=True
+ )
s.settle()
- check("replace=true rebuilds the session",
- len(s.tabs()) == 1 and s.tabs()[0]["name"] == "only", str(s.tabs()))
+ check(
+ "replace=true rebuilds the session",
+ len(s.tabs()) == 1 and s.tabs()[0]["name"] == "only",
+ str(s.tabs()),
+ )
def test_bad_layouts():
with Session(SH, cols=70, rows=14) as s:
s.settle()
- r = s.api("apply-layout", kdl="layout { tab name=\"x\" ")
+ r = s.api("apply-layout", kdl='layout { tab name="x" ')
check("a broken layout is refused", not r["ok"], str(r))
check("with a line number", "line" in str(r.get("error", "")), str(r))
@@ -218,14 +290,20 @@ def test_bad_layouts():
r = s.api("apply-layout", path="/nonexistent/layout.kdl")
check("a missing file is refused", not r["ok"], str(r))
- check("the session survived all of it",
- s.api("alive")["alive"] and len(s.tabs()) == 1, str(s.tabs()))
+ check(
+ "the session survived all of it",
+ s.api("alive")["alive"] and len(s.tabs()) == 1,
+ str(s.tabs()),
+ )
- broken = layout_file("layout { tab name=\"x\"")
+ broken = layout_file('layout { tab name="x"')
with Session(SH, cols=70, rows=14, layout=broken) as s:
s.settle()
- check("a broken layout at startup still gives you a session",
- s.api("alive")["alive"] and len(s.panes()) == 1, str(s.panes()))
+ check(
+ "a broken layout at startup still gives you a session",
+ s.api("alive")["alive"] and len(s.panes()) == 1,
+ str(s.panes()),
+ )
os.unlink(broken)
diff --git a/tests/test_minimize.py b/tests/test_minimize.py
index 979354a..55f6897 100644
--- a/tests/test_minimize.py
+++ b/tests/test_minimize.py
@@ -7,6 +7,7 @@ state, so what is worth checking is that it survives the layout being
recomputed, that there is always a way back, and that it does not invent a
state with nothing on screen.
"""
+
import os
import sys
import tempfile
@@ -48,8 +49,9 @@ def bar_entries(s):
y = bar_row(s)
if y is None:
return []
- return [h for h in s.snapshot().hits
- if h["y"] == y and h["action"].startswith("focus:")]
+ return [
+ h for h in s.snapshot().hits if h["y"] == y and h["action"].startswith("focus:")
+ ]
def minimize_focused(s):
@@ -65,14 +67,18 @@ def test_minimising_takes_a_pane_out_of_the_layout():
before = max(p["w"] for p in open_panes(s))
minimize_focused(s)
- check("one pane is gone from the layout", len(put_away(s)) == 1,
- str(here(s)))
- check("and the other took the space back",
- open_panes(s)[0]["w"] > before, str(here(s)))
- check("it is listed on the bar", len(bar_entries(s)) == 1,
- str(bar_entries(s)))
- check("which sits below the panes",
- bar_entries(s)[0]["y"] == bar_row(s), str(bar_entries(s)))
+ check("one pane is gone from the layout", len(put_away(s)) == 1, str(here(s)))
+ check(
+ "and the other took the space back",
+ open_panes(s)[0]["w"] > before,
+ str(here(s)),
+ )
+ check("it is listed on the bar", len(bar_entries(s)) == 1, str(bar_entries(s)))
+ check(
+ "which sits below the panes",
+ bar_entries(s)[0]["y"] == bar_row(s),
+ str(bar_entries(s)),
+ )
def test_the_bar_is_one_row_however_many_are_put_away():
@@ -93,12 +99,15 @@ def test_the_bar_is_one_row_however_many_are_put_away():
three = open_panes(s)[0]["h"]
check("three are put away", len(put_away(s)) == 3, str(here(s)))
- check("all three are listed", len(bar_entries(s)) == 3,
- str(bar_entries(s)))
- check("and the survivor is no shorter than with one put away",
- three == one, f"{one} -> {three}")
- check("because the bar was one row either way", entries_one == 1,
- str(entries_one))
+ check("all three are listed", len(bar_entries(s)) == 3, str(bar_entries(s)))
+ check(
+ "and the survivor is no shorter than with one put away",
+ three == one,
+ f"{one} -> {three}",
+ )
+ check(
+ "because the bar was one row either way", entries_one == 1, str(entries_one)
+ )
def test_clicking_an_entry_brings_that_pane_back():
@@ -111,16 +120,19 @@ def test_clicking_an_entry_brings_that_pane_back():
minimize_focused(s)
gone = put_away(s)[0]["id"]
e = [h for h in bar_entries(s) if h["action"] == f"focus:{gone}"]
- check("the entry names the pane it stands for", len(e) == 1,
- str(bar_entries(s)))
+ check(
+ "the entry names the pane it stands for", len(e) == 1, str(bar_entries(s))
+ )
if not e:
return
s.click(e[0]["x"] + 1, e[0]["y"])
s.settle(20)
check("clicking it restores the pane", put_away(s) == [], str(here(s)))
- check("and the layout is the one it left",
- sorted((p["x"], p["w"]) for p in open_panes(s)) == layout_before,
- str(here(s)))
+ check(
+ "and the layout is the one it left",
+ sorted((p["x"], p["w"]) for p in open_panes(s)) == layout_before,
+ str(here(s)),
+ )
def test_focusing_it_by_any_route_brings_it_back():
@@ -135,8 +147,11 @@ def test_focusing_it_by_any_route_brings_it_back():
s.api("focus", id=gone)
s.settle(20)
- check("focusing it over the control API restores it",
- put_away(s) == [], str(here(s)))
+ check(
+ "focusing it over the control API restores it",
+ put_away(s) == [],
+ str(here(s)),
+ )
def test_it_will_not_leave_the_tab_empty():
@@ -144,18 +159,19 @@ def test_it_will_not_leave_the_tab_empty():
s.settle(20)
check("one pane to start", len(here(s)) == 1, str(here(s)))
minimize_focused(s)
- check("minimising the only pane is refused", put_away(s) == [],
- str(here(s)))
- check("and it says why", "nothing else" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-200:]))
+ check("minimising the only pane is refused", put_away(s) == [], str(here(s)))
+ check(
+ "and it says why",
+ "nothing else" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-200:]),
+ )
s.api("split", dir="cols")
s.settle(20)
minimize_focused(s)
check("with two, one can go", len(put_away(s)) == 1, str(here(s)))
minimize_focused(s)
- check("but the last one standing cannot", len(put_away(s)) == 1,
- str(here(s)))
+ check("but the last one standing cannot", len(put_away(s)) == 1, str(here(s)))
def test_the_button_minimises_that_pane():
@@ -164,15 +180,17 @@ def test_the_button_minimises_that_pane():
s.api("split", dir="cols")
s.settle(20)
target = here(s)[0]["id"]
- b = [h for h in s.snapshot().hits
- if h["action"] == f"minimize:{target}"]
+ b = [h for h in s.snapshot().hits if h["action"] == f"minimize:{target}"]
check("a pane's frame carries a minimise button", len(b) == 1, str(b))
if not b:
return
s.click(b[0]["x"] + 1, b[0]["y"])
s.settle(20)
- check("clicking it puts that pane away",
- [p["id"] for p in put_away(s)] == [target], str(here(s)))
+ check(
+ "clicking it puts that pane away",
+ [p["id"] for p in put_away(s)] == [target],
+ str(here(s)),
+ )
def test_it_has_no_say_once_the_tab_is_a_list():
@@ -189,13 +207,19 @@ def test_it_has_no_say_once_the_tab_is_a_list():
s.settle(20)
rows = here(s)
check("the tab is a list", any(p["hidden"] for p in rows), str(rows))
- check("and every pane is in it, minimised or not",
- all(p["w"] > 1 for p in rows), str(rows))
+ check(
+ "and every pane is in it, minimised or not",
+ all(p["w"] > 1 for p in rows),
+ str(rows),
+ )
s.resize(90, 20)
s.settle(20)
- check("coming back, it is put away again and not lost",
- len(put_away(s)) == 1 and len(open_panes(s)) == 1, str(here(s)))
+ check(
+ "coming back, it is put away again and not lost",
+ len(put_away(s)) == 1 and len(open_panes(s)) == 1,
+ str(here(s)),
+ )
def test_zoom_hides_the_bar():
@@ -208,12 +232,18 @@ def test_zoom_hides_the_bar():
check("a bar to start", len(bar_entries(s)) == 1, str(bar_entries(s)))
s.send(r"\x01z")
s.settle(20)
- check("zooming shows one pane and nothing else, bar included",
- bar_entries(s) == [], str(bar_entries(s)))
+ check(
+ "zooming shows one pane and nothing else, bar included",
+ bar_entries(s) == [],
+ str(bar_entries(s)),
+ )
s.send(r"\x01z")
s.settle(20)
- check("and unzooming gives the bar back", len(bar_entries(s)) == 1,
- str(bar_entries(s)))
+ check(
+ "and unzooming gives the bar back",
+ len(bar_entries(s)) == 1,
+ str(bar_entries(s)),
+ )
if __name__ == "__main__":
diff --git a/tests/test_move_pane.py b/tests/test_move_pane.py
index 7b6310b..3f1035b 100755
--- a/tests/test_move_pane.py
+++ b/tests/test_move_pane.py
@@ -6,6 +6,7 @@ What matters here is that the tree surgery is sound and that **the program in th
pane never notices** -- a move is the same pty in a different tab, not a new pane
somewhere else and a funeral here. Every check below is about one of those two.
"""
+
import os
import sys
import tempfile
@@ -40,9 +41,9 @@ def by_tab(s):
def two_tabs(s):
"""Two panes in the first tab, one in a second. Returns (tab ids, pane ids)."""
s.settle()
- s.key("\\\\") # split: two panes here
+ s.key("\\\\") # split: two panes here
s.settle(40)
- s.key("c") # a second tab, with one
+ s.key("c") # a second tab, with one
s.settle(40)
tabs = [t["id"] for t in s.tabs()]
return tabs
@@ -62,11 +63,16 @@ def test_a_pane_moves_and_keeps_running():
s.settle(60)
after = by_tab(s)
check("the move is accepted", reply.get("ok") is True, str(reply))
- check("the pane is in the other tab now",
- mover in after[tabs[1]] and mover not in after[tabs[0]], str(after))
- check("and no pane was created or lost",
- sum(len(v) for v in after.values()) == sum(len(v) for v in before.values()),
- "%s -> %s" % (before, after))
+ check(
+ "the pane is in the other tab now",
+ mover in after[tabs[1]] and mover not in after[tabs[0]],
+ str(after),
+ )
+ check(
+ "and no pane was created or lost",
+ sum(len(v) for v in after.values()) == sum(len(v) for v in before.values()),
+ "%s -> %s" % (before, after),
+ )
# Talk to it where it landed: same pty, same shell, still reading.
s.api("select-tab", id=tabs[1])
@@ -75,8 +81,11 @@ def test_a_pane_moves_and_keeps_running():
s.raw("still-here\\n")
snap = s.until_text("still-here")
pane = [p for p in s.panes() if p["id"] == mover][0]
- check("the program in it is the one that was there before",
- "still-here" in snap.pane_text(pane), repr(snap.pane_text(pane)[:120]))
+ check(
+ "the program in it is the one that was there before",
+ "still-here" in snap.pane_text(pane),
+ repr(snap.pane_text(pane)[:120]),
+ )
check("and it is alive", pane["alive"] is True, str(pane))
@@ -92,22 +101,36 @@ def test_it_lands_beside_the_destination_focus():
s.api("move-pane", id=mover, tab=tabs[1], dir="cols")
s.api("select-tab", id=tabs[1])
s.settle(60)
- rects = {p["id"]: (p["x"], p["y"], p["w"], p["h"]) for p in s.panes()
- if p["tab_id"] == tabs[1]}
+ rects = {
+ p["id"]: (p["x"], p["y"], p["w"], p["h"])
+ for p in s.panes()
+ if p["tab_id"] == tabs[1]
+ }
check("both panes are in the destination", len(rects) == 2, str(rects))
- check("side by side, the arrival on the right",
- rects[mover][0] > rects[host][0], str(rects))
- check("and the arrival has the focus",
- [p["focused"] for p in s.panes() if p["id"] == mover] == [True],
- str([p for p in s.panes() if p["id"] == mover]))
+ check(
+ "side by side, the arrival on the right",
+ rects[mover][0] > rects[host][0],
+ str(rects),
+ )
+ check(
+ "and the arrival has the focus",
+ [p["focused"] for p in s.panes() if p["id"] == mover] == [True],
+ str([p for p in s.panes() if p["id"] == mover]),
+ )
s.api("move-pane", id=mover, tab=tabs[0], dir="rows")
s.api("select-tab", id=tabs[0])
s.settle(60)
- rects = {p["id"]: (p["x"], p["y"], p["w"], p["h"]) for p in s.panes()
- if p["tab_id"] == tabs[0]}
- check("`rows` puts it underneath",
- rects[mover][1] > min(r[1] for r in rects.values()), str(rects))
+ rects = {
+ p["id"]: (p["x"], p["y"], p["w"], p["h"])
+ for p in s.panes()
+ if p["tab_id"] == tabs[0]
+ }
+ check(
+ "`rows` puts it underneath",
+ rects[mover][1] > min(r[1] for r in rects.values()),
+ str(rects),
+ )
def test_emptying_a_tab_removes_it():
@@ -118,16 +141,22 @@ def test_emptying_a_tab_removes_it():
with Session(ECHO, cols=90, rows=16) as s:
tabs = two_tabs(s)
s.until_text("alive")
- lonely = by_tab(s)[tabs[1]][0] # the only pane of the *second* tab
+ lonely = by_tab(s)[tabs[1]][0] # the only pane of the *second* tab
reply = s.api("move-pane", id=lonely, tab=tabs[0])
s.settle(60)
after = by_tab(s)
check("the move worked", reply.get("ok") is True, str(reply))
- check("the tab it left is gone", tabs[1] not in [t["id"] for t in s.tabs()],
- str([t["id"] for t in s.tabs()]))
- check("and everything is in the one that is left",
- sorted(after[tabs[0]]) == sorted(sum(after.values(), [])), str(after))
+ check(
+ "the tab it left is gone",
+ tabs[1] not in [t["id"] for t in s.tabs()],
+ str([t["id"] for t in s.tabs()]),
+ )
+ check(
+ "and everything is in the one that is left",
+ sorted(after[tabs[0]]) == sorted(sum(after.values(), [])),
+ str(after),
+ )
check("three panes, none lost", len(after[tabs[0]]) == 3, str(after))
@@ -142,16 +171,23 @@ def test_a_pane_can_be_moved_into_a_tab_of_its_own():
s.settle(60)
made = reply.get("tab")
check("it answers with the tab it made", reply.get("ok") and made, str(reply))
- check("the pane is the only thing in it", by_tab(s).get(made) == [mover],
- str(by_tab(s)))
- check("and the tab has the name it was given",
- [t["name"] for t in s.tabs() if t["id"] == made] == ["solo"],
- str(s.tabs()))
+ check(
+ "the pane is the only thing in it",
+ by_tab(s).get(made) == [mover],
+ str(by_tab(s)),
+ )
+ check(
+ "and the tab has the name it was given",
+ [t["name"] for t in s.tabs() if t["id"] == made] == ["solo"],
+ str(s.tabs()),
+ )
# A move does not follow the pane: which tab you are looking at is a
# question about intent, and this is the mechanism.
- check("we are still looking at the tab we were looking at",
- [t["id"] for t in s.tabs() if t.get("active")] == watching,
- str([t["id"] for t in s.tabs() if t.get("active")]))
+ check(
+ "we are still looking at the tab we were looking at",
+ [t["id"] for t in s.tabs() if t.get("active")] == watching,
+ str([t["id"] for t in s.tabs() if t.get("active")]),
+ )
def test_the_refusals():
@@ -162,19 +198,31 @@ def test_the_refusals():
here = by_tab(s)[tabs[0]][0]
lonely = by_tab(s)[tabs[1]][0]
- check("no such pane", s.api("move-pane", id=999, tab=tabs[1]).get("ok")
- is False, "")
- check("no such tab", s.api("move-pane", id=here, tab=999).get("ok")
- is False, "")
- check("already in that tab",
- s.api("move-pane", id=here, tab=tabs[0]).get("ok") is False, "")
+ check(
+ "no such pane",
+ s.api("move-pane", id=999, tab=tabs[1]).get("ok") is False,
+ "",
+ )
+ check(
+ "no such tab", s.api("move-pane", id=here, tab=999).get("ok") is False, ""
+ )
+ check(
+ "already in that tab",
+ s.api("move-pane", id=here, tab=tabs[0]).get("ok") is False,
+ "",
+ )
# A pane alone in its tab is already in a tab of its own; making it another
# one would remove the tab it is in and add an identical one.
- check("a pane alone in its tab has nowhere of its own to go",
- s.api("move-pane", id=lonely, tab=0).get("ok") is False, "")
- check("and none of that moved anything",
- by_tab(s) == {tabs[0]: sorted(by_tab(s)[tabs[0]]),
- tabs[1]: [lonely]}, str(by_tab(s)))
+ check(
+ "a pane alone in its tab has nowhere of its own to go",
+ s.api("move-pane", id=lonely, tab=0).get("ok") is False,
+ "",
+ )
+ check(
+ "and none of that moved anything",
+ by_tab(s) == {tabs[0]: sorted(by_tab(s)[tabs[0]]), tabs[1]: [lonely]},
+ str(by_tab(s)),
+ )
def test_a_move_drops_what_the_old_tab_thought():
@@ -187,19 +235,24 @@ def test_a_move_drops_what_the_old_tab_thought():
mover = by_tab(s)[tabs[0]][1]
s.api("focus", id=mover)
- s.key("m") # minimise it
+ s.key("m") # minimise it
s.settle(40)
- check("it is put away", [p for p in s.panes() if p["id"] == mover][0]["h"] <= 1,
- str([p for p in s.panes() if p["id"] == mover]))
+ check(
+ "it is put away",
+ [p for p in s.panes() if p["id"] == mover][0]["h"] <= 1,
+ str([p for p in s.panes() if p["id"] == mover]),
+ )
s.api("move-pane", id=mover, tab=tabs[1])
s.api("select-tab", id=tabs[1])
s.settle(60)
pane = [p for p in s.panes() if p["id"] == mover][0]
check("it arrives laid out, not filed away", pane["h"] > 1, str(pane))
- check("and the tab it left is not zoomed onto a pane that has gone",
- all(p["h"] > 1 for p in s.panes() if p["tab_id"] == tabs[0]),
- str([p for p in s.panes() if p["tab_id"] == tabs[0]]))
+ check(
+ "and the tab it left is not zoomed onto a pane that has gone",
+ all(p["h"] > 1 for p in s.panes() if p["tab_id"] == tabs[0]),
+ str([p for p in s.panes() if p["tab_id"] == tabs[0]]),
+ )
def test_the_layout_survives_a_move():
@@ -208,7 +261,7 @@ def test_the_layout_survives_a_move():
with Session(ECHO, cols=100, rows=20) as s:
tabs = two_tabs(s)
s.until_text("alive")
- s.key("-") # a third pane in the first tab
+ s.key("-") # a third pane in the first tab
s.settle(40)
for target in (tabs[1], tabs[0], tabs[1]):
@@ -218,26 +271,36 @@ def test_the_layout_survives_a_move():
s.api("move-pane", id=movable[0], tab=target)
s.settle(60)
- rs = [(p["x"], p["y"], p["w"], p["h"]) for p in s.panes()
- if p["tab_id"] == [t["id"] for t in s.tabs() if t.get("active")][0]]
+ rs = [
+ (p["x"], p["y"], p["w"], p["h"])
+ for p in s.panes()
+ if p["tab_id"] == [t["id"] for t in s.tabs() if t.get("active")][0]
+ ]
for i in range(len(rs)):
for j in range(i + 1, len(rs)):
ax, ay, aw, ah = rs[i]
bx, by, bw, bh = rs[j]
if ax < bx + bw and bx < ax + aw and ay < by + bh and by < ay + ah:
- check("panes do not overlap after moving", False,
- "%s vs %s" % (rs[i], rs[j]))
+ check(
+ "panes do not overlap after moving",
+ False,
+ "%s vs %s" % (rs[i], rs[j]),
+ )
return
check("the visible tab is a sane layout after three moves", True, str(rs))
dump = s.api("dump-layout").get("kdl", "")
- check("and it dumps as a layout", "layout {" in dump and "pane" in dump,
- repr(dump[:120]))
+ check(
+ "and it dumps as a layout",
+ "layout {" in dump and "pane" in dump,
+ repr(dump[:120]),
+ )
path = cfg(dump)
reply = s.api("apply-layout", path=path, replace=True)
s.settle(60)
- check("that the session can be rebuilt from", reply.get("ok") is True,
- str(reply))
+ check(
+ "that the session can be rebuilt from", reply.get("ok") is True, str(reply)
+ )
os.unlink(path)
@@ -250,26 +313,38 @@ def test_the_keys_push_a_pane_a_tab_along():
s.api("select-tab", id=tabs[0])
s.settle(40)
watching = [t["id"] for t in s.tabs() if t.get("active")]
- moved = [p["id"] for p in s.panes()
- if p["tab_id"] == tabs[0] and p["focused"]][0]
+ moved = [p["id"] for p in s.panes() if p["tab_id"] == tabs[0] and p["focused"]][
+ 0
+ ]
s.key(">")
s.settle(60)
- check("`C-a >` moves the focused pane to the next tab",
- moved in by_tab(s)[tabs[1]], str(by_tab(s)))
- check("and says where it went", "moved to tab" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-120:]))
- check("without following it",
- [t["id"] for t in s.tabs() if t.get("active")] == watching,
- str([t["id"] for t in s.tabs() if t.get("active")]))
+ check(
+ "`C-a >` moves the focused pane to the next tab",
+ moved in by_tab(s)[tabs[1]],
+ str(by_tab(s)),
+ )
+ check(
+ "and says where it went",
+ "moved to tab" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-120:]),
+ )
+ check(
+ "without following it",
+ [t["id"] for t in s.tabs() if t.get("active")] == watching,
+ str([t["id"] for t in s.tabs() if t.get("active")]),
+ )
s.api("select-tab", id=tabs[1])
s.api("focus", id=moved)
s.settle(40)
s.key("<")
s.settle(60)
- check("`C-a <` sends it back the other way",
- moved in by_tab(s)[tabs[0]], str(by_tab(s)))
+ check(
+ "`C-a <` sends it back the other way",
+ moved in by_tab(s)[tabs[0]],
+ str(by_tab(s)),
+ )
def test_the_key_for_a_tab_of_its_own():
@@ -278,17 +353,20 @@ def test_the_key_for_a_tab_of_its_own():
s.until_text("alive")
s.api("select-tab", id=tabs[0])
s.settle(40)
- moved = [p["id"] for p in s.panes()
- if p["tab_id"] == tabs[0] and p["focused"]][0]
+ moved = [p["id"] for p in s.panes() if p["tab_id"] == tabs[0] and p["focused"]][
+ 0
+ ]
s.key("b")
s.settle(60)
made = [t["id"] for t in s.tabs() if t["id"] not in tabs]
check("`C-a b` breaks it out into a new tab", len(made) == 1, str(s.tabs()))
- check("with the pane in it", by_tab(s).get(made[0]) == [moved],
- str(by_tab(s)))
- check("and says so", "tab of its own" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-120:]))
+ check("with the pane in it", by_tab(s).get(made[0]) == [moved], str(by_tab(s)))
+ check(
+ "and says so",
+ "tab of its own" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-120:]),
+ )
def test_one_tab_has_nowhere_to_push_to():
@@ -301,8 +379,11 @@ def test_one_tab_has_nowhere_to_push_to():
s.key(">")
s.settle(60)
check("nothing moved", by_tab(s) == before, "%s -> %s" % (before, by_tab(s)))
- check("and it says why", "only one tab" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-120:]))
+ check(
+ "and it says why",
+ "only one tab" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-120:]),
+ )
def test_pushing_the_last_pane_out_takes_the_tab_with_it():
@@ -312,17 +393,25 @@ def test_pushing_the_last_pane_out_takes_the_tab_with_it():
with Session(ECHO, cols=90, rows=16) as s:
tabs = two_tabs(s)
s.until_text("alive")
- s.api("select-tab", id=tabs[1]) # the tab with one pane in it
+ s.api("select-tab", id=tabs[1]) # the tab with one pane in it
s.settle(40)
s.key(">")
s.settle(60)
- check("the tab it emptied is gone",
- [t["id"] for t in s.tabs()] == [tabs[0]], str(s.tabs()))
- check("all three panes are in the survivor",
- len(by_tab(s)[tabs[0]]) == 3, str(by_tab(s)))
- check("and that is what we are looking at",
- [t["id"] for t in s.tabs() if t.get("active")] == [tabs[0]],
- str(s.tabs()))
+ check(
+ "the tab it emptied is gone",
+ [t["id"] for t in s.tabs()] == [tabs[0]],
+ str(s.tabs()),
+ )
+ check(
+ "all three panes are in the survivor",
+ len(by_tab(s)[tabs[0]]) == 3,
+ str(by_tab(s)),
+ )
+ check(
+ "and that is what we are looking at",
+ [t["id"] for t in s.tabs() if t.get("active")] == [tabs[0]],
+ str(s.tabs()),
+ )
def test_the_sheet_and_the_palette_know_about_them():
@@ -334,8 +423,11 @@ def test_the_sheet_and_the_palette_know_about_them():
s.key("?")
s.settle(60)
sheet = s.snapshot().screen()
- check("the sheet lists pushing a pane along", "tab after" in sheet,
- repr([l.strip() for l in s.snapshot().text if "tab after" in l]))
+ check(
+ "the sheet lists pushing a pane along",
+ "tab after" in sheet,
+ repr([l.strip() for l in s.snapshot().text if "tab after" in l]),
+ )
check("under the key you would type", ">" in sheet, "")
s.send("\\x1b")
s.settle(40)
@@ -345,8 +437,11 @@ def test_the_sheet_and_the_palette_know_about_them():
s.send("tab of its own")
s.settle(60)
pal = s.snapshot().screen()
- check("and the palette finds it by its label", "tab of its own" in pal,
- repr([l.strip() for l in s.snapshot().text if "own" in l]))
+ check(
+ "and the palette finds it by its label",
+ "tab of its own" in pal,
+ repr([l.strip() for l in s.snapshot().text if "own" in l]),
+ )
# ---- dragging one onto the strip ----------------------------------------------
@@ -357,13 +452,15 @@ def test_the_sheet_and_the_palette_know_about_them():
def strip_hits(s):
"""Where the tabs and the `+` are, by action."""
- return {h["action"]: h for h in s.snapshot().hits
- if h["action"].startswith("tab:") or h["action"] == "newtab"}
+ return {
+ h["action"]: h
+ for h in s.snapshot().hits
+ if h["action"].startswith("tab:") or h["action"] == "newtab"
+ }
def title_of(s, pane_id):
- return [h for h in s.snapshot().hits
- if h["action"] == "title:%d" % pane_id][0]
+ return [h for h in s.snapshot().hits if h["action"] == "title:%d" % pane_id][0]
def drag_pane_to(s, pane_id, hit, release=True):
@@ -388,12 +485,21 @@ def test_dragging_a_pane_onto_a_tab_moves_it_there():
mover = by_tab(s)[tabs[0]][1]
drag_pane_to(s, mover, strip_hits(s)["tab:%d" % tabs[1]])
- check("the pane is in the tab it was dropped on",
- mover in by_tab(s)[tabs[1]], str(by_tab(s)))
- check("and it says where it went", "moved to tab" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-120:]))
- check("nothing was created or lost",
- sum(len(v) for v in by_tab(s).values()) == 3, str(by_tab(s)))
+ check(
+ "the pane is in the tab it was dropped on",
+ mover in by_tab(s)[tabs[1]],
+ str(by_tab(s)),
+ )
+ check(
+ "and it says where it went",
+ "moved to tab" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-120:]),
+ )
+ check(
+ "nothing was created or lost",
+ sum(len(v) for v in by_tab(s).values()) == 3,
+ str(by_tab(s)),
+ )
def test_dragging_one_onto_the_plus_gives_it_a_tab():
@@ -408,10 +514,12 @@ def test_dragging_one_onto_the_plus_gives_it_a_tab():
drag_pane_to(s, mover, strip_hits(s)["newtab"])
made = [t["id"] for t in s.tabs() if t["id"] not in tabs]
check("a tab was made for it", len(made) == 1, str(s.tabs()))
- check("with the pane in it", by_tab(s).get(made[0]) == [mover],
- str(by_tab(s)))
- check("and it says so", "tab of its own" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-120:]))
+ check("with the pane in it", by_tab(s).get(made[0]) == [mover], str(by_tab(s)))
+ check(
+ "and it says so",
+ "tab of its own" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-120:]),
+ )
def test_the_strip_shows_where_it_would_land():
@@ -434,13 +542,21 @@ def test_the_strip_shows_where_it_would_land():
on = mid.style_at(other["x"] + 1, other["y"])
mine = mid.style_at(own["x"] + 1, own["y"])
plus = mid.style_at(hits["newtab"]["x"] + 1, hits["newtab"]["y"])
- check("the tab under the pointer is filled",
- on and on.get("bg") and on["bg"] != (idle or {}).get("bg"),
- "%s -> %s" % (idle, on))
- check("the `+` says it is a candidate too",
- plus and plus.get("fg") == on.get("bg"), str(plus))
- check("and the tab it already lives in does not offer itself",
- mine and mine.get("fg") != on.get("bg"), str(mine))
+ check(
+ "the tab under the pointer is filled",
+ on and on.get("bg") and on["bg"] != (idle or {}).get("bg"),
+ "%s -> %s" % (idle, on),
+ )
+ check(
+ "the `+` says it is a candidate too",
+ plus and plus.get("fg") == on.get("bg"),
+ str(plus),
+ )
+ check(
+ "and the tab it already lives in does not offer itself",
+ mine and mine.get("fg") != on.get("bg"),
+ str(mine),
+ )
# Leaving the strip takes the offer back: dragging away is not a drop.
pane = [p for p in s.panes() if p["id"] == mover][0]
@@ -449,11 +565,18 @@ def test_the_strip_shows_where_it_would_land():
after = s.snapshot().style_at(other["x"] + 1, other["y"])
s.send(r"\e[<0;%d;%dm" % (pane["x"] + 4, pane["y"] + 4))
s.settle(40)
- check("off the strip, no tab is filled",
- not after or not after.get("bg") or after.get("bg") == (idle or {}).get("bg"),
- str(after))
- check("and the pane stayed where it was", mover in by_tab(s)[tabs[0]],
- str(by_tab(s)))
+ check(
+ "off the strip, no tab is filled",
+ not after
+ or not after.get("bg")
+ or after.get("bg") == (idle or {}).get("bg"),
+ str(after),
+ )
+ check(
+ "and the pane stayed where it was",
+ mover in by_tab(s)[tabs[0]],
+ str(by_tab(s)),
+ )
def test_dropping_a_pane_on_its_own_tab_says_so():
@@ -468,8 +591,11 @@ def test_dropping_a_pane_on_its_own_tab_says_so():
drag_pane_to(s, mover, strip_hits(s)["tab:%d" % tabs[0]])
check("nothing moved", by_tab(s) == before, "%s -> %s" % (before, by_tab(s)))
- check("and it says why", "already in that tab" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-120:]))
+ check(
+ "and it says why",
+ "already in that tab" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-120:]),
+ )
def test_dragging_a_pane_onto_a_pane_still_swaps_them():
@@ -491,9 +617,11 @@ def test_dragging_a_pane_onto_a_pane_still_swaps_them():
s.send(r"\e[<0;%d;%dm" % (right["x"] + 5, right["y"] + 3))
s.settle(60)
now = sorted(s.panes(), key=lambda q: q["x"])
- check("the two panes swapped places",
- now[0]["id"] == right["id"] and now[1]["id"] == left["id"],
- str([(p["id"], p["x"]) for p in now]))
+ check(
+ "the two panes swapped places",
+ now[0]["id"] == right["id"] and now[1]["id"] == left["id"],
+ str([(p["id"], p["x"]) for p in now]),
+ )
if __name__ == "__main__":
diff --git a/tests/test_osc5577.py b/tests/test_osc5577.py
index dbf638b..64631c4 100644
--- a/tests/test_osc5577.py
+++ b/tests/test_osc5577.py
@@ -5,11 +5,13 @@ Byte-compatible with the sl0ppi fork, so these are also the acceptance tests
for the existing pi extensions. The hostile cases matter as much as the happy
ones: a pane's own output is untrusted input.
"""
+
import sys
import time
from harness import Session, check, report
+
# A pane that prints sequences and then reads, so click reports are visible.
# Every % is doubled: these go through printf, which would otherwise eat %3B
# as a (broken) format specifier — which cost one confusing test failure.
@@ -25,8 +27,7 @@ def shell():
eats it, so `printf "<ESC>]5577;..."` never reached printf. A raw-mode read
loop passes bytes through untouched.
"""
- return ["/bin/sh", "-c",
- 'stty raw -echo; while IFS= read -r l; do eval "$l"; done']
+ return ["/bin/sh", "-c", 'stty raw -echo; while IFS= read -r l; do eval "$l"; done']
def emit_cmd(payload):
@@ -48,30 +49,47 @@ def osc(payload):
def test_status_and_buttons():
- with Session(emit(osc("1;status;building 3/7"),
- osc("1;buttons;approve:Approve;cancel:Cancel")),
- cols=62, rows=8) as s:
+ with Session(
+ emit(
+ osc("1;status;building 3/7"), osc("1;buttons;approve:Approve;cancel:Cancel")
+ ),
+ cols=62,
+ rows=8,
+ ) as s:
s.settle()
snap, p = s.snapshot(), s.pane()
bottom = snap.line(p["y"] + p["h"] - 1)
- check("status text is drawn in the frame", "building 3/7" in bottom,
- repr(bottom))
- check("buttons are drawn", "[Approve]" in bottom and "[Cancel]" in bottom,
- repr(bottom))
- check("buttons are anchored right of the status",
- bottom.index("building") < bottom.index("[Approve]"), repr(bottom))
- check("the frame is intact around it",
- bottom.startswith(" ╰") and bottom.rstrip().endswith("╯"),
- repr(bottom))
+ check(
+ "status text is drawn in the frame", "building 3/7" in bottom, repr(bottom)
+ )
+ check(
+ "buttons are drawn",
+ "[Approve]" in bottom and "[Cancel]" in bottom,
+ repr(bottom),
+ )
+ check(
+ "buttons are anchored right of the status",
+ bottom.index("building") < bottom.index("[Approve]"),
+ repr(bottom),
+ )
+ check(
+ "the frame is intact around it",
+ bottom.startswith(" ╰") and bottom.rstrip().endswith("╯"),
+ repr(bottom),
+ )
# nothing of the sequence itself may reach the screen
- check("the escape sequence is not rendered", "5577" not in snap.screen(),
- repr(snap.screen()[:120]))
+ check(
+ "the escape sequence is not rendered",
+ "5577" not in snap.screen(),
+ repr(snap.screen()[:120]),
+ )
def test_click_reports():
- with Session(emit(osc("1;buttons;approve:Approve;cancel:Cancel")),
- cols=62, rows=8) as s:
+ with Session(
+ emit(osc("1;buttons;approve:Approve;cancel:Cancel")), cols=62, rows=8
+ ) as s:
s.settle()
snap, p = s.snapshot(), s.pane()
pos = snap.find("[Approve]")
@@ -80,17 +98,25 @@ def test_click_reports():
return
x, y = pos
action = snap.hit_at(x + 1, y)
- check("the click target is where the glyph is",
- action == f"btn:{p['id']}:approve", str(action))
- check("outside the button is not the button",
- snap.hit_at(x - 1, y) != f"btn:{p['id']}:approve",
- str(snap.hit_at(x - 1, y)))
+ check(
+ "the click target is where the glyph is",
+ action == f"btn:{p['id']}:approve",
+ str(action),
+ )
+ check(
+ "outside the button is not the button",
+ snap.hit_at(x - 1, y) != f"btn:{p['id']}:approve",
+ str(snap.hit_at(x - 1, y)),
+ )
s.click(x + 1, y)
s.settle()
out = s.snapshot().pane_text(p)
- check("clicking sends a click report to the pane",
- "]5577;1;click;approve" in out, repr(out[:120]))
+ check(
+ "clicking sends a click report to the pane",
+ "]5577;1;click;approve" in out,
+ repr(out[:120]),
+ )
cancel = s.snapshot().find("[Cancel]")
s.click(cancel[0] + 1, cancel[1])
@@ -112,83 +138,124 @@ def test_replace_and_clear():
s.raw(emit_cmd("1;status;second"))
s.settle()
- check("status replaces, it does not append",
- "second" in bottom() and "first" not in bottom(), repr(bottom()))
- check("buttons are untouched by a status update", "[A]" in bottom(),
- repr(bottom()))
+ check(
+ "status replaces, it does not append",
+ "second" in bottom() and "first" not in bottom(),
+ repr(bottom()),
+ )
+ check(
+ "buttons are untouched by a status update",
+ "[A]" in bottom(),
+ repr(bottom()),
+ )
s.raw(emit_cmd("1;clear"))
s.settle()
- check("clear removes both",
- "second" not in bottom() and "[A]" not in bottom(), repr(bottom()))
+ check(
+ "clear removes both",
+ "second" not in bottom() and "[A]" not in bottom(),
+ repr(bottom()),
+ )
def test_escaping():
- with Session(emit(osc("1;status;a;b;c"),
- osc("1;buttons;go:one%3Btwo;colon:a%3Ab;pct:100%25")),
- cols=70, rows=8) as s:
+ with Session(
+ emit(
+ osc("1;status;a;b;c"), osc("1;buttons;go:one%3Btwo;colon:a%3Ab;pct:100%25")
+ ),
+ cols=70,
+ rows=8,
+ ) as s:
s.settle()
p = s.pane()
bottom = s.snapshot().line(p["y"] + p["h"] - 1)
- check("status keeps its semicolons (everything after status; is text)",
- "a;b;c" in bottom, repr(bottom))
- check("a %3B in a label becomes a semicolon", "[one;two]" in bottom,
- repr(bottom))
+ check(
+ "status keeps its semicolons (everything after status; is text)",
+ "a;b;c" in bottom,
+ repr(bottom),
+ )
+ check(
+ "a %3B in a label becomes a semicolon", "[one;two]" in bottom, repr(bottom)
+ )
check("a %3A in a label becomes a colon", "[a:b]" in bottom, repr(bottom))
check("a %25 in a label becomes a percent", "[100%]" in bottom, repr(bottom))
def test_hostile_input():
"""A pane's output is untrusted. None of this may take effect."""
- with Session(emit(osc("2;status;from the future"),
- osc("1;nonsense;whatever"),
- osc("1;buttons;bad id:Nope;:NoId;ok:"),
- osc("1;buttons;" + "x" * 40 + ":TooLong"),
- osc("1;status;legit")),
- cols=70, rows=8) as s:
+ with Session(
+ emit(
+ osc("2;status;from the future"),
+ osc("1;nonsense;whatever"),
+ osc("1;buttons;bad id:Nope;:NoId;ok:"),
+ osc("1;buttons;" + "x" * 40 + ":TooLong"),
+ osc("1;status;legit"),
+ ),
+ cols=70,
+ rows=8,
+ ) as s:
s.settle()
p = s.pane()
snap = s.snapshot()
bottom = snap.line(p["y"] + p["h"] - 1)
- check("a future version is ignored entirely",
- "from the future" not in bottom, repr(bottom))
+ check(
+ "a future version is ignored entirely",
+ "from the future" not in bottom,
+ repr(bottom),
+ )
check("an unknown verb is ignored", "whatever" not in bottom, repr(bottom))
check("an invalid button id is dropped", "[Nope]" not in bottom, repr(bottom))
check("an empty id is dropped", "[NoId]" not in bottom, repr(bottom))
- check("an empty label is dropped", snap.hit_at(p["x"] + p["w"] - 3,
- p["y"] + p["h"] - 1) is None
- or "ok" not in str(snap.hits), str(snap.hits))
+ check(
+ "an empty label is dropped",
+ snap.hit_at(p["x"] + p["w"] - 3, p["y"] + p["h"] - 1) is None
+ or "ok" not in str(snap.hits),
+ str(snap.hits),
+ )
check("an over-long id is dropped", "[TooLong]" not in bottom, repr(bottom))
- check("valid sequences after invalid ones still work",
- "legit" in bottom, repr(bottom))
+ check(
+ "valid sequences after invalid ones still work",
+ "legit" in bottom,
+ repr(bottom),
+ )
# in-band purpose: allowed when free, refused when declared
with Session(emit(osc("1;purpose;agent:main")), cols=50, rows=8) as s:
s.settle()
p = s.pane()
- check("a pane may label itself when nothing is declared",
- p["purpose"] == "agent:main" and not p["purpose_declared"], str(p))
+ check(
+ "a pane may label itself when nothing is declared",
+ p["purpose"] == "agent:main" and not p["purpose_declared"],
+ str(p),
+ )
with Session(shell(), cols=50, rows=8) as s:
# declare first, the way a layout or the sl0ppi CLI would
pid = s.pane()["id"]
- s.api("set-purpose", target="pane", id=pid, purpose="project:real",
- declared=True)
+ s.api(
+ "set-purpose", target="pane", id=pid, purpose="project:real", declared=True
+ )
s.settle()
# `\e`, not `\033`: the driver's unescaper reads `\0` as NUL, so a `\033`
# written here reached the pane as NUL + "33" and printf emitted no ESC
# at all -- which made this check pass without ever testing anything.
s.raw(emit_cmd("1;purpose;evil:relabelled"))
s.settle()
- check("a pane cannot relabel a declared purpose",
- s.pane()["purpose"] == "project:real", str(s.pane()))
+ check(
+ "a pane cannot relabel a declared purpose",
+ s.pane()["purpose"] == "project:real",
+ str(s.pane()),
+ )
# ...and the refusal is the lock, not the delivery: the same sequence
# lands once the declared purpose is cleared.
s.api("set-purpose", target="pane", id=pid, purpose="", declared=True)
s.raw(emit_cmd("1;purpose;in-band:again"))
s.settle()
- check("and the sequence itself was arriving all along",
- s.pane()["purpose"] == "in-band:again", str(s.pane()))
+ check(
+ "and the sequence itself was arriving all along",
+ s.pane()["purpose"] == "in-band:again",
+ str(s.pane()),
+ )
def test_split_across_reads():
@@ -205,38 +272,51 @@ def test_split_across_reads():
s.raw(r'printf "\x07"' + "\\n")
s.settle()
bottom = s.snapshot().line(p["y"] + p["h"] - 1)
- check("a sequence split across reads is still understood",
- "in pieces" in bottom, repr(bottom))
+ check(
+ "a sequence split across reads is still understood",
+ "in pieces" in bottom,
+ repr(bottom),
+ )
def test_pi_extension_compat():
"""The exact byte patterns sl0ppi's pi extensions emit (answer-picker).
- OSC = "\\x1b]5577;1;" ST = "\\x1b\\\\"
- write(`${OSC}buttons;${spec.join(";")}${ST}`)
- write(`${OSC}buttons${ST}`) // drop buttons, keep the status
- write(`${OSC}status;alt+N · ready${ST}`)
- CLICK_RE = /\\x1b\\]5577;1;click;([A-Za-z0-9_-]{1,32})(?:\\x1b\\\\|\\x07)/g
- escLabel: % -> %25, ; -> %3B, : -> %3A
+ OSC = "\\x1b]5577;1;" ST = "\\x1b\\\\"
+ write(`${OSC}buttons;${spec.join(";")}${ST}`)
+ write(`${OSC}buttons${ST}`) // drop buttons, keep the status
+ write(`${OSC}status;alt+N · ready${ST}`)
+ CLICK_RE = /\\x1b\\]5577;1;click;([A-Za-z0-9_-]{1,32})(?:\\x1b\\\\|\\x07)/g
+ escLabel: % -> %25, ; -> %3B, : -> %3A
"""
- with Session(emit(osc("1;buttons;ans-continue:continue;ans-yes:yes"),
- osc("1;status;alt+N · ready")),
- cols=70, rows=8) as s:
+ with Session(
+ emit(
+ osc("1;buttons;ans-continue:continue;ans-yes:yes"),
+ osc("1;status;alt+N · ready"),
+ ),
+ cols=70,
+ rows=8,
+ ) as s:
s.settle()
p, snap = s.pane(), s.snapshot()
bottom = snap.line(p["y"] + p["h"] - 1)
- check("answer-picker's buttons render",
- "[continue]" in bottom and "[yes]" in bottom, repr(bottom))
- check("answer-picker's status renders", "alt+N · ready" in bottom,
- repr(bottom))
+ check(
+ "answer-picker's buttons render",
+ "[continue]" in bottom and "[yes]" in bottom,
+ repr(bottom),
+ )
+ check("answer-picker's status renders", "alt+N · ready" in bottom, repr(bottom))
x, y = snap.find("[continue]")
s.click(x + 1, y)
s.settle()
out = s.snapshot().pane_text(p)
# cat -v shows ESC as ^[ , so this is the extension's CLICK_RE on screen
- check("the click report matches the extension's CLICK_RE",
- "^[]5577;1;click;ans-continue^[\\" in out, repr(out[:140]))
+ check(
+ "the click report matches the extension's CLICK_RE",
+ "^[]5577;1;click;ans-continue^[\\" in out,
+ repr(out[:140]),
+ )
with Session(shell(), cols=70, rows=8) as s:
s.settle()
@@ -245,21 +325,30 @@ def test_pi_extension_compat():
s.raw(emit_cmd("1;status;alt+N · ready"))
s.raw(emit_cmd("1;buttons;ans-yes:yes"))
s.settle()
- check("status and buttons together", "[yes]" in bottom() and "ready" in bottom(),
- repr(bottom()))
+ check(
+ "status and buttons together",
+ "[yes]" in bottom() and "ready" in bottom(),
+ repr(bottom()),
+ )
s.raw(emit_cmd("1;buttons")) # hideButtons(): verb with no payload
s.settle()
- check("`buttons` with no payload drops only the buttons",
- "[yes]" not in bottom() and "ready" in bottom(), repr(bottom()))
+ check(
+ "`buttons` with no payload drops only the buttons",
+ "[yes]" not in bottom() and "ready" in bottom(),
+ repr(bottom()),
+ )
def test_hello_handshake():
with Session(emit(osc("1;hello;")), cols=60, rows=8) as s:
s.settle()
out = s.snapshot().pane_text(s.pane())
- check("hello is answered with an implementation and a version",
- "5577;1;hello-reply;sl0ppty;1" in out, repr(out[:120]))
+ check(
+ "hello is answered with an implementation and a version",
+ "5577;1;hello-reply;sl0ppty;1" in out,
+ repr(out[:120]),
+ )
def test_a_reply_is_not_a_request():
@@ -275,10 +364,16 @@ def test_a_reply_is_not_a_request():
s.settle()
time.sleep(0.5) # real time: a loop needs none of our help to run
out = s.snapshot().pane_text(s.pane())
- check("a reply the pane echoes back is not answered again",
- out.count("hello-reply") == 1, repr(out[:200]))
- check("and the pane is still usable afterwards",
- len(out) < 400, "%d chars of screen" % len(out))
+ check(
+ "a reply the pane echoes back is not answered again",
+ out.count("hello-reply") == 1,
+ repr(out[:200]),
+ )
+ check(
+ "and the pane is still usable afterwards",
+ len(out) < 400,
+ "%d chars of screen" % len(out),
+ )
if __name__ == "__main__":
diff --git a/tests/test_palette.py b/tests/test_palette.py
index 2b81710..9741227 100644
--- a/tests/test_palette.py
+++ b/tests/test_palette.py
@@ -10,6 +10,7 @@ It shares its box, its keys and its mouse handling with the pane finder (they
are one picker with a subject), so the navigation and editing checks live in
test_finder.py and are not repeated here.
"""
+
import pathlib
import re
import sys
@@ -19,7 +20,7 @@ from harness import Session, check, report
SH = ["/bin/sh", "-c", 'printf "\\033]2;shell\\007"; stty raw -echo; cat']
-OPEN = r"\x01p" # the default bind: prefix, then p
+OPEN = r"\x01p" # the default bind: prefix, then p
def cfg(text):
@@ -30,12 +31,15 @@ def cfg(text):
def is_open(s):
- return any(h["action"].startswith("run:") or h["action"] == "closepalette"
- for h in s.snapshot().hits)
+ return any(
+ h["action"].startswith("run:") or h["action"] == "closepalette"
+ for h in s.snapshot().hits
+ )
def counter(s):
import re
+
for line in s.snapshot().screen().split("\n"):
m = re.search(r"(\d+) of (\d+)", line)
if m:
@@ -45,9 +49,13 @@ def counter(s):
def rows(s):
snap = s.snapshot()
- return [snap.text[h["y"]] for h in
- sorted((h for h in snap.hits if h["action"].startswith("run:")),
- key=lambda h: h["y"])]
+ return [
+ snap.text[h["y"]]
+ for h in sorted(
+ (h for h in snap.hits if h["action"].startswith("run:")),
+ key=lambda h: h["y"],
+ )
+ ]
def test_it_opens_and_lists_commands():
@@ -57,15 +65,26 @@ def test_it_opens_and_lists_commands():
s.settle(100)
check("the palette opens", is_open(s), repr(s.snapshot().screen()[:200]))
screen = s.snapshot().screen()
- check("it is titled as commands, not as the finder",
- "commands" in screen, repr(screen[:300]))
- check("it lists actions by their phrase",
- "split into columns" in screen, repr(screen))
- check("with the chord that would have run them",
- any("split into columns" in r and "\\" in r for r in rows(s)),
- str(rows(s)))
- check("and there are a lot of them", (counter(s) or (0, 0))[1] > 20,
- str(counter(s)))
+ check(
+ "it is titled as commands, not as the finder",
+ "commands" in screen,
+ repr(screen[:300]),
+ )
+ check(
+ "it lists actions by their phrase",
+ "split into columns" in screen,
+ repr(screen),
+ )
+ check(
+ "with the chord that would have run them",
+ any("split into columns" in r and "\\" in r for r in rows(s)),
+ str(rows(s)),
+ )
+ check(
+ "and there are a lot of them",
+ (counter(s) or (0, 0))[1] > 20,
+ str(counter(s)),
+ )
def test_it_lists_actions_that_have_no_key():
@@ -79,8 +98,11 @@ def test_it_lists_actions_that_have_no_key():
listed = [r for r in rows(s) if "up a line" in r]
check("an unbound action is listed", listed != [], str(rows(s)))
if listed:
- check("with no chord against it, rather than a made-up one",
- "C-a" not in listed[0], repr(listed[0]))
+ check(
+ "with no chord against it, rather than a made-up one",
+ "C-a" not in listed[0],
+ repr(listed[0]),
+ )
def test_typing_filters_it():
@@ -94,10 +116,13 @@ def test_typing_filters_it():
s.settle(80)
check("the phrase finds it", counter(s) == (1, 1), str(counter(s)))
- s.send(r"\x15split-cols") # C-u, then the name a config file uses
+ s.send(r"\x15split-cols") # C-u, then the name a config file uses
s.settle(80)
- check("so does the name you would write in a config",
- counter(s) == (1, 1), str(counter(s)))
+ check(
+ "so does the name you would write in a config",
+ counter(s) == (1, 1),
+ str(counter(s)),
+ )
s.send(r"\x15scroll")
s.settle(80)
@@ -106,9 +131,11 @@ def test_typing_filters_it():
s.send(r"\x15zzzz")
s.settle(80)
- check("a query matching nothing says so",
- "no command matches" in s.snapshot().screen(),
- repr(s.snapshot().screen()[:400]))
+ check(
+ "a query matching nothing says so",
+ "no command matches" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[:400]),
+ )
def test_enter_runs_the_command():
@@ -175,9 +202,11 @@ def test_it_does_not_offer_typing_the_prefix():
s.send(OPEN)
s.send("prefix")
s.settle(100)
- check("the prefix is not on the menu",
- "no command matches" in s.snapshot().screen(),
- repr(s.snapshot().screen()[:400]))
+ check(
+ "the prefix is not on the menu",
+ "no command matches" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[:400]),
+ )
def test_the_bind_is_configurable():
@@ -189,14 +218,16 @@ def test_the_bind_is_configurable():
""")
with Session(SH, cols=80, rows=22, config=conf) as s:
s.settle()
- s.send(r"\x01 ") # prefix, then space
+ s.send(r"\x01 ") # prefix, then space
s.settle(100)
check("the palette opens on the key it was bound to", is_open(s))
- s.send("run a command") # its own row is well down a list of thirty
+ s.send("run a command") # its own row is well down a list of thirty
s.settle(80)
- check("and says so on its own row",
- any("run a command" in r and "space" in r for r in rows(s)),
- str([r for r in rows(s) if "run a command" in r]))
+ check(
+ "and says so on its own row",
+ any("run a command" in r and "space" in r for r in rows(s)),
+ str([r for r in rows(s) if "run a command" in r]),
+ )
def test_the_finder_is_still_its_own_thing():
@@ -205,9 +236,11 @@ def test_the_finder_is_still_its_own_thing():
s.settle()
s.send(r"\x01f")
s.settle(100)
- check("the finder lists panes, not commands",
- not is_open(s) and "split into columns" not in s.snapshot().screen(),
- repr(s.snapshot().screen()[:300]))
+ check(
+ "the finder lists panes, not commands",
+ not is_open(s) and "split into columns" not in s.snapshot().screen(),
+ repr(s.snapshot().screen()[:300]),
+ )
s.send(r"\e")
s.settle(80)
s.send(OPEN)
@@ -222,12 +255,15 @@ def test_every_label_fits_the_column():
came to exist."""
src = pathlib.Path(__file__).resolve().parent.parent / "src" / "config.c"
table = src.read_text()
- table = table[table.index("ACTION_HELP[]"):]
- table = table[:table.index("\n};")]
+ table = table[table.index("ACTION_HELP[]") :]
+ table = table[: table.index("\n};")]
rows = re.findall(r'\{ACT_[A-Z_0-9]+, "([a-z]+)", "([^"]+)"\}', table)
long = [(g, l) for g, l in rows if len(l) > 26]
- check("every action label fits the palette's column", not long,
- str([(l, len(l)) for _, l in long]))
+ check(
+ "every action label fits the palette's column",
+ not long,
+ str([(l, len(l)) for _, l in long]),
+ )
check("and the table was actually read", len(rows) > 20, str(len(rows)))
diff --git a/tests/test_pane_title.py b/tests/test_pane_title.py
index d261e36..da8e863 100644
--- a/tests/test_pane_title.py
+++ b/tests/test_pane_title.py
@@ -11,6 +11,7 @@ still focuses; it just no longer suggests or performs a split. Everything the
rest of the top row does must keep working, which is most of what is checked
here.
"""
+
import sys
import time
@@ -84,19 +85,27 @@ def test_the_title_text_is_its_own_region():
snap = s.snapshot()
first, last, mid = title_span(snap, p)
- check("the title text hit-tests as the pane's name",
- snap.hit_at(mid, p["y"]) == f"panetitle:{p['id']}",
- str(snap.hit_at(mid, p["y"])))
- check("both its ends do too",
- snap.hit_at(first, p["y"]) == f"panetitle:{p['id']}"
- and snap.hit_at(last, p["y"]) == f"panetitle:{p['id']}",
- f"{snap.hit_at(first, p['y'])} .. {snap.hit_at(last, p['y'])}")
- check("the cell just outside it is still the top edge",
- snap.hit_at(first - 1, p["y"]) == f"title:{p['id']}",
- str(snap.hit_at(first - 1, p["y"])))
- check("and so is the rest of the top row",
- snap.hit_at(off_title(p), p["y"]) == f"title:{p['id']}",
- str(snap.hit_at(off_title(p), p["y"])))
+ check(
+ "the title text hit-tests as the pane's name",
+ snap.hit_at(mid, p["y"]) == f"panetitle:{p['id']}",
+ str(snap.hit_at(mid, p["y"])),
+ )
+ check(
+ "both its ends do too",
+ snap.hit_at(first, p["y"]) == f"panetitle:{p['id']}"
+ and snap.hit_at(last, p["y"]) == f"panetitle:{p['id']}",
+ f"{snap.hit_at(first, p['y'])} .. {snap.hit_at(last, p['y'])}",
+ )
+ check(
+ "the cell just outside it is still the top edge",
+ snap.hit_at(first - 1, p["y"]) == f"title:{p['id']}",
+ str(snap.hit_at(first - 1, p["y"])),
+ )
+ check(
+ "and so is the rest of the top row",
+ snap.hit_at(off_title(p), p["y"]) == f"title:{p['id']}",
+ str(snap.hit_at(off_title(p), p["y"])),
+ )
def test_resting_on_the_title_arms_no_guide():
@@ -107,17 +116,21 @@ def test_resting_on_the_title_arms_no_guide():
rest(s, mid, p["y"])
s.settle(60)
- check("resting on the title suggests no split",
- HEAVY not in s.snapshot().line(p["y"]),
- repr(s.snapshot().line(p["y"])))
+ check(
+ "resting on the title suggests no split",
+ HEAVY not in s.snapshot().line(p["y"]),
+ repr(s.snapshot().line(p["y"])),
+ )
# The control: the same dwell two cells away must still arm it, or the
# check above would pass simply because the guide is broken.
rest(s, off_title(p), p["y"])
s.settle(60)
- check("resting on the top edge still does",
- HEAVY in s.snapshot().line(p["y"]),
- repr(s.snapshot().line(p["y"])))
+ check(
+ "resting on the top edge still does",
+ HEAVY in s.snapshot().line(p["y"]),
+ repr(s.snapshot().line(p["y"])),
+ )
def test_clicking_the_title_does_not_split():
@@ -128,13 +141,19 @@ def test_clicking_the_title_does_not_split():
click(s, mid, p["y"])
s.settle(80)
- check("clicking the title splits nothing", len(s.panes()) == 1,
- str(len(s.panes())))
+ check(
+ "clicking the title splits nothing",
+ len(s.panes()) == 1,
+ str(len(s.panes())),
+ )
click(s, off_title(p), p["y"])
s.settle(80)
- check("clicking the top edge still splits up", len(s.panes()) == 2,
- str(len(s.panes())))
+ check(
+ "clicking the top edge still splits up",
+ len(s.panes()) == 2,
+ str(len(s.panes())),
+ )
def test_double_clicking_the_title_does_not_split():
@@ -147,8 +166,11 @@ def test_double_clicking_the_title_does_not_split():
click(s, mid, p["y"])
click(s, mid, p["y"])
s.settle(80)
- check("a double-click on the title leaves one pane",
- len(s.panes()) == 1, str(len(s.panes())))
+ check(
+ "a double-click on the title leaves one pane",
+ len(s.panes()) == 1,
+ str(len(s.panes())),
+ )
def test_the_title_still_drags_the_pane():
@@ -167,10 +189,11 @@ def test_the_title_still_drags_the_pane():
s.settle()
panes = {p["id"]: p for p in s.panes()}
- check("dragging by the title still moves the pane",
- len(panes) == 2
- and panes[left["id"]]["x"] > panes[right["id"]]["x"],
- str([(p["id"], p["x"]) for p in s.panes()]))
+ check(
+ "dragging by the title still moves the pane",
+ len(panes) == 2 and panes[left["id"]]["x"] > panes[right["id"]]["x"],
+ str([(p["id"], p["x"]) for p in s.panes()]),
+ )
def test_hovering_the_title_still_focuses():
@@ -185,9 +208,11 @@ def test_hovering_the_title_still_focuses():
hover(s, mid, other["y"])
s.settle(60)
- check("hovering the title still focuses its pane",
- s.focused()["id"] == other["id"],
- f"{s.focused()['id']} != {other['id']}")
+ check(
+ "hovering the title still focuses its pane",
+ s.focused()["id"] == other["id"],
+ f"{s.focused()['id']} != {other['id']}",
+ )
def test_double_click_opens_an_editor_seeded_with_the_name():
@@ -196,10 +221,12 @@ def test_double_click_opens_an_editor_seeded_with_the_name():
p = s.pane()
open_editor(s, p)
row = s.snapshot().line(p["y"])
- check("double-clicking the title opens an editor", CURSOR in row,
- repr(row))
- check("seeded with the current name, so a rename is an edit",
- "shell" + CURSOR in row, repr(row))
+ check("double-clicking the title opens an editor", CURSOR in row, repr(row))
+ check(
+ "seeded with the current name, so a rename is an edit",
+ "shell" + CURSOR in row,
+ repr(row),
+ )
def test_enter_keeps_the_new_name():
@@ -212,9 +239,11 @@ def test_enter_keeps_the_new_name():
s.send(r"\r")
s.settle(80)
check("Enter commits the new name", titles(s) == ["api"], str(titles(s)))
- check("and the editor is gone",
- CURSOR not in s.snapshot().line(p["y"]),
- repr(s.snapshot().line(p["y"])))
+ check(
+ "and the editor is gone",
+ CURSOR not in s.snapshot().line(p["y"]),
+ repr(s.snapshot().line(p["y"])),
+ )
def test_escape_abandons_it():
@@ -226,11 +255,14 @@ def test_escape_abandons_it():
s.send("discarded")
s.send(r"\e")
s.settle(80)
- check("Escape leaves the old name alone", titles(s) == ["shell"],
- str(titles(s)))
- check("and closes the editor",
- CURSOR not in s.snapshot().line(p["y"]),
- repr(s.snapshot().line(p["y"])))
+ check(
+ "Escape leaves the old name alone", titles(s) == ["shell"], str(titles(s))
+ )
+ check(
+ "and closes the editor",
+ CURSOR not in s.snapshot().line(p["y"]),
+ repr(s.snapshot().line(p["y"])),
+ )
def test_clicking_away_keeps_the_name():
@@ -242,8 +274,11 @@ def test_clicking_away_keeps_the_name():
s.send("elsewhere")
s.click(p["content_x"] + 2, p["content_y"] + 1)
s.settle(80)
- check("clicking away commits, the way leaving a field does",
- titles(s) == ["elsewhere"], str(titles(s)))
+ check(
+ "clicking away commits, the way leaving a field does",
+ titles(s) == ["elsewhere"],
+ str(titles(s)),
+ )
def test_a_typed_name_outranks_the_program():
@@ -258,8 +293,11 @@ def test_a_typed_name_outranks_the_program():
s.raw(r"\e]2;prog-later\x07")
s.settle(120)
- check("the program retitling itself does not undo a rename",
- titles(s) == ["mine"], str(titles(s)))
+ check(
+ "the program retitling itself does not undo a rename",
+ titles(s) == ["mine"],
+ str(titles(s)),
+ )
# Clearing the name hands the pane back — to what the program calls
# itself *now*, not what it said when the rename began.
@@ -267,8 +305,11 @@ def test_a_typed_name_outranks_the_program():
backspace(s, 8)
s.send(r"\r")
s.settle(80)
- check("an empty name returns the pane to its program's title",
- titles(s) == ["prog-later"], str(titles(s)))
+ check(
+ "an empty name returns the pane to its program's title",
+ titles(s) == ["prog-later"],
+ str(titles(s)),
+ )
def test_a_script_can_name_a_pane_too():
@@ -279,25 +320,35 @@ def test_a_script_can_name_a_pane_too():
with Session(SH, cols=60, rows=14) as s:
s.settle()
p = s.pane()
- check("the program's title to start with", titles(s) == ["shell"],
- str(titles(s)))
+ check(
+ "the program's title to start with", titles(s) == ["shell"], str(titles(s))
+ )
reply = s.api("set-name", target="pane", id=p["id"], name="agent: gbos")
s.settle(80)
check("the verb reports ok", reply.get("ok") is True, str(reply))
- check("and the name is what the frame says", titles(s) == ["agent: gbos"],
- str(titles(s)))
+ check(
+ "and the name is what the frame says",
+ titles(s) == ["agent: gbos"],
+ str(titles(s)),
+ )
# The whole point: the program keeps shouting and is ignored.
s.raw(r"\e]2;\xe2\xa0\xb9 Fix toast overlap in frame tests\x07")
s.settle(120)
- check("a program retitling itself cannot take the name back",
- titles(s) == ["agent: gbos"], str(titles(s)))
+ check(
+ "a program retitling itself cannot take the name back",
+ titles(s) == ["agent: gbos"],
+ str(titles(s)),
+ )
s.api("set-name", target="pane", id=p["id"], name="")
s.settle(120)
- check("clearing it hands the label back to the program",
- titles(s) == ["\u2839 Fix toast overlap in frame tests"], str(titles(s)))
+ check(
+ "clearing it hands the label back to the program",
+ titles(s) == ["\u2839 Fix toast overlap in frame tests"],
+ str(titles(s)),
+ )
def test_the_verb_and_the_gesture_are_one_store():
@@ -311,8 +362,11 @@ def test_the_verb_and_the_gesture_are_one_store():
dbl(s, col_of(s.snapshot(), p, "named"), p["y"])
s.settle(80)
row = s.snapshot().line(p["y"])
- check("the editor opens seeded with the scripted name",
- "named" in row and CURSOR in row, repr(row.strip()))
+ check(
+ "the editor opens seeded with the scripted name",
+ "named" in row and CURSOR in row,
+ repr(row.strip()),
+ )
s.send(r"\e")
s.settle(40)
@@ -321,7 +375,7 @@ def test_naming_addresses_the_focused_pane_by_default():
"""0 is the focused pane, as it is for every other verb that takes an id."""
with Session(SH, cols=80, rows=30) as s:
s.settle()
- s.key("-") # two panes, focus lands on the new one
+ s.key("-") # two panes, focus lands on the new one
s.settle(60)
panes = s.panes()
check("there are two panes to choose between", len(panes) == 2, str(panes))
@@ -331,10 +385,16 @@ def test_naming_addresses_the_focused_pane_by_default():
s.api("set-name", target="pane", name="the focused one")
s.settle(80)
by_id = {q["id"]: q["title"] for q in s.panes()}
- check("id 0 means the focused pane", by_id[focused["id"]] == "the focused one",
- str(by_id))
- check("and the other pane keeps the title it had",
- by_id[other["id"]] == other["title"], str(by_id))
+ check(
+ "id 0 means the focused pane",
+ by_id[focused["id"]] == "the focused one",
+ str(by_id),
+ )
+ check(
+ "and the other pane keeps the title it had",
+ by_id[other["id"]] == other["title"],
+ str(by_id),
+ )
def test_the_target_says_which_label_and_bad_ids_say_which_thing():
@@ -349,19 +409,33 @@ def test_the_target_says_which_label_and_bad_ids_say_which_thing():
s.api("set-name", id=tab["id"], name="work")
s.settle(60)
- check("no target still names the tab", s.tabs()[0]["name"] == "work",
- str(s.tabs()[0]))
- check("...and leaves the pane's title alone", titles(s) == ["shell"],
- str(titles(s)))
+ check(
+ "no target still names the tab",
+ s.tabs()[0]["name"] == "work",
+ str(s.tabs()[0]),
+ )
+ check(
+ "...and leaves the pane's title alone",
+ titles(s) == ["shell"],
+ str(titles(s)),
+ )
- check("a pane id that does not exist says so",
- s.api("set-name", target="pane", id=4242, name="x")
- .get("error") == "no such pane", str(s.api("set-name", target="pane", id=4242, name="x")))
- check("and a tab id that does not exist says the other thing",
- s.api("set-name", id=4242, name="x").get("error") == "no such tab",
- str(s.api("set-name", id=4242, name="x")))
- check("the pane kept its title through both refusals",
- titles(s) == ["shell"] and s.pane()["id"] == p["id"], str(titles(s)))
+ check(
+ "a pane id that does not exist says so",
+ s.api("set-name", target="pane", id=4242, name="x").get("error")
+ == "no such pane",
+ str(s.api("set-name", target="pane", id=4242, name="x")),
+ )
+ check(
+ "and a tab id that does not exist says the other thing",
+ s.api("set-name", id=4242, name="x").get("error") == "no such tab",
+ str(s.api("set-name", id=4242, name="x")),
+ )
+ check(
+ "the pane kept its title through both refusals",
+ titles(s) == ["shell"] and s.pane()["id"] == p["id"],
+ str(titles(s)),
+ )
def test_slow_clicks_are_not_a_double_click():
@@ -377,8 +451,7 @@ def test_slow_clicks_are_not_a_double_click():
s.settle(80)
row = s.snapshot().line(p["y"])
check("two slow clicks open no editor", CURSOR not in row, repr(row))
- check("and still split nothing", len(s.panes()) == 1,
- str(len(s.panes())))
+ check("and still split nothing", len(s.panes()) == 1, str(len(s.panes())))
def test_typing_a_name_does_not_reach_the_pane():
@@ -391,8 +464,11 @@ def test_typing_a_name_does_not_reach_the_pane():
s.send(r"\r")
s.settle(120)
body = s.snapshot().pane_text(s.pane())
- check("the editor owns the keyboard: nothing leaked to the shell",
- "zzz" not in body, repr(body))
+ check(
+ "the editor owns the keyboard: nothing leaked to the shell",
+ "zzz" not in body,
+ repr(body),
+ )
def test_closing_the_pane_does_not_wedge_the_keyboard():
@@ -410,8 +486,11 @@ def test_closing_the_pane_does_not_wedge_the_keyboard():
s.send("after")
s.settle(120)
body = s.snapshot().pane_text(s.pane())
- check("and the keyboard went back to the surviving pane",
- "after" in body, repr(body))
+ check(
+ "and the keyboard went back to the surviving pane",
+ "after" in body,
+ repr(body),
+ )
if __name__ == "__main__":
diff --git a/tests/test_portable_layouts.py b/tests/test_portable_layouts.py
index ddcb04a..202f9e1 100644
--- a/tests/test_portable_layouts.py
+++ b/tests/test_portable_layouts.py
@@ -8,6 +8,7 @@ to a directory (it wrote the whole session, absolute); and a hand-edited layout
had to be checkable, because `cmd=` where `command=` was meant is a shell and no
complaint.
"""
+
import json
import os
import pathlib
@@ -44,6 +45,7 @@ def cwds(s, **kw):
the pane *is* (/proc/PID/cwd), so this asserts that the pty really did chdir
there rather than that a string was stored somewhere."""
import re
+
return re.findall(r'cwd="([^"]*)"', s.api("dump-layout", **kw)["kdl"])
@@ -51,29 +53,41 @@ def test_a_relative_cwd_means_the_layout_files_own_directory():
"""The rule `include` already follows for configs, applied to the other half of
the same syntax: relative to the file that wrote it, never to whatever
directory the session was started from."""
- d, path = project("api", """
+ d, path = project(
+ "api",
+ """
layout {
tab name="api" {
pane
pane cwd="src"
}
}
-""")
+""",
+ )
with Session(SH, layout=path) as s:
got = sorted(cwds(s))
- check("a pane with no cwd starts in the layout file's directory",
- got[0] == d, str(got) + " want " + d)
- check("and a relative one resolves against it",
- got[1] == os.path.join(d, "src"), str(got))
+ check(
+ "a pane with no cwd starts in the layout file's directory",
+ got[0] == d,
+ str(got) + " want " + d,
+ )
+ check(
+ "and a relative one resolves against it",
+ got[1] == os.path.join(d, "src"),
+ str(got),
+ )
def test_an_absolute_cwd_still_wins():
"""Every layout that already said `/home/you/dev/api` keeps meaning it."""
- d, path = project("api", """
+ d, path = project(
+ "api",
+ """
layout {
tab name="api" { pane cwd="/tmp" }
}
-""")
+""",
+ )
with Session(SH, layout=path) as s:
check("an absolute cwd is left alone", cwds(s) == ["/tmp"], str(cwds(s)))
@@ -84,16 +98,22 @@ def test_a_layout_sent_as_text_has_no_directory_to_be_relative_to():
rather than silently re-rooting somewhere new."""
with Session(SH) as s:
started_in = cwds(s)[0]
- s.api("apply-layout", kdl='layout { tab name="t" { pane cwd="." } }',
- replace=True)
- check("a relative cwd in text is not re-rooted against a file",
- cwds(s) == [started_in], str(cwds(s)) + " want " + started_in)
+ s.api(
+ "apply-layout", kdl='layout { tab name="t" { pane cwd="." } }', replace=True
+ )
+ check(
+ "a relative cwd in text is not re-rooted against a file",
+ cwds(s) == [started_in],
+ str(cwds(s)) + " want " + started_in,
+ )
def test_a_dump_can_be_asked_for_one_tab_relative_to_a_directory():
"""Which is the whole of what a project's layout file is: this tab, these
commands, these tags, no absolute paths anywhere in it."""
- d, path = project("api", """
+ d, path = project(
+ "api",
+ """
layout {
tab name="api" {
pane purpose="agent:main"
@@ -104,18 +124,17 @@ layout {
}
tab name="notes" { pane }
}
-""")
+""",
+ )
with Session(SH, layout=path) as s:
tabs = s.tabs()
api = [t for t in tabs if t["name"] == "api"][0]
whole = s.api("dump-layout")["kdl"]
- check("a whole-session dump has both tabs", whole.count(" tab ") == 2,
- whole)
+ check("a whole-session dump has both tabs", whole.count(" tab ") == 2, whole)
one = s.api("dump-layout", tab=api["id"], relative_to=d)
kdl = one["kdl"]
- check("asked for one tab, it writes one tab", kdl.count(" tab ") == 1,
- kdl)
+ check("asked for one tab, it writes one tab", kdl.count(" tab ") == 1, kdl)
check("and not the other one", '"notes"' not in kdl, kdl)
check("the project's own directory is `.`", 'cwd="."' in kdl, kdl)
check("a directory under it is relative", 'cwd="src"' in kdl, kdl)
@@ -128,61 +147,80 @@ layout {
check("a whole dump still is", "active=true" in whole, whole)
bad = s.api("dump-layout", tab=9999)
- check("an unknown tab is refused rather than answered with nothing",
- bad.get("ok") is False, str(bad))
+ check(
+ "an unknown tab is refused rather than answered with nothing",
+ bad.get("ok") is False,
+ str(bad),
+ )
def test_a_dump_restores_the_focus_of_every_tab():
"""It used to ask the *current* tab which pane was focused, whichever tab it
was writing -- so a session of three tabs came back with two of them focused
wherever first_leaf happened to land."""
- d, path = project("api", """
+ d, path = project(
+ "api",
+ """
layout {
tab name="one" { pane; pane focus=true }
tab name="two" active=true { pane focus=true; pane }
}
-""")
+""",
+ )
with Session(SH, layout=path) as s:
kdl = s.api("dump-layout")["kdl"]
- check("every tab says which pane it was in", kdl.count("focus=true") == 2,
- kdl)
+ check("every tab says which pane it was in", kdl.count("focus=true") == 2, kdl)
def test_suspend_is_a_policy_because_a_project_is_not_a_session():
"""A dump of a session is honest about what is running. A project's layout must
not be: the pane running this morning's dev server would start one on every
open, which is the thing `suspended` exists to prevent."""
- d, path = project("api", """
+ d, path = project(
+ "api",
+ """
layout {
tab name="api" {
pane
pane command="sleep 60"
}
}
-""")
+""",
+ )
with Session(SH, layout=path) as s:
asis = s.api("dump-layout")
- check("as-is writes what is actually suspended", asis["suspended"] == 0,
- str(asis))
+ check(
+ "as-is writes what is actually suspended", asis["suspended"] == 0, str(asis)
+ )
cmds = s.api("dump-layout", suspend="commands")
- check("`commands` suspends the pane that was given one",
- cmds["suspended"] == 1, str(cmds))
- check("and leaves the shell alone",
- cmds["kdl"].count("suspended=true") == 1, cmds["kdl"])
+ check(
+ "`commands` suspends the pane that was given one",
+ cmds["suspended"] == 1,
+ str(cmds),
+ )
+ check(
+ "and leaves the shell alone",
+ cmds["kdl"].count("suspended=true") == 1,
+ cmds["kdl"],
+ )
every = s.api("dump-layout", suspend="all")
check("`all` means all", every["suspended"] == 2, str(every))
none = s.api("dump-layout", suspend="none")
check("`none` means none", none["suspended"] == 0, str(none))
- check("a word nobody knows is refused rather than guessed",
- s.api("dump-layout", suspend="mostly").get("ok") is False,
- str(s.api("dump-layout", suspend="mostly")))
+ check(
+ "a word nobody knows is refused rather than guessed",
+ s.api("dump-layout", suspend="mostly").get("ok") is False,
+ str(s.api("dump-layout", suspend="mostly")),
+ )
def test_a_layout_that_round_trips_through_a_dump_is_the_same_layout():
"""The property that makes saving a project's layout worth doing at all."""
- d, path = project("api", """
+ d, path = project(
+ "api",
+ """
layout {
tab name="api" {
pane purpose="agent:main"
@@ -192,7 +230,8 @@ layout {
}
}
}
-""")
+""",
+ )
with Session(SH, layout=path) as s:
first = s.api("dump-layout", tab=s.tabs()[0]["id"], relative_to=d)["kdl"]
again = os.path.join(d, "again.layout.kdl")
@@ -200,15 +239,20 @@ layout {
f.write(first)
with Session(SH, layout=again) as s2:
second = s2.api("dump-layout", tab=s2.tabs()[0]["id"], relative_to=d)["kdl"]
- check("the shape, the tags and the directories all survive a round trip",
- first == second, first + "\n--- became ---\n" + second)
+ check(
+ "the shape, the tags and the directories all survive a round trip",
+ first == second,
+ first + "\n--- became ---\n" + second,
+ )
def test_a_layout_is_checked_as_a_layout():
"""`--check` used to answer a layout with the name of the flag that reads one,
which is true and no help: the file somebody asked about is the file they want
checked. One flag, and the document decides which schema it is held to."""
- d, path = project("api", """
+ d, path = project(
+ "api",
+ """
layout {
tab name="api" cwd="." {
pane cmd="pi"
@@ -219,7 +263,8 @@ layout {
}
}
}
-""")
+""",
+ )
code, said = lint(path)
check("it exits non-zero", code == 1, said)
for want in [
@@ -231,16 +276,22 @@ layout {
"a pane holds panes, not `wobble`",
]:
check("it names: " + want, want in said, said)
- check("every problem carries a file and a line",
- said.count("sl0ppty.layout.kdl:") >= 6, said)
+ check(
+ "every problem carries a file and a line",
+ said.count("sl0ppty.layout.kdl:") >= 6,
+ said,
+ )
def test_a_clean_layout_says_so():
- d, path = project("api", """
+ d, path = project(
+ "api",
+ """
layout {
tab name="api" { pane purpose="agent:main"; pane command="htop" }
}
-""")
+""",
+ )
code, said = lint(path)
check("a layout with nothing wrong exits zero", code == 0, said)
check("and says which document it read", "a layout" in said, said)
@@ -261,20 +312,27 @@ def test_a_config_handed_to_check_is_still_a_config():
with open(conf, "w") as f:
f.write("gap 2\nwobble 3\n")
code, said = lint(conf)
- check("a config is held to the config schema",
- "unknown setting: wobble" in said, said)
+ check(
+ "a config is held to the config schema", "unknown setting: wobble" in said, said
+ )
check("and not the layout one", "unknown pane property" not in said, said)
def test_a_syntax_error_in_a_layout_is_reported_as_the_layout_it_is():
"""A file that will not parse has no top-level names to be recognised by, and
then the extension is all there is -- which is the job D2 gave it."""
- d, path = project("api", 'layout {\n tab { pane\n')
+ d, path = project("api", "layout {\n tab { pane\n")
code, said = lint(path)
- check("it says which file and which line the syntax broke on",
- "sl0ppty.layout.kdl:" in said and "unclosed" in said, said)
- check("and does not talk about config defaults",
- "defaults would stand" not in said, said)
+ check(
+ "it says which file and which line the syntax broke on",
+ "sl0ppty.layout.kdl:" in said and "unclosed" in said,
+ said,
+ )
+ check(
+ "and does not talk about config defaults",
+ "defaults would stand" not in said,
+ said,
+ )
def test_the_layout_property_lists_cannot_go_stale():
@@ -286,13 +344,21 @@ def test_the_layout_property_lists_cannot_go_stale():
section = src[start:end]
import re
+
read = set(re.findall(r'kdl_prop(?:_int|_bool)?\((?:node|t), "([a-z_]+)"', section))
- listed = set(re.findall(r'"([a-z_]+)"', section[section.index("PANE_PROPS"):
- section.index("static bool in_list")]))
+ listed = set(
+ re.findall(
+ r'"([a-z_]+)"',
+ section[section.index("PANE_PROPS") : section.index("static bool in_list")],
+ )
+ )
check("the lists were actually found", len(listed) >= 8, str(listed))
missing = read - listed
- check("every property the loader reads is in the checker's lists",
- not missing, "missing: " + str(sorted(missing)))
+ check(
+ "every property the loader reads is in the checker's lists",
+ not missing,
+ "missing: " + str(sorted(missing)),
+ )
def test_a_dump_records_what_a_pane_is_actually_running():
@@ -304,11 +370,17 @@ def test_a_dump_records_what_a_pane_is_actually_running():
s.raw("sleep 300\\r")
s.settle(120)
d = s.api("dump-layout")
- check("the command it is running is in the file",
- 'command="sleep 300"' in d["kdl"], d["kdl"])
+ check(
+ "the command it is running is in the file",
+ 'command="sleep 300"' in d["kdl"],
+ d["kdl"],
+ )
cmds = s.api("dump-layout", suspend="commands")
- check("and `commands` writes it asleep, so a restore lays it out",
- 'command="sleep 300" suspended=true' in cmds["kdl"], cmds["kdl"])
+ check(
+ "and `commands` writes it asleep, so a restore lays it out",
+ 'command="sleep 300" suspended=true' in cmds["kdl"],
+ cmds["kdl"],
+ )
check("counted with the rest", cmds["suspended"] == 1, str(cmds))
@@ -318,11 +390,16 @@ def test_a_shell_at_a_prompt_is_not_a_command():
with Session(["/bin/sh"]) as s:
s.settle(120)
d = s.api("dump-layout")
- check("an idle pane is written with no command",
- "command=" not in d["kdl"], d["kdl"])
- check("and `commands` finds nothing to suspend",
- s.api("dump-layout", suspend="commands")["suspended"] == 0,
- d["kdl"])
+ check(
+ "an idle pane is written with no command",
+ "command=" not in d["kdl"],
+ d["kdl"],
+ )
+ check(
+ "and `commands` finds nothing to suspend",
+ s.api("dump-layout", suspend="commands")["suspended"] == 0,
+ d["kdl"],
+ )
def test_a_background_job_is_not_what_the_pane_is_running():
@@ -331,8 +408,11 @@ def test_a_background_job_is_not_what_the_pane_is_running():
with Session(["/bin/sh"]) as s:
s.raw("sleep 300 &\\r")
s.settle(120)
- check("the pane is still a shell", "command=" not in
- s.api("dump-layout")["kdl"], s.api("dump-layout")["kdl"])
+ check(
+ "the pane is still a shell",
+ "command=" not in s.api("dump-layout")["kdl"],
+ s.api("dump-layout")["kdl"],
+ )
def test_an_argument_with_spaces_survives_the_round_trip():
@@ -343,50 +423,74 @@ def test_an_argument_with_spaces_survives_the_round_trip():
s.raw("python3 -c 'import time; time.sleep(300)' \\r")
s.settle(150)
kdl = s.api("dump-layout")["kdl"]
- check("the argument is quoted as one word",
- "'import time; time.sleep(300)'" in kdl, kdl)
+ check(
+ "the argument is quoted as one word",
+ "'import time; time.sleep(300)'" in kdl,
+ kdl,
+ )
# Re-running it must rebuild the same argv, which only holds if the
# quoting is right: python exits non-zero on a broken -c.
s.api("apply-layout", kdl=kdl, replace=True)
s.settle(150)
again = s.api("dump-layout")["kdl"]
- check("and it comes back running the same thing",
- "'import time; time.sleep(300)'" in again, again)
+ check(
+ "and it comes back running the same thing",
+ "'import time; time.sleep(300)'" in again,
+ again,
+ )
pane = s.panes()[0]
- check("alive, so the command line was not mangled", pane["alive"] is True,
- str(pane))
+ check(
+ "alive, so the command line was not mangled",
+ pane["alive"] is True,
+ str(pane),
+ )
def test_a_layout_declared_command_outranks_what_is_running():
"""It survives the program exiting (D14), and re-saving a project must not
degrade `npm run dev` into whatever the process tree looks like this minute."""
- d, path = project("api", """
+ d, path = project(
+ "api",
+ """
layout {
tab name="api" { pane command="sh -c 'exec sleep 300'" }
}
-""")
+""",
+ )
with Session(SH, layout=path) as s:
s.settle(150)
kdl = s.api("dump-layout")["kdl"]
- check("the file keeps what the layout said",
- """command="sh -c 'exec sleep 300'\"""" in kdl, kdl)
- check("not what the kernel would have reported",
- 'command="sleep 300"' not in kdl, kdl)
+ check(
+ "the file keeps what the layout said",
+ """command="sh -c 'exec sleep 300'\"""" in kdl,
+ kdl,
+ )
+ check(
+ "not what the kernel would have reported",
+ 'command="sleep 300"' not in kdl,
+ kdl,
+ )
def test_a_dead_pane_still_says_what_it_ran():
"""D14: a pane that was given a command outlives it, and so does the record."""
- d, path = project("api", """
+ d, path = project(
+ "api",
+ """
layout {
tab name="api" { pane command="echo done-and-gone" }
}
-""")
+""",
+ )
with Session(SH, layout=path) as s:
s.settle(200)
kdl = s.api("dump-layout")["kdl"]
- check("the command is still in the file",
- 'command="echo done-and-gone"' in kdl, kdl)
+ check(
+ "the command is still in the file",
+ 'command="echo done-and-gone"' in kdl,
+ kdl,
+ )
if __name__ == "__main__":
diff --git a/tests/test_prefix.py b/tests/test_prefix.py
index ab69a3a..eff6911 100644
--- a/tests/test_prefix.py
+++ b/tests/test_prefix.py
@@ -9,6 +9,7 @@ teaches you to distrust the thing that is telling you which mode you are in.
So this is mostly one assertion made in several places: rebind it, and the
badge, the cheatsheet, the swallowing and the pass-through all follow.
"""
+
import sys
import tempfile
@@ -31,41 +32,42 @@ def strip(s):
def test_the_badge_names_the_key_you_bound():
- for prefix, chord, shown in (("ctrl+a", r"\x01", "C-a"),
- ("ctrl+b", r"\x02", "C-b"),
- ("ctrl+space", r"\x00", "C-space"),
- ("alt+x", r"\ex", "M-x")):
+ for prefix, chord, shown in (
+ ("ctrl+a", r"\x01", "C-a"),
+ ("ctrl+b", r"\x02", "C-b"),
+ ("ctrl+space", r"\x00", "C-space"),
+ ("alt+x", r"\ex", "M-x"),
+ ):
with Session(SH, cols=60, rows=8, config=cfg(prefix)) as s:
s.settle()
- check(f"{prefix}: no badge until it is pressed",
- shown not in strip(s), strip(s))
+ check(
+ f"{prefix}: no badge until it is pressed",
+ shown not in strip(s),
+ strip(s),
+ )
s.send(chord)
s.settle()
- check(f"{prefix}: the badge says {shown}", shown in strip(s),
- strip(s))
+ check(f"{prefix}: the badge says {shown}", shown in strip(s), strip(s))
def test_a_rebound_prefix_actually_works():
with Session(SH, cols=80, rows=14, config=cfg("ctrl+b")) as s:
s.settle()
check("one pane to start", len(s.panes()) == 1)
- s.send(r"\x02\\") # C-b \ : split
+ s.send(r"\x02\\") # C-b \ : split
s.settle()
- check("the new prefix runs commands", len(s.panes()) == 2,
- str(s.panes()))
+ check("the new prefix runs commands", len(s.panes()) == 2, str(s.panes()))
def test_the_old_prefix_goes_to_the_program_instead():
"""The reason to move it: C-a is start-of-line to half the world."""
with Session(SH, cols=80, rows=14, config=cfg("ctrl+b")) as s:
s.settle()
- s.send(r"\x01") # C-a, no longer ours
+ s.send(r"\x01") # C-a, no longer ours
s.settle(40)
- check("C-a is not swallowed as a prefix", "C-a" not in strip(s),
- strip(s))
+ check("C-a is not swallowed as a prefix", "C-a" not in strip(s), strip(s))
snap = s.until(lambda sn: "^A" in sn.screen())
- check("it reached the program", "^A" in snap.screen(),
- snap.screen()[:200])
+ check("it reached the program", "^A" in snap.screen(), snap.screen()[:200])
def test_pressing_the_prefix_twice_sends_it_on():
@@ -75,8 +77,9 @@ def test_pressing_the_prefix_twice_sends_it_on():
s.settle(40)
check("the badge is gone again", "C-b" not in strip(s), strip(s))
snap = s.until(lambda sn: "^B" in sn.screen())
- check("and the pane got the keystroke", "^B" in snap.screen(),
- snap.screen()[:200])
+ check(
+ "and the pane got the keystroke", "^B" in snap.screen(), snap.screen()[:200]
+ )
def test_the_cheatsheet_agrees_with_the_badge():
@@ -85,8 +88,7 @@ def test_the_cheatsheet_agrees_with_the_badge():
s.send(r"\x02?")
s.settle()
screen = s.snapshot().screen()
- check("the cheatsheet names the same key", "C-b then:" in screen,
- screen)
+ check("the cheatsheet names the same key", "C-b then:" in screen, screen)
check("and not the old one", "C-a then:" not in screen, screen)
diff --git a/tests/test_purpose_editor.py b/tests/test_purpose_editor.py
index c29c98d..f269b86 100644
--- a/tests/test_purpose_editor.py
+++ b/tests/test_purpose_editor.py
@@ -6,6 +6,7 @@ none, and a layout dumped from a session someone built by splitting panes was a
shape with no tags in it. The editor is the one the pane title and the tab label
already share, with a third subject.
"""
+
import os
import sys
@@ -27,28 +28,38 @@ def emit_cmd(payload):
def shell():
"""A pane that runs lines we send it, with no line editor to eat an ESC --
the same fixture the OSC tests use, for the same reason."""
- return ["/bin/sh", "-c",
- 'stty raw -echo; while IFS= read -r l; do eval "$l"; done']
+ return ["/bin/sh", "-c", 'stty raw -echo; while IFS= read -r l; do eval "$l"; done']
def test_a_purpose_can_be_typed_where_it_will_live():
with Session(SH) as s:
s.key("P")
snap = s.snapshot()
- check("the editor announces which label it is editing",
- "purpose" in snap.screen(), snap.screen())
+ check(
+ "the editor announces which label it is editing",
+ "purpose" in snap.screen(),
+ snap.screen(),
+ )
s.send("service:web")
- check("what is typed shows in the title cell",
- "purpose service:web" in s.snapshot().screen(), s.snapshot().screen())
+ check(
+ "what is typed shows in the title cell",
+ "purpose service:web" in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
s.send("\\r")
check("enter keeps it", s.pane()["purpose"] == "service:web", str(s.pane()))
- check("and it counts as declared, because an operator set it",
- s.pane()["purpose_declared"] is True, str(s.pane()))
- check("the editor is gone",
- "purpose service:web" not in s.snapshot().screen(),
- s.snapshot().screen())
+ check(
+ "and it counts as declared, because an operator set it",
+ s.pane()["purpose_declared"] is True,
+ str(s.pane()),
+ )
+ check(
+ "the editor is gone",
+ "purpose service:web" not in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
def test_escape_abandons_it():
@@ -56,8 +67,9 @@ def test_escape_abandons_it():
s.key("P")
s.send("half-typed")
s.send("\\033")
- check("escape leaves the purpose alone", s.pane()["purpose"] == "",
- str(s.pane()))
+ check(
+ "escape leaves the purpose alone", s.pane()["purpose"] == "", str(s.pane())
+ )
def test_it_seeds_with_what_is_there():
@@ -66,11 +78,17 @@ def test_it_seeds_with_what_is_there():
with Session(SH) as s:
s.api("set-purpose", id=s.focused()["id"], purpose="agent:main")
s.key("P")
- check("the editor opens on the purpose it has",
- "purpose agent:main" in s.snapshot().screen(), s.snapshot().screen())
+ check(
+ "the editor opens on the purpose it has",
+ "purpose agent:main" in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
s.send(":2\\r")
- check("so an edit is an edit", s.pane()["purpose"] == "agent:main:2",
- str(s.pane()))
+ check(
+ "so an edit is an edit",
+ s.pane()["purpose"] == "agent:main:2",
+ str(s.pane()),
+ )
def test_a_typed_purpose_outranks_the_program_in_the_pane():
@@ -82,8 +100,11 @@ def test_a_typed_purpose_outranks_the_program_in_the_pane():
s.send("project:real\\r")
s.raw(emit_cmd("1;purpose;evil:relabelled"))
s.settle()
- check("a pane cannot relabel a purpose an operator declared",
- s.pane()["purpose"] == "project:real", str(s.pane()))
+ check(
+ "a pane cannot relabel a purpose an operator declared",
+ s.pane()["purpose"] == "project:real",
+ str(s.pane()),
+ )
def test_clearing_it_hands_the_label_back_to_the_program():
@@ -106,8 +127,11 @@ def test_clearing_it_hands_the_label_back_to_the_program():
s.raw(emit_cmd("1;purpose;mine-now"))
s.settle()
- check("so the pane can speak for itself again",
- s.pane()["purpose"] == "mine-now", str(s.pane()))
+ check(
+ "so the pane can speak for itself again",
+ s.pane()["purpose"] == "mine-now",
+ str(s.pane()),
+ )
def test_the_action_is_bindable_and_in_the_palette():
@@ -115,11 +139,17 @@ def test_the_action_is_bindable_and_in_the_palette():
s.key("p")
s.send("purpose")
snap = s.snapshot()
- check("the palette offers it without it having to be bound",
- "tag it with a purpose" in snap.screen(), snap.screen())
+ check(
+ "the palette offers it without it having to be bound",
+ "tag it with a purpose" in snap.screen(),
+ snap.screen(),
+ )
s.send("\\r")
- check("and choosing it opens the editor",
- "purpose" in s.snapshot().screen(), s.snapshot().screen())
+ check(
+ "and choosing it opens the editor",
+ "purpose" in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
def test_id_zero_is_the_focused_pane_here_too():
@@ -132,18 +162,18 @@ def test_id_zero_is_the_focused_pane_here_too():
r = s.api("set-purpose", id=0, purpose="service:web")
check("0 is accepted", r["ok"] is True, str(r))
focused = [p for p in s.panes() if p["focused"]][0]
- check("and it tagged the focused pane",
- focused["purpose"] == "service:web", str(focused))
+ check(
+ "and it tagged the focused pane",
+ focused["purpose"] == "service:web",
+ str(focused),
+ )
others = [p for p in s.panes() if not p["focused"]]
- check("and nothing else", all(p["purpose"] == "" for p in others),
- str(others))
+ check("and nothing else", all(p["purpose"] == "" for p in others), str(others))
t = s.api("set-purpose", target="tab", id=0, purpose="notes")
- check("a tab takes 0 as the one you are looking at", t["ok"] is True,
- str(t))
+ check("a tab takes 0 as the one you are looking at", t["ok"] is True, str(t))
here = [x for x in s.tabs() if x["active"]][0]
- check("and that is the tab it tagged", here["purpose"] == "notes",
- str(here))
+ check("and that is the tab it tagged", here["purpose"] == "notes", str(here))
def test_a_dumped_layout_carries_what_was_tagged_by_hand():
@@ -156,8 +186,11 @@ def test_a_dumped_layout_carries_what_was_tagged_by_hand():
s.key("P")
s.send("shell:scratch\\r")
kdl = s.api("dump-layout")["kdl"]
- check("both tags are in the file", 'purpose="agent:main"' in kdl and
- 'purpose="shell:scratch"' in kdl, kdl)
+ check(
+ "both tags are in the file",
+ 'purpose="agent:main"' in kdl and 'purpose="shell:scratch"' in kdl,
+ kdl,
+ )
if __name__ == "__main__":
diff --git a/tests/test_reload.py b/tests/test_reload.py
index ce40cdc..e4973a5 100644
--- a/tests/test_reload.py
+++ b/tests/test_reload.py
@@ -7,6 +7,7 @@ settings take effect without a restart. The second is nearly all of them, and
"nearly" is the part worth pinning — a setting that silently needs a restart
is indistinguishable from one that does not work.
"""
+
import os
import subprocess
import sys
@@ -48,12 +49,18 @@ def test_the_things_that_take_effect_immediately():
check("geometry", s.pane()["x"] == 6, str(s.pane()))
reload_with(s, path, "status_bar false\n")
- check("chrome on and off", "+" not in s.snapshot().line(1),
- repr(s.snapshot().line(1)))
+ check(
+ "chrome on and off",
+ "+" not in s.snapshot().line(1),
+ repr(s.snapshot().line(1)),
+ )
reload_with(s, path, 'close_mark "@"\n')
- check("the marks in a frame", "@" in s.snapshot().line(2),
- repr(s.snapshot().line(2)))
+ check(
+ "the marks in a frame",
+ "@" in s.snapshot().line(2),
+ repr(s.snapshot().line(2)),
+ )
reload_with(s, path, "dim_unfocused 0\n")
s.key("\\\\")
@@ -69,8 +76,7 @@ def test_the_things_that_take_effect_immediately():
before = len(s.tabs())
s.send(r"\x02c")
s.settle(30)
- check("the leader key itself", len(s.tabs()) == before + 1,
- str(s.tabs()))
+ check("the leader key itself", len(s.tabs()) == before + 1, str(s.tabs()))
os.unlink(path)
@@ -100,8 +106,12 @@ def test_a_config_written_after_the_session_started_is_noticed():
env = dict(os.environ, SL0PPTY_CONFIG=path)
name = "reloadtest-%d" % os.getpid()
- subprocess.run([BIN, "-s", name, "--", "/bin/sh", "-c", "read x"],
- env=env, capture_output=True, timeout=5)
+ subprocess.run(
+ [BIN, "-s", name, "--", "/bin/sh", "-c", "read x"],
+ env=env,
+ capture_output=True,
+ timeout=5,
+ )
# The session *creates* its config directory at startup, precisely so there is
# always somewhere to watch -- and writing the file before it has is a race
@@ -115,8 +125,11 @@ def test_a_config_written_after_the_session_started_is_noticed():
made = True
break
time.sleep(0.02)
- check("the session created the directory it needs to watch", made,
- os.path.dirname(path))
+ check(
+ "the session created the directory it needs to watch",
+ made,
+ os.path.dirname(path),
+ )
try:
deadline = time.time() + 3
got = None
@@ -124,17 +137,24 @@ def test_a_config_written_after_the_session_started_is_noticed():
with open(path, "w") as f:
f.write('theme { frame_focus "#00ff00" }\n')
time.sleep(0.4)
- out = subprocess.run([BIN, "-s", name, "cmd",
- '{"cmd":"snapshot","format":"json"}'],
- capture_output=True, text=True, env=env).stdout
+ out = subprocess.run(
+ [BIN, "-s", name, "cmd", '{"cmd":"snapshot","format":"json"}'],
+ capture_output=True,
+ text=True,
+ env=env,
+ ).stdout
if '"#00ff00"' in out:
got = True
break
- check("writing a config later is picked up without a restart", got,
- "the session never saw the new file")
+ check(
+ "writing a config later is picked up without a restart",
+ got,
+ "the session never saw the new file",
+ )
finally:
- subprocess.run([BIN, "-s", name, "cmd", '{"cmd":"quit"}'],
- capture_output=True, env=env)
+ subprocess.run(
+ [BIN, "-s", name, "cmd", '{"cmd":"quit"}'], capture_output=True, env=env
+ )
def test_an_included_file_is_watched_too():
@@ -154,13 +174,20 @@ def test_an_included_file_is_watched_too():
env = dict(os.environ, SL0PPTY_CONFIG=main)
name = "inctest-%d" % os.getpid()
- subprocess.run([BIN, "-s", name, "--", "/bin/sh", "-c", "read x"],
- env=env, capture_output=True, timeout=5)
+ subprocess.run(
+ [BIN, "-s", name, "--", "/bin/sh", "-c", "read x"],
+ env=env,
+ capture_output=True,
+ timeout=5,
+ )
def screen():
- return subprocess.run([BIN, "-s", name, "cmd",
- '{"cmd":"snapshot","format":"json"}'],
- capture_output=True, text=True, env=env).stdout
+ return subprocess.run(
+ [BIN, "-s", name, "cmd", '{"cmd":"snapshot","format":"json"}'],
+ capture_output=True,
+ text=True,
+ env=env,
+ ).stdout
try:
# Polled for the same reason as the directory above: `-s` returning means
@@ -172,8 +199,7 @@ def test_an_included_file_is_watched_too():
started = True
break
time.sleep(0.02)
- check("the included theme applied at startup", started,
- "green never appeared")
+ check("the included theme applied at startup", started, "green never appeared")
# The include line is untouched; only the file it points at changes.
deadline = time.time() + 3
@@ -185,11 +211,15 @@ def test_an_included_file_is_watched_too():
if '"#ff00ff"' in screen():
got = True
break
- check("saving the included file reloads the session", got,
- "the session never saw the theme change")
+ check(
+ "saving the included file reloads the session",
+ got,
+ "the session never saw the theme change",
+ )
finally:
- subprocess.run([BIN, "-s", name, "cmd", '{"cmd":"quit"}'],
- capture_output=True, env=env)
+ subprocess.run(
+ [BIN, "-s", name, "cmd", '{"cmd":"quit"}'], capture_output=True, env=env
+ )
for name, fn in sorted(list(globals().items())):
diff --git a/tests/test_responsive.py b/tests/test_responsive.py
index 363a5f7..d3dc90d 100644
--- a/tests/test_responsive.py
+++ b/tests/test_responsive.py
@@ -7,9 +7,10 @@ against a pane count that a stack lies about. Here the layout is recomputed
from the tree and the rect every frame, so the equivalent bug has nowhere to
live — and these tests are what says so.
"""
+
import os
-import tempfile
import sys
+import tempfile
from harness import Session, check, report
@@ -40,8 +41,11 @@ def invariants(s, label):
for j in range(i + 1, len(rs)):
bx, by, bw, bh = rs[j]
if not (ax + aw <= bx or bx + bw <= ax or ay + ah <= by or by + bh <= ay):
- check(f"{label}: visible panes do not overlap", False,
- f"{rs[i]} vs {rs[j]}")
+ check(
+ f"{label}: visible panes do not overlap",
+ False,
+ f"{rs[i]} vs {rs[j]}",
+ )
return panes
check(f"{label}: {len(rs)} visible, no overlap, all on screen", True)
return panes
@@ -50,26 +54,36 @@ def invariants(s, label):
def test_collapse():
with Session(SH, cols=120, rows=32) as s:
grid(s)
- check("nothing is hidden when there is room",
- not any(p["hidden"] for p in s.panes()), str(s.panes()))
+ check(
+ "nothing is hidden when there is room",
+ not any(p["hidden"] for p in s.panes()),
+ str(s.panes()),
+ )
invariants(s, "roomy")
s.resize(56, 16)
s.settle()
panes = s.panes()
- check("a squeezed layout collapses instead of shrinking to nothing",
- any(p["hidden"] for p in panes), str([p["hidden"] for p in panes]))
+ check(
+ "a squeezed layout collapses instead of shrinking to nothing",
+ any(p["hidden"] for p in panes),
+ str([p["hidden"] for p in panes]),
+ )
invariants(s, "squeezed")
snap = s.snapshot()
- check("the focused pane keeps a usable size",
- [p for p in panes if p["focused"]][0]["content_h"] >= 3,
- str([p for p in panes if p["focused"]]))
+ check(
+ "the focused pane keeps a usable size",
+ [p for p in panes if p["focused"]][0]["content_h"] >= 3,
+ str([p for p in panes if p["focused"]]),
+ )
hidden = [q for q in s.panes() if q["hidden"]]
tops = sum(1 for row in snap.text if row.strip().startswith("\u256d"))
- check("every collapsed pane is drawn as the top edge of a pane",
- tops == len(hidden) + 1, # the headers, plus the open pane
- f"{tops} tops for {len(hidden)} hidden: " + repr(snap.screen()))
+ check(
+ "every collapsed pane is drawn as the top edge of a pane",
+ tops == len(hidden) + 1, # the headers, plus the open pane
+ f"{tops} tops for {len(hidden)} hidden: " + repr(snap.screen()),
+ )
def test_collapse_expand_cycle():
@@ -84,18 +98,26 @@ def test_collapse_expand_cycle():
s.resize(120, 32)
s.settle()
after = [(p["x"], p["y"], p["w"], p["h"]) for p in s.panes()]
- check("wide again: nothing is hidden",
- not any(p["hidden"] for p in s.panes()), str(s.panes()))
- check("the layout is exactly what it was before the cycle",
- before == after, f"{before}\n != {after}")
+ check(
+ "wide again: nothing is hidden",
+ not any(p["hidden"] for p in s.panes()),
+ str(s.panes()),
+ )
+ check(
+ "the layout is exactly what it was before the cycle",
+ before == after,
+ f"{before}\n != {after}",
+ )
# the fork's actual symptom: a pane added after the cycle got stacked
s.key("-")
s.settle()
panes = invariants(s, "after the cycle")
- check("a pane added after a narrow/wide cycle is not stacked",
- len(panes) == 5 and not any(p["hidden"] for p in s.panes()),
- str([(p["id"], p["hidden"]) for p in s.panes()]))
+ check(
+ "a pane added after a narrow/wide cycle is not stacked",
+ len(panes) == 5 and not any(p["hidden"] for p in s.panes()),
+ str([(p["id"], p["hidden"]) for p in s.panes()]),
+ )
def test_focus_expands():
@@ -112,10 +134,15 @@ def test_focus_expands():
s.api("focus", id=target)
s.settle()
now = {p["id"]: p for p in s.panes()}
- check("focusing a collapsed pane expands it", not now[target]["hidden"],
- str(now[target]))
- check("and something else collapsed in its place",
- any(p["hidden"] for p in s.panes()))
+ check(
+ "focusing a collapsed pane expands it",
+ not now[target]["hidden"],
+ str(now[target]),
+ )
+ check(
+ "and something else collapsed in its place",
+ any(p["hidden"] for p in s.panes()),
+ )
invariants(s, "after expanding")
@@ -131,12 +158,15 @@ def test_click_a_header():
h = hidden[0]
snap = s.snapshot()
action = snap.hit_at(h["x"] + 1, h["y"])
- check("a collapsed header is clickable", action == f"focus:{h['id']}",
- str(action))
+ check(
+ "a collapsed header is clickable", action == f"focus:{h['id']}", str(action)
+ )
s.click(h["x"] + 1, h["y"])
s.settle()
- check("clicking a header expands that pane",
- not [p for p in s.panes() if p["id"] == h["id"]][0]["hidden"])
+ check(
+ "clicking a header expands that pane",
+ not [p for p in s.panes() if p["id"] == h["id"]][0]["hidden"],
+ )
def test_hidden_panes_keep_running():
@@ -154,19 +184,26 @@ def test_hidden_panes_keep_running():
s.resize(50, 14)
s.settle()
now = {p["id"]: p for p in s.panes()}
- check("the pane we wrote to is hidden", now[first["id"]]["hidden"],
- str(now[first["id"]]))
- check("a hidden pane is not resized, so its program does not reflow",
- (now[first["id"]]["content_w"], now[first["id"]]["content_h"])
- == size_before,
- f"{size_before} -> {(now[first['id']]['content_w'], now[first['id']]['content_h'])}")
+ check(
+ "the pane we wrote to is hidden",
+ now[first["id"]]["hidden"],
+ str(now[first["id"]]),
+ )
+ check(
+ "a hidden pane is not resized, so its program does not reflow",
+ (now[first["id"]]["content_w"], now[first["id"]]["content_h"])
+ == size_before,
+ f"{size_before} -> {(now[first['id']]['content_w'], now[first['id']]['content_h'])}",
+ )
s.resize(120, 32)
s.api("focus", id=first["id"])
s.settle()
- check("its content survived being collapsed",
- "written-before-collapse" in s.snapshot().screen(),
- repr(s.snapshot().screen()[:200]))
+ check(
+ "its content survived being collapsed",
+ "written-before-collapse" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[:200]),
+ )
def test_tiny_terminal():
@@ -182,11 +219,14 @@ def test_tiny_terminal():
# ---- collapsing flattens the hierarchy -------------------------------------
+
def nested_layout():
"""left column of three stacked panes, plus one on the right."""
f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
- f.write('layout {\n tab name="t" {\n pane split="rows" { pane\n'
- ' pane\n pane }\n pane\n }\n}\n')
+ f.write(
+ 'layout {\n tab name="t" {\n pane split="rows" { pane\n'
+ " pane\n pane }\n pane\n }\n}\n"
+ )
f.close()
return f.name
@@ -202,17 +242,28 @@ def test_collapsing_flattens_the_tree():
hidden = [p for p in panes if p["hidden"]]
shown = [p for p in panes if not p["hidden"]]
- check("everything but the focused pane becomes a header",
- len(hidden) == 3 and len(shown) == 1, str(panes))
- check("each header is exactly one row",
- all(p["h"] == 1 for p in hidden), str([p["h"] for p in hidden]))
+ check(
+ "everything but the focused pane becomes a header",
+ len(hidden) == 3 and len(shown) == 1,
+ str(panes),
+ )
+ check(
+ "each header is exactly one row",
+ all(p["h"] == 1 for p in hidden),
+ str([p["h"] for p in hidden]),
+ )
# The nesting is what there is no room to express, so it stops
# existing: no two panes sit side by side while collapsed.
- check("no two panes share a row",
- len({p["y"] for p in hidden}) == len(hidden),
- str([p["y"] for p in hidden]))
- check("the headers come first and the body below them",
- shown[0]["y"] > max(p["y"] for p in hidden), str(panes))
+ check(
+ "no two panes share a row",
+ len({p["y"] for p in hidden}) == len(hidden),
+ str([p["y"] for p in hidden]),
+ )
+ check(
+ "the headers come first and the body below them",
+ shown[0]["y"] > max(p["y"] for p in hidden),
+ str(panes),
+ )
os.unlink(lay)
@@ -229,19 +280,27 @@ def test_every_collapsed_pane_is_clickable():
hidden = [p for p in s.panes() if p["hidden"]]
actions = [snap.hit_at(p["x"] + 2, p["y"]) for p in hidden]
- check("every header carries its own pane's focus action",
- actions == [f"focus:{p['id']}" for p in hidden], str(actions))
- check("and they are all different", len(set(actions)) == len(actions),
- str(actions))
+ check(
+ "every header carries its own pane's focus action",
+ actions == [f"focus:{p['id']}" for p in hidden],
+ str(actions),
+ )
+ check(
+ "and they are all different",
+ len(set(actions)) == len(actions),
+ str(actions),
+ )
# Clicking the *last* header must reach that pane, not the first one.
target = hidden[-1]
s.click(target["x"] + 2, target["y"])
s.settle(20)
now = {p["id"]: p for p in s.panes()}
- check("clicking the last header expands that exact pane",
- not now[target["id"]]["hidden"] and now[target["id"]]["h"] > 1,
- str(s.panes()))
+ check(
+ "clicking the last header expands that exact pane",
+ not now[target["id"]]["hidden"] and now[target["id"]]["h"] > 1,
+ str(s.panes()),
+ )
os.unlink(lay)
@@ -255,8 +314,7 @@ def test_a_flattened_stack_offers_no_resize_handles():
s.settle(20)
snap = s.snapshot()
edges = [h for h in snap.hits if h["action"].startswith("edge:")]
- check("a collapsed stack registers no resize edges", edges == [],
- str(edges))
+ check("a collapsed stack registers no resize edges", edges == [], str(edges))
os.unlink(lay)
@@ -280,33 +338,41 @@ def test_a_tab_is_laid_out_or_it_is_a_list_never_both():
hidden = [p for p in panes if p["hidden"]]
check("exactly one pane is open", len(shown) == 1, str(panes))
- check("every other pane is a header, including the one that did fit",
- len(hidden) == len(panes) - 1, str(panes))
- check("the headers all span the full width",
- len({p["w"] for p in hidden}) == 1
- and hidden[0]["w"] == shown[0]["w"],
- str([(p["id"], p["w"]) for p in panes]))
+ check(
+ "every other pane is a header, including the one that did fit",
+ len(hidden) == len(panes) - 1,
+ str(panes),
+ )
+ check(
+ "the headers all span the full width",
+ len({p["w"] for p in hidden}) == 1 and hidden[0]["w"] == shown[0]["w"],
+ str([(p["id"], p["w"]) for p in panes]),
+ )
# The give-away for the old behaviour: a full-height pane beside a
# stack. Nothing may sit next to a header any more.
- check("no pane sits beside a header",
- all(p["x"] == hidden[0]["x"] for p in panes),
- str([(p["id"], p["x"]) for p in panes]))
+ check(
+ "no pane sits beside a header",
+ all(p["x"] == hidden[0]["x"] for p in panes),
+ str([(p["id"], p["x"]) for p in panes]),
+ )
os.unlink(conf.name)
os.unlink(lay)
# ---- hovering a row in the stack -------------------------------------------
-FRAME_FOCUS = "#ff5fd7" # the rule, while hovered
-TITLE_FOCUS = "#ffffff" # the title, while hovered
+FRAME_FOCUS = "#ff5fd7" # the rule, while hovered
+TITLE_FOCUS = "#ffffff" # the title, while hovered
FRAME_IDLE = "#45454a"
def stacked_session():
"""A tab small enough to be a list, with two headers and one body."""
lay = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
- lay.write('layout {\n tab name="t" {\n pane\n'
- ' pane split="rows" { pane\n pane }\n }\n}\n')
+ lay.write(
+ 'layout {\n tab name="t" {\n pane\n'
+ ' pane split="rows" { pane\n pane }\n }\n}\n'
+ )
lay.close()
conf = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
conf.write("min_pane cols=24 rows=17\n")
@@ -331,31 +397,43 @@ def test_hovering_a_row_lights_it_up():
with stacked_session() as s:
s.settle(20)
headers = [p for p in s.panes() if p["hidden"]]
- check("the tab is a list with rows to hover", len(headers) >= 2,
- str(s.panes()))
+ check("the tab is a list with rows to hover", len(headers) >= 2, str(s.panes()))
if len(headers) < 2:
return
snap = s.snapshot()
- check("nothing is lit before the pointer arrives",
- all(rule_fg(snap, p) == FRAME_IDLE for p in headers),
- str([rule_fg(snap, p) for p in headers]))
+ check(
+ "nothing is lit before the pointer arrives",
+ all(rule_fg(snap, p) == FRAME_IDLE for p in headers),
+ str([rule_fg(snap, p) for p in headers]),
+ )
hover(s, headers[0]["x"] + 4, headers[0]["y"])
snap = s.snapshot()
- check("the row under the pointer is lit",
- rule_fg(snap, headers[0]) == FRAME_FOCUS,
- str(rule_fg(snap, headers[0])))
- check("and only that row",
- all(rule_fg(snap, p) == FRAME_IDLE for p in headers[1:]), "")
- check("by its foreground: no bar is painted behind it",
- row_bg(snap, headers[0]) is None, str(row_bg(snap, headers[0])))
+ check(
+ "the row under the pointer is lit",
+ rule_fg(snap, headers[0]) == FRAME_FOCUS,
+ str(rule_fg(snap, headers[0])),
+ )
+ check(
+ "and only that row",
+ all(rule_fg(snap, p) == FRAME_IDLE for p in headers[1:]),
+ "",
+ )
+ check(
+ "by its foreground: no bar is painted behind it",
+ row_bg(snap, headers[0]) is None,
+ str(row_bg(snap, headers[0])),
+ )
hover(s, headers[1]["x"] + 4, headers[1]["y"])
snap = s.snapshot()
- check("the light follows the pointer down the list",
- rule_fg(snap, headers[1]) == FRAME_FOCUS
- and rule_fg(snap, headers[0]) == FRAME_IDLE, "")
+ check(
+ "the light follows the pointer down the list",
+ rule_fg(snap, headers[1]) == FRAME_FOCUS
+ and rule_fg(snap, headers[0]) == FRAME_IDLE,
+ "",
+ )
def test_leaving_the_stack_puts_the_light_out():
@@ -368,8 +446,11 @@ def test_leaving_the_stack_puts_the_light_out():
hover(s, headers[0]["x"] + 4, headers[0]["y"])
hover(s, body["content_x"] + 2, body["content_y"] + 2)
snap = s.snapshot()
- check("no row is lit once the pointer is off the list",
- all(rule_fg(snap, p) == FRAME_IDLE for p in headers), "")
+ check(
+ "no row is lit once the pointer is off the list",
+ all(rule_fg(snap, p) == FRAME_IDLE for p in headers),
+ "",
+ )
def test_hovering_a_row_does_not_open_it():
@@ -385,17 +466,26 @@ def test_hovering_a_row_does_not_open_it():
hover(s, h["x"] + 4, h["y"])
s.settle(20)
after = [p["id"] for p in s.panes() if p["focused"]]
- check("hovering every row changes nothing about which is open",
- before == after, f"{before} -> {after}")
- check("and they are all still headers",
- len([p for p in s.panes() if p["hidden"]]) == len(headers), "")
+ check(
+ "hovering every row changes nothing about which is open",
+ before == after,
+ f"{before} -> {after}",
+ )
+ check(
+ "and they are all still headers",
+ len([p for p in s.panes() if p["hidden"]]) == len(headers),
+ "",
+ )
# ...but clicking one still does open it.
s.click(headers[-1]["x"] + 4, headers[-1]["y"])
s.settle(20)
now = {p["id"]: p for p in s.panes()}
- check("clicking the row it lit is what opens that pane",
- not now[headers[-1]["id"]]["hidden"], str(s.panes()))
+ check(
+ "clicking the row it lit is what opens that pane",
+ not now[headers[-1]["id"]]["hidden"],
+ str(s.panes()),
+ )
def test_a_header_is_shaped_like_a_pane_top():
@@ -407,37 +497,49 @@ def test_a_header_is_shaped_like_a_pane_top():
if not headers:
return
for h in headers:
- row = snap.line(h["y"])[h["x"]:h["x"] + h["w"]]
- check(f"row at y={h['y']} opens and closes with a corner",
- row.startswith("\u256d") and row.endswith("\u256e"), repr(row))
- check(f"row at y={h['y']} keeps air around its title",
- " pane " in row or " p " in row, repr(row))
+ row = snap.line(h["y"])[h["x"] : h["x"] + h["w"]]
+ check(
+ f"row at y={h['y']} opens and closes with a corner",
+ row.startswith("\u256d") and row.endswith("\u256e"),
+ repr(row),
+ )
+ check(
+ f"row at y={h['y']} keeps air around its title",
+ " pane " in row or " p " in row,
+ repr(row),
+ )
# The title must not be welded to the rule on either side.
i = row.find("pane")
- check(f"row at y={h['y']} has a rule, a space, then the title",
- i > 2 and row[i - 1] == " " and row[i - 2] == "\u2500",
- repr(row))
+ check(
+ f"row at y={h['y']} has a rule, a space, then the title",
+ i > 2 and row[i - 1] == " " and row[i - 2] == "\u2500",
+ repr(row),
+ )
def test_the_title_inset_is_configurable():
lay = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
- lay.write('layout {\n tab name="t" {\n pane\n'
- ' pane split="rows" { pane\n pane }\n }\n}\n')
+ lay.write(
+ 'layout {\n tab name="t" {\n pane\n'
+ ' pane split="rows" { pane\n pane }\n }\n}\n'
+ )
lay.close()
seen = {}
for inset in (0, 6):
conf = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
conf.write(f"min_pane cols=24 rows=17\ntitle_inset {inset}\n")
conf.close()
- with Session(SH, cols=100, rows=20, config=conf.name,
- layout=lay.name) as s:
+ with Session(SH, cols=100, rows=20, config=conf.name, layout=lay.name) as s:
s.settle(20)
h = [p for p in s.panes() if p["hidden"]][0]
- row = s.snapshot().line(h["y"])[h["x"]:h["x"] + h["w"]]
+ row = s.snapshot().line(h["y"])[h["x"] : h["x"] + h["w"]]
seen[inset] = row.find("pane")
os.unlink(conf.name)
- check("a bigger inset pushes the title further from the corner",
- seen[6] == seen[0] + 6, str(seen))
+ check(
+ "a bigger inset pushes the title further from the corner",
+ seen[6] == seen[0] + 6,
+ str(seen),
+ )
os.unlink(lay.name)
@@ -452,40 +554,58 @@ def test_width_collapses_a_stacked_layout_too():
"""
with Session(SH, cols=100, rows=24) as s:
s.settle(20)
- s.api("split", dir="rows") # stacked: width is the perpendicular axis
+ s.api("split", dir="rows") # stacked: width is the perpendicular axis
s.settle(20)
- check("two stacked panes, both open, at a comfortable width",
- len([p for p in s.panes() if not p["hidden"]]) == 2, str(s.panes()))
+ check(
+ "two stacked panes, both open, at a comfortable width",
+ len([p for p in s.panes() if not p["hidden"]]) == 2,
+ str(s.panes()),
+ )
s.resize(60, 24)
s.settle(20)
- check("still open while each pane clears the floor",
- not any(p["hidden"] for p in s.panes()), str(s.panes()))
+ check(
+ "still open while each pane clears the floor",
+ not any(p["hidden"] for p in s.panes()),
+ str(s.panes()),
+ )
s.resize(26, 24)
s.settle(20)
- check("narrow enough, and a stacked layout collapses like any other",
- any(p["hidden"] for p in s.panes()), str(s.panes()))
+ check(
+ "narrow enough, and a stacked layout collapses like any other",
+ any(p["hidden"] for p in s.panes()),
+ str(s.panes()),
+ )
s.resize(100, 24)
s.settle(20)
- check("and comes back when there is room again",
- not any(p["hidden"] for p in s.panes()), str(s.panes()))
+ check(
+ "and comes back when there is room again",
+ not any(p["hidden"] for p in s.panes()),
+ str(s.panes()),
+ )
def test_height_collapses_a_side_by_side_layout_too():
"""The same hole, the other way round."""
with Session(SH, cols=100, rows=24) as s:
s.settle(20)
- s.api("split", dir="cols") # side by side: height is perpendicular
+ s.api("split", dir="cols") # side by side: height is perpendicular
s.settle(20)
- check("two panes side by side at a comfortable height",
- not any(p["hidden"] for p in s.panes()), str(s.panes()))
+ check(
+ "two panes side by side at a comfortable height",
+ not any(p["hidden"] for p in s.panes()),
+ str(s.panes()),
+ )
s.resize(100, 9)
s.settle(20)
- check("short enough, and a side-by-side layout collapses",
- any(p["hidden"] for p in s.panes()), str(s.panes()))
+ check(
+ "short enough, and a side-by-side layout collapses",
+ any(p["hidden"] for p in s.panes()),
+ str(s.panes()),
+ )
if __name__ == "__main__":
diff --git a/tests/test_rotate.py b/tests/test_rotate.py
index c6b4a7f..ba2d62f 100755
--- a/tests/test_rotate.py
+++ b/tests/test_rotate.py
@@ -8,6 +8,7 @@ the order stands. Get that backwards and you have a mirror image, which looks
almost right and is not a rotation — the tell is that four turns no longer come
back to where you started, which is what most of this file checks.
"""
+
import sys
import tempfile
@@ -18,40 +19,58 @@ SH = ["/bin/sh", "-c", "stty raw -echo; cat"]
def rects(s):
"""Every pane as (id, x, y, w, h), in reading order."""
- return [(p["id"], p["x"], p["y"], p["w"], p["h"])
- for p in sorted(s.panes(), key=lambda p: (p["y"], p["x"]))]
+ return [
+ (p["id"], p["x"], p["y"], p["w"], p["h"])
+ for p in sorted(s.panes(), key=lambda p: (p["y"], p["x"]))
+ ]
def turn(s, n=1):
for _ in range(n):
- s.key(r"\x20") # C-a space: the escape, so the driver's line keeps it
+ s.key(r"\x20") # C-a space: the escape, so the driver's line keeps it
s.settle()
def test_two_rows_become_two_columns_the_right_way_round():
with Session(SH, cols=80, rows=24) as s:
s.settle()
- s.key("-") # rows: the first pane on top, the new one below
+ s.key("-") # rows: the first pane on top, the new one below
s.settle()
top, bottom = rects(s)
- check("it starts as two rows", top[2] < bottom[2] and top[3] == bottom[3],
- str(rects(s)))
+ check(
+ "it starts as two rows",
+ top[2] < bottom[2] and top[3] == bottom[3],
+ str(rects(s)),
+ )
turn(s)
left, right = sorted(rects(s), key=lambda r: r[1])
- check("one turn makes two columns",
- left[2] == right[2] and left[1] < right[1], str(rects(s)))
- check("and the pane that was on top is now on the right",
- right[0] == top[0], f"top was {top[0]}, right is {right[0]}")
- check("the one that was underneath is on the left",
- left[0] == bottom[0], f"bottom was {bottom[0]}, left is {left[0]}")
+ check(
+ "one turn makes two columns",
+ left[2] == right[2] and left[1] < right[1],
+ str(rects(s)),
+ )
+ check(
+ "and the pane that was on top is now on the right",
+ right[0] == top[0],
+ f"top was {top[0]}, right is {right[0]}",
+ )
+ check(
+ "the one that was underneath is on the left",
+ left[0] == bottom[0],
+ f"bottom was {bottom[0]}, left is {left[0]}",
+ )
turn(s)
a, b = rects(s)
- check("two turns are two rows again", a[3] == b[3] and a[2] < b[2],
- str(rects(s)))
- check("...with the panes the other way up than they started",
- a[0] == bottom[0] and b[0] == top[0], str(rects(s)))
+ check(
+ "two turns are two rows again", a[3] == b[3] and a[2] < b[2], str(rects(s))
+ )
+ check(
+ "...with the panes the other way up than they started",
+ a[0] == bottom[0] and b[0] == top[0],
+ str(rects(s)),
+ )
def test_four_turns_are_the_identity():
@@ -59,7 +78,7 @@ def test_four_turns_are_the_identity():
tree and nothing else, so pressing it enough times undoes it exactly."""
with Session(SH, cols=100, rows=30) as s:
s.settle()
- s.key("\\\\") # a nest: one pane beside a stack of two
+ s.key("\\\\") # a nest: one pane beside a stack of two
s.key("-")
s.settle()
start = rects(s)
@@ -69,11 +88,13 @@ def test_four_turns_are_the_identity():
for i in range(1, 4):
turn(s)
seen.append(rects(s))
- check(f"turn {i} changes the arrangement", seen[-1] != start,
- str(seen[-1]))
+ check(f"turn {i} changes the arrangement", seen[-1] != start, str(seen[-1]))
turn(s)
- check("the fourth turn comes back exactly", rects(s) == start,
- f"{start} -> {rects(s)}")
+ check(
+ "the fourth turn comes back exactly",
+ rects(s) == start,
+ f"{start} -> {rects(s)}",
+ )
def test_a_share_of_one_axis_becomes_a_share_of_the_other():
@@ -82,21 +103,26 @@ def test_a_share_of_one_axis_becomes_a_share_of_the_other():
s.key("\\\\")
s.settle()
for _ in range(4):
- s.key("L") # push the boundary right: the left pane grows
+ s.key("L") # push the boundary right: the left pane grows
s.settle()
left, right = sorted(rects(s), key=lambda r: r[1])
- check("the columns are uneven", left[3] > right[3] + 8,
- f"{left[3]} vs {right[3]}")
+ check(
+ "the columns are uneven", left[3] > right[3] + 8, f"{left[3]} vs {right[3]}"
+ )
ratio = left[3] / right[3]
turn(s)
top, bottom = sorted(rects(s), key=lambda r: r[2])
- check("the wider pane is now the taller one",
- top[0] == left[0] and top[4] > bottom[4],
- f"{rects(s)} (left was {left[0]})")
- check("and it kept roughly the share it had",
- abs(top[4] / bottom[4] - ratio) < 0.35,
- f"{ratio:.2f} across -> {top[4] / bottom[4]:.2f} down")
+ check(
+ "the wider pane is now the taller one",
+ top[0] == left[0] and top[4] > bottom[4],
+ f"{rects(s)} (left was {left[0]})",
+ )
+ check(
+ "and it kept roughly the share it had",
+ abs(top[4] / bottom[4] - ratio) < 0.35,
+ f"{ratio:.2f} across -> {top[4] / bottom[4]:.2f} down",
+ )
def test_focus_stays_on_the_pane_you_were_in():
@@ -106,8 +132,11 @@ def test_focus_stays_on_the_pane_you_were_in():
s.settle()
was = s.focused()["id"]
turn(s, 3)
- check("the pane you were in is the pane you are in",
- s.focused()["id"] == was, f"{was} -> {s.focused()['id']}")
+ check(
+ "the pane you were in is the pane you are in",
+ s.focused()["id"] == was,
+ f"{was} -> {s.focused()['id']}",
+ )
def test_a_turn_that_would_not_fit_is_refused():
@@ -119,23 +148,31 @@ def test_a_turn_that_would_not_fit_is_refused():
s.key("-")
s.settle()
for _ in range(5):
- s.key("K") # squeeze the top pane: fine as a row, too thin as a column
+ s.key("K") # squeeze the top pane: fine as a row, too thin as a column
s.settle()
before = rects(s)
turn(s)
- check("the layout is exactly as it was", rects(s) == before,
- f"{before} -> {rects(s)}")
- check("and it says why",
- "no room to turn it" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-200:]))
+ check(
+ "the layout is exactly as it was",
+ rects(s) == before,
+ f"{before} -> {rects(s)}",
+ )
+ check(
+ "and it says why",
+ "no room to turn it" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-200:]),
+ )
- s.key("=") # even shares fit either way round
+ s.key("=") # even shares fit either way round
s.settle()
turn(s)
cols = sorted(rects(s), key=lambda r: r[1])
- check("evening it out first lets the same turn through",
- cols[0][2] == cols[1][2] and cols[0][1] < cols[1][1], str(rects(s)))
+ check(
+ "evening it out first lets the same turn through",
+ cols[0][2] == cols[1][2] and cols[0][1] < cols[1][1],
+ str(rects(s)),
+ )
def test_one_pane_has_nothing_to_turn():
@@ -143,10 +180,14 @@ def test_one_pane_has_nothing_to_turn():
s.settle()
before = rects(s)
turn(s)
- check("it says so", "nothing to turn" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-200:]))
- check("and the pane is untouched", rects(s) == before,
- f"{before} -> {rects(s)}")
+ check(
+ "it says so",
+ "nothing to turn" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-200:]),
+ )
+ check(
+ "and the pane is untouched", rects(s) == before, f"{before} -> {rects(s)}"
+ )
def test_it_is_offered_by_name():
@@ -154,21 +195,27 @@ def test_it_is_offered_by_name():
s.settle()
s.key("?")
s.settle()
- check("the cheatsheet lists it with its key",
- "space turn the layout a quarter" in s.snapshot().screen(),
- repr(s.snapshot().screen()[:400]))
+ check(
+ "the cheatsheet lists it with its key",
+ "space turn the layout a quarter" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[:400]),
+ )
s.send("q")
s.key("p")
s.send("rotate")
s.settle()
snap = s.snapshot()
- check("the palette finds it by its config name",
- "turn the layout a quarter" in snap.screen(),
- repr(snap.screen()[:400]))
- check("and the row runs it",
- any(h["action"].startswith("run:") for h in snap.hits),
- str([h["action"] for h in snap.hits][:8]))
+ check(
+ "the palette finds it by its config name",
+ "turn the layout a quarter" in snap.screen(),
+ repr(snap.screen()[:400]),
+ )
+ check(
+ "and the row runs it",
+ any(h["action"].startswith("run:") for h in snap.hits),
+ str([h["action"] for h in snap.hits][:8]),
+ )
def test_a_rotated_layout_survives_a_dump():
@@ -187,9 +234,11 @@ def test_a_rotated_layout_survives_a_dump():
lay.close()
with Session(SH, cols=90, rows=24, layout=lay.name) as s2:
s2.settle(60)
- check("and comes back in the same arrangement",
- [r[1:] for r in rects(s2)] == [r[1:] for r in turned],
- f"{turned} -> {rects(s2)}")
+ check(
+ "and comes back in the same arrangement",
+ [r[1:] for r in rects(s2)] == [r[1:] for r in turned],
+ f"{turned} -> {rects(s2)}",
+ )
if __name__ == "__main__":
diff --git a/tests/test_screen.py b/tests/test_screen.py
index d7bfa5b..c302785 100644
--- a/tests/test_screen.py
+++ b/tests/test_screen.py
@@ -6,6 +6,7 @@ takes milliseconds. Content is addressed through the layout (`content_x/y`)
rather than assumed to start at 0,0 — panes have chrome, and a test that
hardcodes the offset is a test that breaks every time the chrome changes.
"""
+
import sys
from harness import Session, check, report
@@ -24,50 +25,82 @@ def test_text_and_cursor():
s.settle()
p = s.pane()
snap = s.snapshot()
- check("text lands on the pane's first row",
- snap.pane_line(p, 0).startswith("hello"), repr(snap.pane_line(p, 0)))
- check("cursor follows the text",
- (snap.cursor["x"], snap.cursor["y"]) == (p["content_x"] + 5, p["content_y"]),
- str(snap.cursor))
+ check(
+ "text lands on the pane's first row",
+ snap.pane_line(p, 0).startswith("hello"),
+ repr(snap.pane_line(p, 0)),
+ )
+ check(
+ "cursor follows the text",
+ (snap.cursor["x"], snap.cursor["y"])
+ == (p["content_x"] + 5, p["content_y"]),
+ str(snap.cursor),
+ )
s.raw("\\r\\nsecond")
s.settle()
snap = s.snapshot()
- check("newline moves to the next row",
- snap.pane_line(p, 1).startswith("second"), repr(snap.pane_line(p, 1)))
- check("cursor is on that row",
- snap.cursor["y"] == p["content_y"] + 1, str(snap.cursor))
+ check(
+ "newline moves to the next row",
+ snap.pane_line(p, 1).startswith("second"),
+ repr(snap.pane_line(p, 1)),
+ )
+ check(
+ "cursor is on that row",
+ snap.cursor["y"] == p["content_y"] + 1,
+ str(snap.cursor),
+ )
def test_styles():
- with Session(SH(r'printf "n\033[1mB\033[31mR\033[0mn"; sleep 5'), cols=30, rows=6) as s:
+ with Session(
+ SH(r'printf "n\033[1mB\033[31mR\033[0mn"; sleep 5'), cols=30, rows=6
+ ) as s:
s.settle()
p, snap = s.pane(), s.snapshot()
cx, cy = p["content_x"], p["content_y"]
- check("plain cell has no style run", snap.style_at(cx, cy) is None,
- str(snap.style_at(cx, cy)))
+ check(
+ "plain cell has no style run",
+ snap.style_at(cx, cy) is None,
+ str(snap.style_at(cx, cy)),
+ )
b = snap.style_at(cx + 1, cy)
check("bold is recorded", b and b["attrs"] == ["bold"], str(b))
r = snap.style_at(cx + 2, cy)
- check("colour is resolved to rgb",
- r and r["fg"] is not None and "bold" in r["attrs"], str(r))
- check("reset ends the run", snap.style_at(cx + 3, cy) is None,
- str(snap.style_at(cx + 3, cy)))
+ check(
+ "colour is resolved to rgb",
+ r and r["fg"] is not None and "bold" in r["attrs"],
+ str(r),
+ )
+ check(
+ "reset ends the run",
+ snap.style_at(cx + 3, cy) is None,
+ str(snap.style_at(cx + 3, cy)),
+ )
def test_wide_and_clusters():
with Session(SH('printf "[\\346\\227\\245]"; sleep 5'), cols=30, rows=6) as s:
s.settle()
p, snap = s.pane(), s.snapshot()
- check("wide char occupies one cell in the dump",
- snap.pane_line(p, 0).startswith("[日]"), repr(snap.pane_line(p, 0)))
+ check(
+ "wide char occupies one cell in the dump",
+ snap.pane_line(p, 0).startswith("[日]"),
+ repr(snap.pane_line(p, 0)),
+ )
- with Session(SH('printf "[\\360\\237\\207\\263\\360\\237\\207\\261]"; sleep 5'),
- cols=30, rows=6) as s:
+ with Session(
+ SH('printf "[\\360\\237\\207\\263\\360\\237\\207\\261]"; sleep 5'),
+ cols=30,
+ rows=6,
+ ) as s:
s.settle()
p, snap = s.pane(), s.snapshot()
- check("a flag is one grapheme cluster, not two",
- snap.pane_line(p, 0).startswith("[🇳🇱]"), repr(snap.pane_line(p, 0)))
+ check(
+ "a flag is one grapheme cluster, not two",
+ snap.pane_line(p, 0).startswith("[🇳🇱]"),
+ repr(snap.pane_line(p, 0)),
+ )
def test_scrollback():
@@ -78,10 +111,12 @@ def test_scrollback():
s.raw("\\r\\n".join(str(i) for i in range(1, n + 3)))
s.settle()
snap = s.snapshot()
- check("viewport shows the tail, not the head",
- snap.pane_line(p, 0).startswith("3")
- and snap.pane_line(p, n - 1).startswith(str(n + 2)),
- repr(snap.pane_text(p)))
+ check(
+ "viewport shows the tail, not the head",
+ snap.pane_line(p, 0).startswith("3")
+ and snap.pane_line(p, n - 1).startswith(str(n + 2)),
+ repr(snap.pane_text(p)),
+ )
def test_resize():
@@ -90,14 +125,18 @@ def test_resize():
s.resize(50, 12)
s.settle()
snap, p = s.snapshot(), s.pane()
- check("resize changes the composited size",
- snap.cols == 50 and snap.rows == 12 and len(snap.line(0)) == 50,
- f"{snap.cols}x{snap.rows}")
+ check(
+ "resize changes the composited size",
+ snap.cols == 50 and snap.rows == 12 and len(snap.line(0)) == 50,
+ f"{snap.cols}x{snap.rows}",
+ )
# chrome budget: gap 1 (x2 aspect => 2 cols) each side, a border, the
# one-row tab strip along the top and the status line along the bottom
- check("the pane grows with the screen, minus its chrome",
- p["content_w"] == snap.cols - 6 and p["content_h"] == snap.rows - 6,
- str(p))
+ check(
+ "the pane grows with the screen, minus its chrome",
+ p["content_w"] == snap.cols - 6 and p["content_h"] == snap.rows - 6,
+ str(p),
+ )
check("pane still responds after resize", s.alive())
@@ -113,24 +152,33 @@ def test_input_round_trip():
s.settle()
s.send(r"\e[98;5u") # kitty ctrl-b -> legacy pane
s.settle()
- check("kitty key is re-encoded for a legacy pane",
- "^B" in s.snapshot().screen())
+ check(
+ "kitty key is re-encoded for a legacy pane", "^B" in s.snapshot().screen()
+ )
with Session(CAT, cols=50, rows=8) as s:
s.settle()
s.send(r"\e[200~pasted\e[201~")
s.settle()
- check("paste is unwrapped for a pane that did not ask",
- "pasted" in s.snapshot().screen())
+ check(
+ "paste is unwrapped for a pane that did not ask",
+ "pasted" in s.snapshot().screen(),
+ )
- kitty = ["/bin/sh", "-c",
- 'stty raw -echo; printf "\\033[>1u\\033[?1000h\\033[?1006h"; cat -v']
+ kitty = [
+ "/bin/sh",
+ "-c",
+ 'stty raw -echo; printf "\\033[>1u\\033[?1000h\\033[?1006h"; cat -v',
+ ]
with Session(kitty, cols=50, rows=8) as s:
s.settle()
s.send(r"\x02") # legacy ctrl-b -> kitty pane
s.settle()
- check("legacy key is re-encoded for a kitty pane",
- "^[[98;5u" in s.snapshot().screen(), s.snapshot().screen())
+ check(
+ "legacy key is re-encoded for a kitty pane",
+ "^[[98;5u" in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
def test_terminal_replies():
@@ -143,26 +191,37 @@ def test_terminal_replies():
with Session(SH('stty raw -echo; printf "\\033[6n"; cat -v'), cols=40, rows=8) as s:
s.settle()
out = s.snapshot().pane_text(s.pane())
- check("device status report is answered", "R" in out and "^[[" in out,
- repr(out[:60]))
+ check(
+ "device status report is answered",
+ "R" in out and "^[[" in out,
+ repr(out[:60]),
+ )
with Session(SH('printf "\\033]2;build.sh\\007"; sleep 5'), cols=44, rows=8) as s:
s.settle()
p = s.pane()
check("OSC 2 sets the pane title", p["title"] == "build.sh", str(p["title"]))
- check("the title is drawn in the frame",
- "build.sh" in s.snapshot().line(p["y"]), repr(s.snapshot().line(p["y"])))
+ check(
+ "the title is drawn in the frame",
+ "build.sh" in s.snapshot().line(p["y"]),
+ repr(s.snapshot().line(p["y"])),
+ )
def test_hitlist_contract():
with Session(SH("sleep 5"), cols=30, rows=6) as s:
s.settle()
p, snap = s.pane(), s.snapshot()
- check("the pane body is hit-testable",
- snap.hit_at(p["content_x"], p["content_y"]) == f"pane:{p['id']}",
- str(snap.hit_at(p["content_x"], p["content_y"])))
- check("the gap ring belongs to nothing", snap.hit_at(0, 0) is None,
- str(snap.hit_at(0, 0)))
+ check(
+ "the pane body is hit-testable",
+ snap.hit_at(p["content_x"], p["content_y"]) == f"pane:{p['id']}",
+ str(snap.hit_at(p["content_x"], p["content_y"])),
+ )
+ check(
+ "the gap ring belongs to nothing",
+ snap.hit_at(0, 0) is None,
+ str(snap.hit_at(0, 0)),
+ )
if __name__ == "__main__":
diff --git a/tests/test_scroll.py b/tests/test_scroll.py
index 8157f15..9ddc6dc 100644
--- a/tests/test_scroll.py
+++ b/tests/test_scroll.py
@@ -6,6 +6,7 @@ us — it is deciding whose wheel it is. A program tracking the mouse gets the
events; a full-screen program without mouse tracking has no scrollback of ours
to show, so the wheel means arrow keys to it; everything else scrolls us.
"""
+
import sys
from harness import Session, check, report
@@ -33,8 +34,11 @@ def test_wheel_scrolls():
s.settle()
fill(s)
p = s.pane()
- check("the viewport is at the bottom", "60" in s.snapshot().pane_text(p),
- repr(s.snapshot().pane_text(p)))
+ check(
+ "the viewport is at the bottom",
+ "60" in s.snapshot().pane_text(p),
+ repr(s.snapshot().pane_text(p)),
+ )
wheel(s, p["content_x"] + 2, p["content_y"] + 2, up=True)
s.settle(80)
@@ -43,8 +47,11 @@ def test_wheel_scrolls():
wheel(s, p["content_x"] + 2, p["content_y"] + 2, up=False)
s.settle(80)
- check("and forward again", "60" in s.snapshot().pane_text(p),
- repr(s.snapshot().pane_text(p)))
+ check(
+ "and forward again",
+ "60" in s.snapshot().pane_text(p),
+ repr(s.snapshot().pane_text(p)),
+ )
def test_indicator():
@@ -53,8 +60,11 @@ def test_indicator():
fill(s)
p = s.pane()
snap = s.snapshot()
- check("no indicator when the viewport is live",
- "▲" not in snap.line(p["y"]), repr(snap.line(p["y"])))
+ check(
+ "no indicator when the viewport is live",
+ "▲" not in snap.line(p["y"]),
+ repr(snap.line(p["y"])),
+ )
for _ in range(3):
wheel(s, p["content_x"] + 2, p["content_y"] + 2, up=True)
@@ -63,19 +73,26 @@ def test_indicator():
row = snap.line(p["y"])
# A space between the arrow and the count: U+25B2 is drawn wide enough
# in plenty of fonts to touch what follows it.
- check("scrolling shows how far down the live view is", "\u25b2 9" in row,
- repr(row))
+ check(
+ "scrolling shows how far down the live view is",
+ "\u25b2 9" in row,
+ repr(row),
+ )
x = row.index("▲")
- check("the indicator is clickable",
- snap.hit_at(x, p["y"]) == f"scrollbottom:{p['id']}",
- str(snap.hit_at(x, p["y"])))
+ check(
+ "the indicator is clickable",
+ snap.hit_at(x, p["y"]) == f"scrollbottom:{p['id']}",
+ str(snap.hit_at(x, p["y"])),
+ )
s.click(x, p["y"])
s.settle(80)
snap = s.snapshot()
- check("clicking it returns to the live view",
- "▲" not in snap.line(p["y"]) and "60" in snap.pane_text(p),
- repr(snap.line(p["y"])))
+ check(
+ "clicking it returns to the live view",
+ "▲" not in snap.line(p["y"]) and "60" in snap.pane_text(p),
+ repr(snap.line(p["y"])),
+ )
def test_keyboard():
@@ -85,8 +102,11 @@ def test_keyboard():
p = s.pane()
s.send(r"\x01\e[5~") # C-a PageUp
s.settle(80)
- check("C-a PageUp scrolls a page", "60" not in s.snapshot().pane_text(p),
- repr(s.snapshot().pane_text(p)))
+ check(
+ "C-a PageUp scrolls a page",
+ "60" not in s.snapshot().pane_text(p),
+ repr(s.snapshot().pane_text(p)),
+ )
s.send(r"\x01\e[6~") # C-a PageDown
s.settle(80)
@@ -97,13 +117,15 @@ def test_keyboard():
text = s.snapshot().pane_text(p)
# the top of the scrollback is the session's very first line, which is
# the shell's first prompt — not the first line of `seq`
- check("C-a Home goes to the top of the scrollback",
- "sh-5.3$" in text and "60" not in text, repr(text[:120]))
+ check(
+ "C-a Home goes to the top of the scrollback",
+ "sh-5.3$" in text and "60" not in text,
+ repr(text[:120]),
+ )
s.send(r"\x01\e[F") # C-a End
s.settle(80)
- check("C-a End returns to the bottom",
- "60" in s.snapshot().pane_text(p))
+ check("C-a End returns to the bottom", "60" in s.snapshot().pane_text(p))
def test_typing_returns_to_the_live_view():
@@ -119,9 +141,11 @@ def test_typing_returns_to_the_live_view():
s.settle(120)
text = s.snapshot().pane_text(p)
check("typing snaps back to the bottom", "60" in text, repr(text[-80:]))
- check("and the indicator goes with it",
- "▲" not in s.snapshot().line(p["y"]),
- repr(s.snapshot().line(p["y"])))
+ check(
+ "and the indicator goes with it",
+ "▲" not in s.snapshot().line(p["y"]),
+ repr(s.snapshot().line(p["y"])),
+ )
def test_scroll_position_is_per_pane():
@@ -135,41 +159,52 @@ def test_scroll_position_is_per_pane():
s.send(r"\x01\e[5~")
s.settle(80)
snap = s.snapshot()
+
# side-by-side panes share a screen row, so read each one's columns
def frame_of(pane):
- return snap.line(pane["y"])[pane["x"]:pane["x"] + pane["w"]]
+ return snap.line(pane["y"])[pane["x"] : pane["x"] + pane["w"]]
check("one pane is scrolled", "▲" in frame_of(left), repr(frame_of(left)))
- check("the other is not", "▲" not in frame_of(right),
- repr(frame_of(right)))
+ check("the other is not", "▲" not in frame_of(right), repr(frame_of(right)))
def test_who_owns_the_wheel():
# a full-screen program with no mouse tracking: the wheel means arrows
- alt = ["/bin/sh", "-c",
- 'stty raw -echo; printf "\\033[?1049h"; cat -v']
+ alt = ["/bin/sh", "-c", 'stty raw -echo; printf "\\033[?1049h"; cat -v']
with Session(alt, cols=60, rows=12) as s:
s.settle()
p = s.pane()
wheel(s, p["content_x"] + 2, p["content_y"] + 2, up=True)
s.settle(80)
out = s.snapshot().pane_text(p)
- check("on the alternate screen the wheel becomes arrow keys",
- out.count("^[[A") == 3, repr(out[:120]))
- check("and does not scroll us instead",
- "▲" not in s.snapshot().line(p["y"]), repr(s.snapshot().line(p["y"])))
+ check(
+ "on the alternate screen the wheel becomes arrow keys",
+ out.count("^[[A") == 3,
+ repr(out[:120]),
+ )
+ check(
+ "and does not scroll us instead",
+ "▲" not in s.snapshot().line(p["y"]),
+ repr(s.snapshot().line(p["y"])),
+ )
# a program tracking the mouse gets the wheel events themselves
- tracker = ["/bin/sh", "-c",
- 'stty raw -echo; printf "\\033[?1000h\\033[?1006h"; cat -v']
+ tracker = [
+ "/bin/sh",
+ "-c",
+ 'stty raw -echo; printf "\\033[?1000h\\033[?1006h"; cat -v',
+ ]
with Session(tracker, cols=60, rows=12) as s:
s.settle()
p = s.pane()
wheel(s, p["content_x"] + 2, p["content_y"] + 2, up=True)
s.settle(80)
out = s.snapshot().pane_text(p)
- check("a mouse-tracking program gets the wheel", "^[[<64;3;3M" in out,
- repr(out[:120]))
+ check(
+ "a mouse-tracking program gets the wheel",
+ "^[[<64;3;3M" in out,
+ repr(out[:120]),
+ )
def test_output_while_scrolled():
@@ -184,11 +219,16 @@ def test_output_while_scrolled():
s.raw("echo NEWLINE\\n")
s.settle(150)
after = s.snapshot()
- check("new output does not yank the viewport back",
- after.pane_line(p, 0) == before,
- f"{before!r} -> {after.pane_line(p, 0)!r}")
- check("but the indicator shows the gap growing",
- "▲" in after.line(p["y"]), repr(after.line(p["y"])))
+ check(
+ "new output does not yank the viewport back",
+ after.pane_line(p, 0) == before,
+ f"{before!r} -> {after.pane_line(p, 0)!r}",
+ )
+ check(
+ "but the indicator shows the gap growing",
+ "▲" in after.line(p["y"]),
+ repr(after.line(p["y"])),
+ )
if __name__ == "__main__":
diff --git a/tests/test_scrollback.py b/tests/test_scrollback.py
index 8654202..bd52d83 100644
--- a/tests/test_scrollback.py
+++ b/tests/test_scrollback.py
@@ -4,6 +4,7 @@ lib-vt is handed no limit and picks its own: 10,000 *bytes*, which measured at 6
lines of an 80-column pane. Nobody chose that number, and nothing could change it
— D4 promised scrollback and then left the size to a library default.
"""
+
import os
import re
import sys
@@ -17,9 +18,11 @@ TOP = r"\x01\x1b[1~" # C-a Home: scroll to the oldest line
def spew(n):
"""A pane that prints `n` numbered lines and then waits."""
- return ["/bin/sh", "-c",
- 'i=1; while [ $i -le %d ]; do echo "line-$i"; i=$((i+1)); done; read x'
- % n]
+ return [
+ "/bin/sh",
+ "-c",
+ 'i=1; while [ $i -le %d ]; do echo "line-$i"; i=$((i+1)); done; read x' % n,
+ ]
def conf(text):
@@ -53,15 +56,21 @@ def test_a_pane_keeps_the_configured_number_of_lines():
cfg = conf("scrollback 400\n")
with Session(spew(3000), cols=80, rows=8, config=cfg) as s:
n = kept(s, 3000)
- check("a small setting is honoured", n is not None and 250 < n < 700,
- "kept %s of 3000" % n)
+ check(
+ "a small setting is honoured",
+ n is not None and 250 < n < 700,
+ "kept %s of 3000" % n,
+ )
os.unlink(cfg)
cfg = conf("scrollback 2000\n")
with Session(spew(3000), cols=80, rows=8, config=cfg) as s:
n = kept(s, 3000)
- check("and a larger one keeps more", n is not None and n > 1500,
- "kept %s of 3000" % n)
+ check(
+ "and a larger one keeps more",
+ n is not None and n > 1500,
+ "kept %s of 3000" % n,
+ )
os.unlink(cfg)
@@ -70,10 +79,16 @@ def test_zero_keeps_no_history_at_all():
cfg = conf("scrollback 0\n")
with Session(spew(3000), cols=80, rows=8, config=cfg) as s:
n = kept(s, 3000)
- check("nothing is retained past the viewport", n is not None and n <= 10,
- "kept %s of 3000" % n)
- check("and scrolling up says so", "scrolled" not in s.snapshot().screen(),
- s.snapshot().screen())
+ check(
+ "nothing is retained past the viewport",
+ n is not None and n <= 10,
+ "kept %s of 3000" % n,
+ )
+ check(
+ "and scrolling up says so",
+ "scrolled" not in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
os.unlink(cfg)
@@ -82,8 +97,11 @@ def test_the_default_is_not_a_library_accident():
longer than that, which is exactly when scrollback is wanted."""
with Session(spew(3000), cols=80, rows=8) as s:
n = kept(s, 3000)
- check("the default keeps everything a 3000-line run printed",
- n is not None and n > 2900, "kept %s of 3000" % n)
+ check(
+ "the default keeps everything a 3000-line run printed",
+ n is not None and n > 2900,
+ "kept %s of 3000" % n,
+ )
def test_the_byte_ceiling_is_the_other_limit():
@@ -93,25 +111,35 @@ def test_the_byte_ceiling_is_the_other_limit():
cfg = conf("scrollback 5000\nscrollback_bytes 500000\n")
with Session(spew(3000), cols=80, rows=8, config=cfg) as s:
n = kept(s, 3000)
- check("a low ceiling clips a high line count",
- n is not None and n < 1200, "kept %s of 3000" % n)
+ check(
+ "a low ceiling clips a high line count",
+ n is not None and n < 1200,
+ "kept %s of 3000" % n,
+ )
os.unlink(cfg)
cfg = conf("scrollback 5000\nscrollback_bytes 0\n")
with Session(spew(3000), cols=80, rows=8, config=cfg) as s:
n = kept(s, 3000)
- check("and no ceiling lets the line count stand",
- n is not None and n > 2900, "kept %s of 3000" % n)
+ check(
+ "and no ceiling lets the line count stand",
+ n is not None and n > 2900,
+ "kept %s of 3000" % n,
+ )
os.unlink(cfg)
def test_a_negative_is_refused_rather_than_wrapped():
"""A signed value read into a size_t is how "unlimited" gets into a program by
accident: eighteen quintillion lines, and nobody asked for it."""
- from harness import BIN
import subprocess
- for line, word in (("scrollback -1\n", "scrollback is a number of lines"),
- ("scrollback_bytes -1\n", "scrollback_bytes is a byte")):
+
+ from harness import BIN
+
+ for line, word in (
+ ("scrollback -1\n", "scrollback is a number of lines"),
+ ("scrollback_bytes -1\n", "scrollback_bytes is a byte"),
+ ):
cfg = conf(line)
out = subprocess.run([BIN, "--check", cfg], capture_output=True, text=True)
said = out.stderr + out.stdout
@@ -126,16 +154,22 @@ def test_it_applies_to_the_next_pane_not_the_ones_holding_history():
cfg = conf("scrollback 2000\n")
with Session(spew(3000), cols=80, rows=8, config=cfg) as s:
before = kept(s, 3000)
- check("the pane has its history", before is not None and before > 1500,
- "kept %s" % before)
+ check(
+ "the pane has its history",
+ before is not None and before > 1500,
+ "kept %s" % before,
+ )
with open(cfg, "w") as f:
f.write("scrollback 0\n")
r = s.api("reload")
check("the config reloads", r["ok"], str(r))
after = kept(s, 3000)
- check("and the pane already open keeps what it had",
- after is not None and after > 1500, "kept %s" % after)
+ check(
+ "and the pane already open keeps what it had",
+ after is not None and after > 1500,
+ "kept %s" % after,
+ )
os.unlink(cfg)
diff --git a/tests/test_session.py b/tests/test_session.py
index f8aa30d..cd6ab58 100644
--- a/tests/test_session.py
+++ b/tests/test_session.py
@@ -4,6 +4,7 @@
The point of the whole milestone is that the session outlives the client, so
the assertions are about what survives: content, layout, and the process.
"""
+
import fcntl
import os
import pty
@@ -16,7 +17,10 @@ import termios
import time
import uuid
-BIN = os.environ.get("SL0PPTY_BIN", os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "build", "sl0ppty"))
+BIN = os.environ.get(
+ "SL0PPTY_BIN",
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "build", "sl0ppty"),
+)
fails = 0
@@ -62,8 +66,9 @@ def drain(fd, idle=0.3, limit=3.0):
def control(name, line):
- out = subprocess.run([BIN, "-s", name, "cmd", line],
- capture_output=True, text=True, timeout=10)
+ out = subprocess.run(
+ [BIN, "-s", name, "cmd", line], capture_output=True, text=True, timeout=10
+ )
return out.stdout.strip()
@@ -88,21 +93,30 @@ def main():
drain(fd)
os.write(fd, b"marker-alpha")
out = drain(fd)
- check("a session starts and shows typed text", b"marker-alpha" in out,
- repr(out[-80:]))
+ check(
+ "a session starts and shows typed text", b"marker-alpha" in out, repr(out[-80:])
+ )
- check("the session is listed as running",
- name in subprocess.run([BIN, "ls"], capture_output=True, text=True).stdout,
- "")
+ check(
+ "the session is listed as running",
+ name in subprocess.run([BIN, "ls"], capture_output=True, text=True).stdout,
+ "",
+ )
# --- split, so the layout is worth preserving -----------------------
os.write(fd, b"\x01\\")
drain(fd)
panes_before = control(name, "panes")
- check("control socket answers while attached", panes_before.startswith("["),
- panes_before[:60])
- check("the split is visible to the control socket",
- panes_before.count('"id"') == 2, panes_before[:120])
+ check(
+ "control socket answers while attached",
+ panes_before.startswith("["),
+ panes_before[:60],
+ )
+ check(
+ "the split is visible to the control socket",
+ panes_before.count('"id"') == 2,
+ panes_before[:120],
+ )
# --- probes must not disturb the client -----------------------------
# Regression: `ls` connects to check liveness. When a connection counted
@@ -118,8 +132,7 @@ def main():
os.write(fd, b"-still-here")
out = drain(fd)
- check("the client survived 40 connections", b"still-here" in out,
- repr(out[-80:]))
+ check("the client survived 40 connections", b"still-here" in out, repr(out[-80:]))
# --- detach ---------------------------------------------------------
os.write(fd, b"\x01d")
@@ -130,24 +143,32 @@ def main():
# --- the session must still be there --------------------------------
time.sleep(0.2)
panes_after = control(name, "panes")
- check("the session outlives its client", panes_after.startswith("["),
- panes_after[:60])
- check("the layout survived the detach",
- panes_after.count('"id"') == 2, panes_after[:120])
+ check(
+ "the session outlives its client", panes_after.startswith("["), panes_after[:60]
+ )
+ check(
+ "the layout survived the detach",
+ panes_after.count('"id"') == 2,
+ panes_after[:120],
+ )
snap = control(name, "snapshot text")
- check("pane content survived the detach", "marker-alpha" in snap,
- repr(snap[:120]))
- check("a detached session can still be driven",
- control(name, "raw \\r\\ntyped-while-detached") == ""
- and "typed-while-detached" in control(name, "snapshot text"),
- repr(control(name, "snapshot text")[:200]))
+ check("pane content survived the detach", "marker-alpha" in snap, repr(snap[:120]))
+ check(
+ "a detached session can still be driven",
+ control(name, "raw \\r\\ntyped-while-detached") == ""
+ and "typed-while-detached" in control(name, "snapshot text"),
+ repr(control(name, "snapshot text")[:200]),
+ )
# --- reattach -------------------------------------------------------
pid2, fd2 = attach(name)
out = drain(fd2)
- check("reattaching repaints the whole screen", b"marker-alpha" in out,
- repr(out[-120:]))
+ check(
+ "reattaching repaints the whole screen",
+ b"marker-alpha" in out,
+ repr(out[-120:]),
+ )
check("reattached client can still type", True)
os.write(fd2, b"-beta")
out = drain(fd2)
@@ -157,8 +178,11 @@ def main():
pid3, fd3 = attach(name)
drain(fd3)
tail = drain(fd2, idle=0.2, limit=2.0)
- check("the displaced client is told it was replaced",
- b"replaced" in tail or wait_gone(pid2, 1.0), repr(tail[-60:]))
+ check(
+ "the displaced client is told it was replaced",
+ b"replaced" in tail or wait_gone(pid2, 1.0),
+ repr(tail[-60:]),
+ )
# --- closing the last pane ends the session -------------------------
os.write(fd3, b"\x01x") # close pane 2
diff --git a/tests/test_shader_expr.py b/tests/test_shader_expr.py
index 834e5c1..38ec6f0 100644
--- a/tests/test_shader_expr.py
+++ b/tests/test_shader_expr.py
@@ -6,14 +6,18 @@ one of them constant. What is being checked here is that the expression really
reaches the cells, that a broken one costs its own shader and nothing else,
and that the states table gets the same treatment as the global chain.
"""
+
import sys
import tempfile
from harness import Session, check, report
# Prints a full row of known-coloured text, then waits.
-SH = ["/bin/sh", "-c",
- 'printf "\\033]2;p\\007"; printf "%s\\n" ABCDEFGHIJKLMNOPQRSTUVWXYZ; read x']
+SH = [
+ "/bin/sh",
+ "-c",
+ 'printf "\\033]2;p\\007"; printf "%s\\n" ABCDEFGHIJKLMNOPQRSTUVWXYZ; read x',
+]
def cfg(text):
@@ -31,8 +35,10 @@ def cfg(text):
# effect: that is the difference between "left alone" and "repainted the same".
def fg_row(snap, pane, row=0):
y = pane["content_y"] + row
- return [(snap.style_at(pane["content_x"] + x, y) or {}).get("fg")
- for x in range(pane["content_w"])]
+ return [
+ (snap.style_at(pane["content_x"] + x, y) or {}).get("fg")
+ for x in range(pane["content_w"])
+ ]
def test_an_expression_decides_the_amount_per_cell():
@@ -40,22 +46,33 @@ def test_an_expression_decides_the_amount_per_cell():
with Session(SH, cols=44, rows=8, config=c) as s:
snap = s.until_text("ABC")
row = fg_row(snap, s.pane())
- check("the first ten columns are dimmed",
- all(c == row[0] and c != "#ffffff" for c in row[:10]), str(row[:12]))
- check("and the rest are untouched, not repainted the same",
- all(c is None for c in row[10:20]), str(row[10:20]))
+ check(
+ "the first ten columns are dimmed",
+ all(c == row[0] and c != "#ffffff" for c in row[:10]),
+ str(row[:12]),
+ )
+ check(
+ "and the rest are untouched, not repainted the same",
+ all(c is None for c in row[10:20]),
+ str(row[10:20]),
+ )
def test_a_constant_expression_is_just_a_number():
"""Folded at compile time, so it has to behave identically to writing it."""
a = cfg('shaders {\n dim amount="100 + 28"\n}\n')
- b = cfg('shaders {\n dim amount=128\n}\n')
- with Session(SH, cols=44, rows=8, config=a) as s1, \
- Session(SH, cols=44, rows=8, config=b) as s2:
+ b = cfg("shaders {\n dim amount=128\n}\n")
+ with (
+ Session(SH, cols=44, rows=8, config=a) as s1,
+ Session(SH, cols=44, rows=8, config=b) as s2,
+ ):
r1 = fg_row(s1.until_text("ABC"), s1.pane())
r2 = fg_row(s2.until_text("ABC"), s2.pane())
- check("an expression that reads nothing matches the number", r1 == r2,
- f"{r1[:4]} vs {r2[:4]}")
+ check(
+ "an expression that reads nothing matches the number",
+ r1 == r2,
+ f"{r1[:4]} vs {r2[:4]}",
+ )
def test_the_rect_is_known_to_the_expression():
@@ -66,9 +83,11 @@ def test_the_rect_is_known_to_the_expression():
snap = s.until_text("ABC")
pane = s.pane()
row = fg_row(snap, pane)
- check("the last columns are dimmed, not the first",
- row[0] is None and row[-1] not in (None, "#ffffff"),
- f"{row[0]} .. {row[-1]}")
+ check(
+ "the last columns are dimmed, not the first",
+ row[0] is None and row[-1] not in (None, "#ffffff"),
+ f"{row[0]} .. {row[-1]}",
+ )
def test_it_follows_a_resize():
@@ -82,22 +101,25 @@ def test_it_follows_a_resize():
row = fg_row(snap, pane)
# The cached map has to be rebuilt for the new rect, or the dimmed
# band would still be sitting where the old right-hand edge was.
- check("the band moved with the edge",
- row[-1] not in (None, "#ffffff") and row[len(row) - 8] is None,
- str(row[-10:]))
+ check(
+ "the band moved with the edge",
+ row[-1] not in (None, "#ffffff") and row[len(row) - 8] is None,
+ str(row[-10:]),
+ )
def test_a_broken_expression_costs_only_its_own_shader():
- c = cfg('shaders {\n'
- ' dim amount="nosuchthing * 2"\n'
- ' tint color="#00ff00" amount="255"\n'
- '}\n')
+ c = cfg(
+ "shaders {\n"
+ ' dim amount="nosuchthing * 2"\n'
+ ' tint color="#00ff00" amount="255"\n'
+ "}\n"
+ )
with Session(SH, cols=44, rows=8, config=c) as s:
snap = s.until_text("ABC")
check("the session starts anyway", s.alive())
row = fg_row(snap, s.pane())
- check("and the shader after it still ran", row[0] == "#00ff00",
- str(row[:3]))
+ check("and the shader after it still ran", row[0] == "#00ff00", str(row[:3]))
def test_states_take_expressions_too():
@@ -112,8 +134,9 @@ def test_states_take_expressions_too():
r0 = fg_row(snap, unfocused, 0)
r1 = fg_row(snap, unfocused, 1)
check("even rows are left alone", r0[0] is None, str(r0[:3]))
- check("odd rows are dimmed", r1[0] != "#ffffff" and r1[0] is not None,
- str(r1[:3]))
+ check(
+ "odd rows are dimmed", r1[0] != "#ffffff" and r1[0] is not None, str(r1[:3])
+ )
def test_an_expression_cannot_hang_the_session():
@@ -126,9 +149,11 @@ def test_an_expression_cannot_hang_the_session():
check("the session is alive and drawing", s.alive())
# Unstyled, not merely undimmed: with its only shader refused there
# is no pass at all, so the cells still defer to the terminal.
- check("and the refused shader did nothing",
- fg_row(snap, s.pane())[0] in (None, "#ffffff"),
- str(fg_row(snap, s.pane())[:3]))
+ check(
+ "and the refused shader did nothing",
+ fg_row(snap, s.pane())[0] in (None, "#ffffff"),
+ str(fg_row(snap, s.pane())[:3]),
+ )
for name, fn in sorted(list(globals().items())):
diff --git a/tests/test_shader_plugin.py b/tests/test_shader_plugin.py
index 9648a0d..cd7e9bd 100644
--- a/tests/test_shader_plugin.py
+++ b/tests/test_shader_plugin.py
@@ -9,6 +9,7 @@ that is not there.
The example in contrib/ is built and loaded here too, so it cannot rot.
"""
+
import os
import shutil
import sys
@@ -50,18 +51,25 @@ def fg_at(snap, pane, row=0, col=0):
def test_a_plugin_shader_is_used_like_a_builtin():
- cfg, d = session_dir([GOOD], 'shader_dir "%s"\nshaders {\n'
- ' testfill amount=255 color="#00ff00"\n}\n')
+ cfg, d = session_dir(
+ [GOOD],
+ 'shader_dir "%s"\nshaders {\n testfill amount=255 color="#00ff00"\n}\n',
+ )
# the directory is known only after it exists, so write the config now
with open(cfg, "w") as f:
- f.write('shader_dir "%s"\nshaders {\n'
- ' testfill amount=255 color="#00ff00"\n}\n' % d)
+ f.write(
+ 'shader_dir "%s"\nshaders {\n'
+ ' testfill amount=255 color="#00ff00"\n}\n' % d
+ )
with Session(SH, cols=40, rows=10, config=cfg) as s:
snap = s.until_text("BLOCK")
pane = s.pane()
- check("the loaded shader ran", bg_at(snap, pane) == "#00ff00",
- str(snap.style_at(pane["content_x"], pane["content_y"])))
+ check(
+ "the loaded shader ran",
+ bg_at(snap, pane) == "#00ff00",
+ str(snap.style_at(pane["content_x"], pane["content_y"])),
+ )
def test_it_is_found_beside_the_config_by_default():
@@ -71,42 +79,53 @@ def test_it_is_found_beside_the_config_by_default():
with Session(SH, cols=40, rows=10, config=cfg) as s:
snap = s.until_text("BLOCK")
- check("no shader_dir needed", bg_at(snap, s.pane()) == "#0000ff",
- snap.screen())
+ check("no shader_dir needed", bg_at(snap, s.pane()) == "#0000ff", snap.screen())
def test_the_config_number_reaches_the_plugin():
cfg, d = session_dir([GOOD], "")
with open(cfg, "w") as f:
- f.write('shader_dir "%s"\nshaders {\n'
- ' teststripe amount=255 band=3 color="#ff0000"\n}\n' % d)
+ f.write(
+ 'shader_dir "%s"\nshaders {\n'
+ ' teststripe amount=255 band=3 color="#ff0000"\n}\n' % d
+ )
with Session(SH, cols=40, rows=10, config=cfg) as s:
snap = s.until_text("BLOCK")
pane = s.pane()
got = [bg_at(snap, pane, 0, x) for x in range(6)]
- check("every third column, as asked",
- got[0] == "#ff0000" and got[3] == "#ff0000" and
- got[1] != "#ff0000" and got[2] != "#ff0000", str(got))
+ check(
+ "every third column, as asked",
+ got[0] == "#ff0000"
+ and got[3] == "#ff0000"
+ and got[1] != "#ff0000"
+ and got[2] != "#ff0000",
+ str(got),
+ )
def test_a_stale_plugin_is_refused_and_the_rest_still_load():
cfg, d = session_dir([GOOD, BAD], "")
with open(cfg, "w") as f:
- f.write('shader_dir "%s"\nshaders {\n'
- ' testfill amount=255 color="#00ff00"\n}\n' % d)
+ f.write(
+ 'shader_dir "%s"\nshaders {\n'
+ ' testfill amount=255 color="#00ff00"\n}\n' % d
+ )
with Session(SH, cols=40, rows=10, config=cfg) as s:
snap = s.until_text("BLOCK")
check("the session is alive", s.alive())
- check("the good plugin still loaded",
- bg_at(snap, s.pane()) == "#00ff00", snap.screen())
+ check(
+ "the good plugin still loaded",
+ bg_at(snap, s.pane()) == "#00ff00",
+ snap.screen(),
+ )
def test_an_unknown_shader_is_skipped_not_fatal():
cfg, d = session_dir([], "")
with open(cfg, "w") as f:
- f.write('shaders {\n nosuchshader amount=255\n dim amount=0\n}\n')
+ f.write("shaders {\n nosuchshader amount=255\n dim amount=0\n}\n")
with Session(SH, cols=40, rows=10, config=cfg) as s:
snap = s.until_text("BLOCK")
@@ -117,8 +136,10 @@ def test_an_unknown_shader_is_skipped_not_fatal():
def test_no_shader_directory_at_all_is_normal():
cfg, d = session_dir([], "")
with open(cfg, "w") as f:
- f.write('shader_dir "/nonexistent/sl0ppty/shaders"\n'
- 'shaders {\n dim amount=100\n}\n')
+ f.write(
+ 'shader_dir "/nonexistent/sl0ppty/shaders"\n'
+ "shaders {\n dim amount=100\n}\n"
+ )
with Session(SH, cols=40, rows=10, config=cfg) as s:
snap = s.until_text("BLOCK")
@@ -130,20 +151,24 @@ def test_the_directory_may_be_written_with_a_tilde():
cfg, d = session_dir([GOOD], "", shaders_subdir=True)
home = os.path.dirname(d)
with open(cfg, "w") as f:
- f.write('shader_dir "~/shaders"\nshaders {\n'
- ' testfill amount=255 color="#00ff00"\n}\n')
+ f.write(
+ 'shader_dir "~/shaders"\nshaders {\n'
+ ' testfill amount=255 color="#00ff00"\n}\n'
+ )
with Session(SH, cols=40, rows=10, config=cfg, env={"HOME": home}) as s:
snap = s.until_text("BLOCK")
- check("~ in shader_dir is the home directory",
- bg_at(snap, s.pane()) == "#00ff00", snap.screen())
+ check(
+ "~ in shader_dir is the home directory",
+ bg_at(snap, s.pane()) == "#00ff00",
+ snap.screen(),
+ )
def test_the_contrib_example_builds_and_loads():
cfg, d = session_dir([EXAMPLE], "")
with open(cfg, "w") as f:
- f.write('shader_dir "%s"\nshaders {\n'
- ' checker amount=255 band=2\n}\n' % d)
+ f.write('shader_dir "%s"\nshaders {\n checker amount=255 band=2\n}\n' % d)
with Session(SH, cols=40, rows=10, config=cfg) as s:
snap = s.until_text("BLOCK")
diff --git a/tests/test_shader_presets.py b/tests/test_shader_presets.py
index 3ab3c0e..504a8f6 100644
--- a/tests/test_shader_presets.py
+++ b/tests/test_shader_presets.py
@@ -10,6 +10,7 @@ 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
@@ -25,10 +26,14 @@ 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']
+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.
@@ -74,8 +79,11 @@ def test_every_preset_changes_the_pane():
while now == base and time.monotonic() < deadline:
s.settle(20)
now = cells(s.snapshot(), s.pane())
- check(f"{name} changes the pane", now != base,
- f"identical to unshaded: {now[:3]}")
+ check(
+ f"{name} changes the pane",
+ now != base,
+ f"identical to unshaded: {now[:3]}",
+ )
os.unlink(cfg.name)
@@ -94,28 +102,34 @@ def test_a_few_of_them_do_the_specific_thing_they_claim():
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))
+ 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))
+ 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]))
+ 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)
@@ -133,14 +147,20 @@ def test_the_tour_can_name_every_one_of_them():
# 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())
+ 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())
+ 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)
@@ -150,10 +170,16 @@ def test_the_files_match_the_page_they_came_from():
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())
+ 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())):
diff --git a/tests/test_shader_repl.py b/tests/test_shader_repl.py
index 95cb80e..11a1749 100755
--- a/tests/test_shader_repl.py
+++ b/tests/test_shader_repl.py
@@ -10,6 +10,7 @@ becomes a list of what was true once.
The rest is the loop itself, driven the way a person drives it -- typed lines,
history, a tab -- in a real session with a real pty.
"""
+
import ast
import os
import re
@@ -32,12 +33,17 @@ REPL = os.path.join(ROOT, "contrib", "shader-repl")
def repl_lists():
"""The vocabulary the script offers, read out of the script."""
src = open(REPL).read()
+
def names(var):
body = re.search(r"^%s = (\[[^\]]*\])" % var, src, re.M).group(1)
- return [w.strip().strip('"\'').rstrip("=(").strip('"')
- for w in re.findall(r'["\'][^"\']+["\']', body)]
- return {v: names(v)
- for v in ("COMMANDS", "SHADERS", "PROPS", "VARS", "CONSTS", "FUNCS")}
+ return [
+ w.strip().strip("\"'").rstrip("=(").strip('"')
+ for w in re.findall(r'["\'][^"\']+["\']', body)
+ ]
+
+ return {
+ v: names(v) for v in ("COMMANDS", "SHADERS", "PROPS", "VARS", "CONSTS", "FUNCS")
+ }
def source_names(path, pattern):
@@ -74,8 +80,13 @@ def cfg(text):
def session(cfg_path, data_home, rows=16):
"""A pane running the repl, with its history in a directory we can inspect."""
env = {"XDG_DATA_HOME": data_home, "TERM": "xterm-256color"}
- return Session(["/bin/sh", "-c", "exec python3 %s" % REPL],
- cols=84, rows=rows, config=cfg_path, env=env)
+ return Session(
+ ["/bin/sh", "-c", "exec python3 %s" % REPL],
+ cols=84,
+ rows=rows,
+ config=cfg_path,
+ env=env,
+ )
def test_the_script_is_a_script():
@@ -87,7 +98,10 @@ def test_the_script_is_a_script():
ast.parse(open(REPL).read())
ok, why = True, ""
except SyntaxError as exc:
- ok, why = False, "%s line %s: %s" % (exc.__class__.__name__, exc.lineno, exc.msg)
+ ok, why = (
+ False,
+ "%s line %s: %s" % (exc.__class__.__name__, exc.lineno, exc.msg),
+ )
check("contrib/shader-repl parses", ok, why)
@@ -96,29 +110,40 @@ def test_the_completion_list_matches_the_code():
# src/shader.c's REGISTRY: `{"dim", sh_dim, ...}`
shader_c = open(os.path.join(ROOT, "src", "shader.c")).read()
- registry = shader_c[shader_c.index("REGISTRY[] = {"):]
- registry = registry[:registry.index("\n};")]
+ registry = shader_c[shader_c.index("REGISTRY[] = {") :]
+ registry = registry[: registry.index("\n};")]
builtin = set(re.findall(r'\{"([a-z_-]+)"', registry))
- check("every built-in shader can be completed",
- builtin <= set(lists["SHADERS"]),
- "missing: %s" % sorted(builtin - set(lists["SHADERS"])))
- check("and nothing is offered that is not one",
- set(lists["SHADERS"]) <= builtin,
- "not shaders: %s" % sorted(set(lists["SHADERS"]) - builtin))
+ check(
+ "every built-in shader can be completed",
+ builtin <= set(lists["SHADERS"]),
+ "missing: %s" % sorted(builtin - set(lists["SHADERS"])),
+ )
+ check(
+ "and nothing is offered that is not one",
+ set(lists["SHADERS"]) <= builtin,
+ "not shaders: %s" % sorted(set(lists["SHADERS"]) - builtin),
+ )
variables = source_names("expr.c", r'\{"([a-z_]+)", V_[A-Z_]+')
- check("every expression variable can be completed",
- variables <= set(lists["VARS"]),
- "missing: %s" % sorted(variables - set(lists["VARS"])))
+ check(
+ "every expression variable can be completed",
+ variables <= set(lists["VARS"]),
+ "missing: %s" % sorted(variables - set(lists["VARS"])),
+ )
consts = source_names("expr.c", r'\{"([A-Z]+)", \d+\},?\s')
- check("every constant can be completed", consts <= set(lists["CONSTS"]),
- "missing: %s" % sorted(consts - set(lists["CONSTS"])))
+ check(
+ "every constant can be completed",
+ consts <= set(lists["CONSTS"]),
+ "missing: %s" % sorted(consts - set(lists["CONSTS"])),
+ )
funcs = source_names("expr.c", r'\{"([a-z]+)", \d, OP_[A-Z]+\}')
- check("every expression function can be completed",
- funcs <= set(lists["FUNCS"]),
- "missing: %s" % sorted(funcs - set(lists["FUNCS"])))
+ check(
+ "every expression function can be completed",
+ funcs <= set(lists["FUNCS"]),
+ "missing: %s" % sorted(funcs - set(lists["FUNCS"])),
+ )
def test_every_offered_shader_is_accepted_by_a_session():
@@ -138,8 +163,9 @@ def test_every_offered_shader_is_accepted_by_a_session():
reply = last_reply(s)
if "content" not in reply: # `N chrome, M content` means it ran
refused.append((name, reply))
- check("every completable shader parses in a real session", not refused,
- str(refused))
+ check(
+ "every completable shader parses in a real session", not refused, str(refused)
+ )
os.unlink(path)
@@ -163,16 +189,20 @@ def test_history_survives_the_next_run():
check("a history file is written", os.path.exists(hist), hist)
saved = open(hist).read() if os.path.exists(hist) else ""
check("with the chain that was typed in it", "#00ff88" in saved, repr(saved))
- check("and without the command that ended the run", ":quit" not in saved,
- repr(saved))
+ check(
+ "and without the command that ended the run", ":quit" not in saved, repr(saved)
+ )
with session(path, home) as s:
s.until_text("chrome>")
- s.raw("\\e[A") # up: readline recalls the last line
+ s.raw("\\e[A") # up: readline recalls the last line
s.settle(60)
line = last_line(s)
- check("and the next run recalls it with one press of up",
- "#00ff88" in line, repr(line))
+ check(
+ "and the next run recalls it with one press of up",
+ "#00ff88" in line,
+ repr(line),
+ )
os.unlink(path)
@@ -181,15 +211,18 @@ def test_tab_completes():
home = tempfile.mkdtemp()
with session(path, home) as s:
s.until_text("chrome>")
- s.raw("grays\\t") # one match: completed in place
+ s.raw("grays\\t") # one match: completed in place
s.settle(60)
out = s.snapshot().pane_text(s.pane())
- check("a unique prefix completes to the shader name",
- "grayscale" in out, repr(out[-120:]))
+ check(
+ "a unique prefix completes to the shader name",
+ "grayscale" in out,
+ repr(out[-120:]),
+ )
s.raw(" amount=40\\r")
s.until_text("ok")
- s.raw(":cl\\t") # `:c` is ambiguous with :chrome/:content; `:cl` is not
+ s.raw(":cl\\t") # `:c` is ambiguous with :chrome/:content; `:cl` is not
s.settle(60)
out = s.snapshot().pane_text(s.pane())
check("and so does a command", ":clear" in out, repr(out[-120:]))
@@ -206,24 +239,30 @@ def test_load_takes_a_preset_by_name():
pane = s.pane()
ring = lambda: tuple(
(s.snapshot().style_at(x, pane["y"]) or {}).get("fg")
- for x in range(pane["x"], pane["x"] + pane["w"]))
+ for x in range(pane["x"], pane["x"] + pane["w"])
+ )
before = {ring() for _ in range(3)}
s.raw(":load sine-comet\\r")
s.settle(60)
out = last_line(s)
- check("it says how much of the file ran",
- "1 chrome, 0 content" in "\n".join(
- l.strip() for l in
- s.snapshot().pane_text(pane).split("\n")), repr(out))
+ check(
+ "it says how much of the file ran",
+ "1 chrome, 0 content"
+ in "\n".join(l.strip() for l in s.snapshot().pane_text(pane).split("\n")),
+ repr(out),
+ )
seen = set()
deadline = time.monotonic() + 3
while len(seen) < 2 and time.monotonic() < deadline:
seen.add(ring())
s.settle(60)
- check("and the preset is running on the frame", len(seen) > 1,
- "%d rings before, %d after" % (len(before), len(seen)))
+ check(
+ "and the preset is running on the frame",
+ len(seen) > 1,
+ "%d rings before, %d after" % (len(before), len(seen)),
+ )
os.unlink(path)
@@ -234,11 +273,13 @@ def test_load_routes_a_files_entries_by_their_own_where():
home = tempfile.mkdtemp()
both = os.path.join(home, "both.kdl")
with open(both, "w") as f:
- f.write('// two rects in one file\n'
- 'shaders {\n'
- ' tint where="chrome" channel="fg" color="#ff0033" amount=255\n'
- ' tint where="content" channel="bg" color="#00ff88" amount=255\n'
- '}\n')
+ f.write(
+ "// two rects in one file\n"
+ "shaders {\n"
+ ' tint where="chrome" channel="fg" color="#ff0033" amount=255\n'
+ ' tint where="content" channel="bg" color="#00ff88" amount=255\n'
+ "}\n"
+ )
with session(path, home) as s:
s.until_text("chrome>")
pane = s.pane()
@@ -246,13 +287,20 @@ def test_load_routes_a_files_entries_by_their_own_where():
s.settle(80)
snap = s.snapshot()
text = "\n".join(l.strip() for l in snap.pane_text(pane).split("\n"))
- check("both chains are reported", "1 chrome, 1 content" in text, repr(text[-120:]))
- check("the frame took the chrome entry",
- (snap.style_at(pane["x"], pane["y"]) or {}).get("fg") == "#ff0033",
- str(snap.style_at(pane["x"], pane["y"])))
+ check(
+ "both chains are reported", "1 chrome, 1 content" in text, repr(text[-120:])
+ )
+ check(
+ "the frame took the chrome entry",
+ (snap.style_at(pane["x"], pane["y"]) or {}).get("fg") == "#ff0033",
+ str(snap.style_at(pane["x"], pane["y"])),
+ )
body = snap.style_at(pane["content_x"] + 1, pane["content_y"] + 1)
- check("and the contents took the other one",
- (body or {}).get("bg") == "#00ff88", str(body))
+ check(
+ "and the contents took the other one",
+ (body or {}).get("bg") == "#00ff88",
+ str(body),
+ )
os.unlink(path)
@@ -265,13 +313,17 @@ def test_load_says_what_is_wrong_rather_than_nothing():
with session(path, home) as s:
s.until_text("chrome>")
pane = s.pane()
- cases = [(":load nosuchthing", "no such file"),
- (":load %s" % plain, "no shaders { } block"),
- (":load", ":load <file.kdl>")]
+ cases = [
+ (":load nosuchthing", "no such file"),
+ (":load %s" % plain, "no shaders { } block"),
+ (":load", ":load <file.kdl>"),
+ ]
for line, want in cases:
s.raw(line + "\\r")
s.settle(60)
- text = "\n".join(l.strip() for l in s.snapshot().pane_text(pane).split("\n"))
+ text = "\n".join(
+ l.strip() for l in s.snapshot().pane_text(pane).split("\n")
+ )
check("`%s` says so: %s" % (line, want), want in text, repr(text[-140:]))
os.unlink(path)
@@ -290,13 +342,19 @@ def test_paste_after_a_load_is_the_file():
s.raw(":paste\\r")
s.settle(60)
text = s.snapshot().pane_text(pane)
- check("`:paste` names the file it came from", "marching-ants.kdl" in text,
- repr(text[-200:]))
+ check(
+ "`:paste` names the file it came from",
+ "marching-ants.kdl" in text,
+ repr(text[-200:]),
+ )
# An `include` line rather than the file's thirty: the config has a word
# for "that file, as written", and it fits on the pane you are looking at.
check("and offers it as an include", 'include "' in text, repr(text[-200:]))
- check("with a path the session can read",
- "/contrib/chrome/marching-ants.kdl" in text, repr(text[-200:]))
+ check(
+ "with a path the session can read",
+ "/contrib/chrome/marching-ants.kdl" in text,
+ repr(text[-200:]),
+ )
os.unlink(path)
@@ -307,8 +365,11 @@ def test_load_completes_a_preset_name():
s.until_text("chrome>")
s.raw(":load march\\t")
s.settle(60)
- check("tab after `:load` completes a preset name",
- "marching-ants" in last_line(s), repr(last_line(s)))
+ check(
+ "tab after `:load` completes a preset name",
+ "marching-ants" in last_line(s),
+ repr(last_line(s)),
+ )
os.unlink(path)
@@ -323,14 +384,16 @@ def test_the_prompt_takes_what_a_config_file_says():
s.until_text("chrome>")
pane = s.pane()
for line, want in (
- ('tint color="#ff0033" amount=200', "1 chrome, 0 content"),
- ('shaders { tint color="#ff0033" amount=200 }', "1 chrome, 0 content"),
- ('include "contrib/chrome/heartbeat.kdl"', "heartbeat.kdl"),
- ('load marching-ants', "marching-ants.kdl")):
+ ('tint color="#ff0033" amount=200', "1 chrome, 0 content"),
+ ('shaders { tint color="#ff0033" amount=200 }', "1 chrome, 0 content"),
+ ('include "contrib/chrome/heartbeat.kdl"', "heartbeat.kdl"),
+ ("load marching-ants", "marching-ants.kdl"),
+ ):
s.raw(line + "\\r")
s.settle(70)
- text = "\n".join(l.strip() for l in
- s.snapshot().pane_text(pane).split("\n"))
+ text = "\n".join(
+ l.strip() for l in s.snapshot().pane_text(pane).split("\n")
+ )
check("takes `%s`" % line[:38], want in text, repr(text[-160:]))
os.unlink(path)
@@ -344,29 +407,42 @@ def test_a_block_can_be_pasted_over_several_lines():
with session(path, home) as s:
s.until_text("chrome>")
pane = s.pane()
- for line in ('shaders {',
- ' tint where="chrome" color="#ff0033" amount=255',
- ' tint where="content" channel="bg" color="#00ff88" amount=255'):
+ for line in (
+ "shaders {",
+ ' tint where="chrome" color="#ff0033" amount=255',
+ ' tint where="content" channel="bg" color="#00ff88" amount=255',
+ ):
s.raw(line + "\\r")
s.settle(40)
- check("it waits for the closing brace", "...>" in s.snapshot().screen(),
- repr(last_line(s)))
+ check(
+ "it waits for the closing brace",
+ "...>" in s.snapshot().screen(),
+ repr(last_line(s)),
+ )
s.raw("}\\r")
s.settle(80)
snap = s.snapshot()
- check("the frame took the chrome entry",
- (snap.style_at(pane["x"], pane["y"]) or {}).get("fg") == "#ff0033",
- str(snap.style_at(pane["x"], pane["y"])))
+ check(
+ "the frame took the chrome entry",
+ (snap.style_at(pane["x"], pane["y"]) or {}).get("fg") == "#ff0033",
+ str(snap.style_at(pane["x"], pane["y"])),
+ )
body = snap.style_at(pane["content_x"] + 1, pane["content_y"] + 1)
- check("and the contents took the other one",
- (body or {}).get("bg") == "#00ff88", str(body))
+ check(
+ "and the contents took the other one",
+ (body or {}).get("bg") == "#00ff88",
+ str(body),
+ )
s.raw("shaders {\\r")
s.settle(40)
s.raw("\\r") # an empty line: give up on it
s.settle(40)
- check("an empty line drops an unfinished block",
- "(dropped)" in s.snapshot().screen(), repr(last_line(s)))
+ check(
+ "an empty line drops an unfinished block",
+ "(dropped)" in s.snapshot().screen(),
+ repr(last_line(s)),
+ )
os.unlink(path)
@@ -388,18 +464,26 @@ def test_paste_round_trips_through_the_prompt():
s.settle(60)
before = (
(s.snapshot().style_at(pane["x"], pane["y"]) or {}).get("fg"),
- (s.snapshot().style_at(pane["content_x"] + 1,
- pane["content_y"] + 1) or {}).get("fg"))
+ (
+ s.snapshot().style_at(pane["content_x"] + 1, pane["content_y"] + 1)
+ or {}
+ ).get("fg"),
+ )
s.raw(":paste\\r")
s.settle(60)
- lines = [l.strip() for l in s.snapshot().pane_text(pane).split("\n")
- if l.strip()]
- block = lines[lines.index("shaders {"):]
- block = block[:block.index("}") + 1]
- check("`:paste` printed a block with both rects in it",
- len(block) == 4 and 'where="chrome"' in block[1]
- and 'where="content"' in block[2], str(block))
+ lines = [
+ l.strip() for l in s.snapshot().pane_text(pane).split("\n") if l.strip()
+ ]
+ block = lines[lines.index("shaders {") :]
+ block = block[: block.index("}") + 1]
+ check(
+ "`:paste` printed a block with both rects in it",
+ len(block) == 4
+ and 'where="chrome"' in block[1]
+ and 'where="content"' in block[2],
+ str(block),
+ )
s.raw(":both\\r")
s.settle(60)
@@ -409,10 +493,16 @@ def test_paste_round_trips_through_the_prompt():
s.settle(80)
after = (
(s.snapshot().style_at(pane["x"], pane["y"]) or {}).get("fg"),
- (s.snapshot().style_at(pane["content_x"] + 1,
- pane["content_y"] + 1) or {}).get("fg"))
- check("typing it back gives the same pane", before == after,
- "%s -> %s" % (before, after))
+ (
+ s.snapshot().style_at(pane["content_x"] + 1, pane["content_y"] + 1)
+ or {}
+ ).get("fg"),
+ )
+ check(
+ "typing it back gives the same pane",
+ before == after,
+ "%s -> %s" % (before, after),
+ )
os.unlink(path)
@@ -427,10 +517,16 @@ def test_an_unknown_word_says_where_files_go():
s.raw("nonesuch amount=1\\r")
s.settle(60)
text = "\n".join(l.strip() for l in s.snapshot().pane_text(pane).split("\n"))
- check("it names the mistake", "unknown shader: nonesuch" in text,
- repr(text[-160:]))
- check("and points at how files are loaded", ":load <path>" in text,
- repr(text[-160:]))
+ check(
+ "it names the mistake",
+ "unknown shader: nonesuch" in text,
+ repr(text[-160:]),
+ )
+ check(
+ "and points at how files are loaded",
+ ":load <path>" in text,
+ repr(text[-160:]),
+ )
os.unlink(path)
diff --git a/tests/test_shaders.py b/tests/test_shaders.py
index 214d4d7..7afd643 100644
--- a/tests/test_shaders.py
+++ b/tests/test_shaders.py
@@ -10,6 +10,7 @@ Colours are asserted through the style runs the snapshot already reports, so
these are real end-to-end checks: a program's output, composed, shaded, and
read back the way a client would see it.
"""
+
import sys
import tempfile
@@ -17,9 +18,12 @@ from harness import Session, check, report
# A pane that paints a known, non-default colour into its own content.
GREEN = "#00ff00"
-SH = ["/bin/sh", "-c",
- 'printf "\\033]2;p\\007"; printf "\\033[38;2;0;255;0mBLOCK\\033[0m\\n"; '
- 'stty raw -echo; cat']
+SH = [
+ "/bin/sh",
+ "-c",
+ 'printf "\\033]2;p\\007"; printf "\\033[38;2;0;255;0mBLOCK\\033[0m\\n"; '
+ "stty raw -echo; cat",
+]
def cfg(text):
@@ -36,7 +40,9 @@ def unfocused_cfg(amount):
def drag_cfg(chain):
"""Both drop states get the same chain, which is what the defaults do."""
return "states {\n drop_target { %s }\n drop_hover { %s }\n}\n" % (
- chain, chain)
+ chain,
+ chain,
+ )
def content_fg(snap, pane):
@@ -62,8 +68,9 @@ def check_at_rest(s, label):
for p in s.panes():
fg = content_fg(snap, p)
ok = fg == GREEN if p["focused"] else fg not in (GREEN, None)
- check(f"{label}: pane {p['id']} is at rest",
- ok, f"{fg} (focused={p['focused']})")
+ check(
+ f"{label}: pane {p['id']} is at rest", ok, f"{fg} (focused={p['focused']})"
+ )
def split(s):
@@ -93,6 +100,7 @@ def release(s, x, y):
# ---- the unfocused state ---------------------------------------------------
+
def test_the_default_dims_the_pane_you_are_not_in():
"""`dim_unfocused` writes the unfocused chain when a config has not, so out
of the box the pane you left is pushed back and the one you are in is not.
@@ -102,12 +110,16 @@ def test_the_default_dims_the_pane_you_are_not_in():
split(s)
focused, other = by_focus(s)
snap = s.snapshot()
- check("the unfocused pane is dimmed",
- content_fg(snap, other) not in (GREEN, None),
- str(content_fg(snap, other)))
- check("and the focused one is left alone",
- content_fg(snap, focused) == GREEN,
- str(content_fg(snap, focused)))
+ check(
+ "the unfocused pane is dimmed",
+ content_fg(snap, other) not in (GREEN, None),
+ str(content_fg(snap, other)),
+ )
+ check(
+ "and the focused one is left alone",
+ content_fg(snap, focused) == GREEN,
+ str(content_fg(snap, focused)),
+ )
def test_nothing_is_dimmed_with_the_knob_at_zero():
@@ -116,10 +128,11 @@ def test_nothing_is_dimmed_with_the_knob_at_zero():
split(s)
focused, other = by_focus(s)
snap = s.snapshot()
- check("neither pane is touched",
- content_fg(snap, other) == GREEN and
- content_fg(snap, focused) == GREEN,
- f"{content_fg(snap, other)} {content_fg(snap, focused)}")
+ check(
+ "neither pane is touched",
+ content_fg(snap, other) == GREEN and content_fg(snap, focused) == GREEN,
+ f"{content_fg(snap, other)} {content_fg(snap, focused)}",
+ )
def test_dims_everything_but_the_focused_pane():
@@ -128,15 +141,21 @@ def test_dims_everything_but_the_focused_pane():
split(s)
focused, other = by_focus(s)
snap = s.snapshot()
- check("the focused pane keeps its colour",
- content_fg(snap, focused) == GREEN,
- str(content_fg(snap, focused)))
- check("an unfocused pane is dimmed",
- content_fg(snap, other) not in (GREEN, None),
- str(content_fg(snap, other)))
- check("dimmed means darker, not merely different",
- int(content_fg(snap, other)[3:5], 16) < 0xff,
- str(content_fg(snap, other)))
+ check(
+ "the focused pane keeps its colour",
+ content_fg(snap, focused) == GREEN,
+ str(content_fg(snap, focused)),
+ )
+ check(
+ "an unfocused pane is dimmed",
+ content_fg(snap, other) not in (GREEN, None),
+ str(content_fg(snap, other)),
+ )
+ check(
+ "dimmed means darker, not merely different",
+ int(content_fg(snap, other)[3:5], 16) < 0xFF,
+ str(content_fg(snap, other)),
+ )
def test_the_dimming_follows_focus():
@@ -149,12 +168,16 @@ def test_the_dimming_follows_focus():
snap = s.snapshot()
now_focused = [p for p in s.panes() if p["id"] == first_other["id"]][0]
now_other = [p for p in s.panes() if p["id"] == first_focused["id"]][0]
- check("the newly focused pane is undimmed",
- content_fg(snap, now_focused) == GREEN,
- str(content_fg(snap, now_focused)))
- check("and the one that lost focus is dimmed now",
- content_fg(snap, now_other) not in (GREEN, None),
- str(content_fg(snap, now_other)))
+ check(
+ "the newly focused pane is undimmed",
+ content_fg(snap, now_focused) == GREEN,
+ str(content_fg(snap, now_focused)),
+ )
+ check(
+ "and the one that lost focus is dimmed now",
+ content_fg(snap, now_other) not in (GREEN, None),
+ str(content_fg(snap, now_other)),
+ )
def test_dimming_never_touches_the_chrome():
@@ -163,19 +186,25 @@ def test_dimming_never_touches_the_chrome():
split(s)
_, other = by_focus(s)
snap = s.snapshot()
- check("a dimmed pane's frame keeps its full colour",
- frame_fg(snap, other) == "#45454a", str(frame_fg(snap, other)))
+ check(
+ "a dimmed pane's frame keeps its full colour",
+ frame_fg(snap, other) == "#45454a",
+ str(frame_fg(snap, other)),
+ )
# Find where the strip actually starts rather than assuming a column:
# status_pad decides that, and a test should not have an opinion on it.
strip = snap.line(1)
first = len(strip) - len(strip.lstrip())
- check("and the tab strip is untouched",
- "#" in str(snap.style_at(first, 1)),
- f"col {first}: " + str(snap.style_at(first, 1)))
+ check(
+ "and the tab strip is untouched",
+ "#" in str(snap.style_at(first, 1)),
+ f"col {first}: " + str(snap.style_at(first, 1)),
+ )
# ---- the drag states -------------------------------------------------------
+
def test_dragging_greys_the_other_panes():
with Session(SH, cols=90, rows=16) as s:
s.until_text("BLOCK")
@@ -184,28 +213,38 @@ def test_dragging_greys_the_other_panes():
press(s, left["x"] + 4, left["y"])
motion(s, right["x"] + 5, right["y"] + 3)
snap = s.snapshot()
- check("the pane being dragged keeps its colour",
- content_fg(snap, left) == GREEN, str(content_fg(snap, left)))
- check("every other pane goes grey",
- content_fg(snap, right) not in (GREEN, None),
- str(content_fg(snap, right)))
+ check(
+ "the pane being dragged keeps its colour",
+ content_fg(snap, left) == GREEN,
+ str(content_fg(snap, left)),
+ )
+ check(
+ "every other pane goes grey",
+ content_fg(snap, right) not in (GREEN, None),
+ str(content_fg(snap, right)),
+ )
# The default strength is 200, not 255: a partial desaturation that
# keeps a trace of the original hue. So the test is that the channels
# have converged, not that they are equal.
grey = content_fg(snap, right)
- ch = [int(grey[i:i + 2], 16) for i in (1, 3, 5)]
- check("the colour has collapsed towards grey",
- max(ch) - min(ch) < 80, f"{grey} spread={max(ch) - min(ch)}")
- check("the two channels that started equal stay equal",
- ch[0] == ch[2], str(grey))
+ ch = [int(grey[i : i + 2], 16) for i in (1, 3, 5)]
+ check(
+ "the colour has collapsed towards grey",
+ max(ch) - min(ch) < 80,
+ f"{grey} spread={max(ch) - min(ch)}",
+ )
+ check(
+ "the two channels that started equal stay equal", ch[0] == ch[2], str(grey)
+ )
release(s, right["x"] + 5, right["y"] + 3)
check_at_rest(s, "after the drop")
def test_full_strength_is_a_true_grey():
- with Session(SH, cols=90, rows=16,
- config=cfg(drag_cfg("grayscale amount=255"))) as s:
+ with Session(
+ SH, cols=90, rows=16, config=cfg(drag_cfg("grayscale amount=255"))
+ ) as s:
s.until_text("BLOCK")
left, right = split(s)
@@ -213,8 +252,11 @@ def test_full_strength_is_a_true_grey():
motion(s, right["x"] + 5, right["y"] + 3)
s.settle(120)
grey = content_fg(s.snapshot(), right)
- check("at 255 every channel agrees exactly",
- grey and grey[1:3] == grey[3:5] == grey[5:7], str(grey))
+ check(
+ "at 255 every channel agrees exactly",
+ grey and grey[1:3] == grey[3:5] == grey[5:7],
+ str(grey),
+ )
release(s, right["x"] + 5, right["y"] + 3)
s.settle(120)
@@ -242,8 +284,11 @@ def test_the_drag_greying_outranks_the_focus_dimming():
press(s, other["x"] + 4, other["y"])
motion(s, focused["x"] + 5, focused["y"] + 3)
snap = s.snapshot()
- check("the dragged pane lifts off the page, dimming or not",
- content_fg(snap, other) == GREEN, str(content_fg(snap, other)))
+ check(
+ "the dragged pane lifts off the page, dimming or not",
+ content_fg(snap, other) == GREEN,
+ str(content_fg(snap, other)),
+ )
release(s, focused["x"] + 5, focused["y"] + 3)
s.settle(200)
@@ -263,30 +308,41 @@ def drag_and_sample(conf):
def test_the_two_drag_knobs_are_independent():
both_off = drag_and_sample(drag_cfg(""))
- check("with both off a drag leaves every pane alone", both_off == GREEN,
- str(both_off))
+ check(
+ "with both off a drag leaves every pane alone", both_off == GREEN, str(both_off)
+ )
# Desaturate only: channels converge, brightness roughly survives.
gray_only = drag_and_sample(drag_cfg("grayscale amount=255"))
- ch = [int(gray_only[i:i + 2], 16) for i in (1, 3, 5)]
- check("grayscale alone desaturates without darkening much",
- ch[0] == ch[1] == ch[2] and ch[1] > 0x60, str(gray_only))
+ ch = [int(gray_only[i : i + 2], 16) for i in (1, 3, 5)]
+ check(
+ "grayscale alone desaturates without darkening much",
+ ch[0] == ch[1] == ch[2] and ch[1] > 0x60,
+ str(gray_only),
+ )
# Darken only: the hue survives, the brightness does not.
dim_only = drag_and_sample(drag_cfg("dim amount=200"))
- ch = [int(dim_only[i:i + 2], 16) for i in (1, 3, 5)]
- check("dim alone darkens without desaturating",
- ch[0] == 0 and ch[2] == 0 and 0 < ch[1] < 0x60, str(dim_only))
+ ch = [int(dim_only[i : i + 2], 16) for i in (1, 3, 5)]
+ check(
+ "dim alone darkens without desaturating",
+ ch[0] == 0 and ch[2] == 0 and 0 < ch[1] < 0x60,
+ str(dim_only),
+ )
def test_the_default_drag_look_is_grey_and_recessed():
"""Both together: the dragged pane has to win on contrast, not just hue."""
got = drag_and_sample("")
- ch = [int(got[i:i + 2], 16) for i in (1, 3, 5)]
- check("a non-dragged pane is pushed well below full brightness",
- max(ch) < 0x80, f"{got} max={max(ch):#x}")
- check("and towards grey", max(ch) - min(ch) < 60,
- f"{got} spread={max(ch) - min(ch)}")
+ ch = [int(got[i : i + 2], 16) for i in (1, 3, 5)]
+ check(
+ "a non-dragged pane is pushed well below full brightness",
+ max(ch) < 0x80,
+ f"{got} max={max(ch):#x}",
+ )
+ check(
+ "and towards grey", max(ch) - min(ch) < 60, f"{got} spread={max(ch) - min(ch)}"
+ )
# ---- the drop-target border (chrome, not a shader) -------------------------
@@ -301,25 +357,33 @@ def test_non_dragged_panes_get_a_dashed_border():
left, right = split(s)
snap = s.snapshot()
- check("borders are solid when nothing is being dragged",
- DASH_H not in snap.line(right["y"]), repr(snap.line(right["y"])))
+ check(
+ "borders are solid when nothing is being dragged",
+ DASH_H not in snap.line(right["y"]),
+ repr(snap.line(right["y"])),
+ )
press(s, left["x"] + 4, left["y"])
motion(s, right["x"] + 5, right["y"] + 3)
snap = s.snapshot()
- top = snap.line(right["y"])[right["x"]:right["x"] + right["w"]]
- dragged_top = snap.line(left["y"])[left["x"]:left["x"] + left["w"]]
+ top = snap.line(right["y"])[right["x"] : right["x"] + right["w"]]
+ dragged_top = snap.line(left["y"])[left["x"] : left["x"] + left["w"]]
check("a pane you could drop onto is dashed", DASH_H in top, repr(top))
- check("the pane in your hand stays solid",
- DASH_H not in dragged_top and SOLID_H in dragged_top,
- repr(dragged_top))
+ check(
+ "the pane in your hand stays solid",
+ DASH_H not in dragged_top and SOLID_H in dragged_top,
+ repr(dragged_top),
+ )
release(s, right["x"] + 5, right["y"] + 3)
snap = s.snapshot()
for p in s.panes():
- row = snap.line(p["y"])[p["x"]:p["x"] + p["w"]]
- check(f"pane {p['id']} is solid again after the drop",
- DASH_H not in row, repr(row))
+ row = snap.line(p["y"])[p["x"] : p["x"] + p["w"]]
+ check(
+ f"pane {p['id']} is solid again after the drop",
+ DASH_H not in row,
+ repr(row),
+ )
def test_a_press_that_never_moves_dashes_nothing():
@@ -328,8 +392,7 @@ def test_a_press_that_never_moves_dashes_nothing():
left, right = split(s)
press(s, left["x"] + 4, left["y"])
snap = s.snapshot()
- check("pressing without moving dashes nothing",
- DASH_H not in snap.screen(), "")
+ check("pressing without moving dashes nothing", DASH_H not in snap.screen(), "")
release(s, left["x"] + 4, left["y"])
s.settle(120)
@@ -342,9 +405,11 @@ def test_the_hovered_target_is_still_highlighted_over_the_dashes():
motion(s, right["x"] + 5, right["y"] + 3)
snap = s.snapshot()
run = snap.style_at(right["x"], right["y"])
- check("the pane under the pointer keeps its drop highlight",
- run and run["fg"] == "#ff5fd7" and "bold" in run["attrs"],
- str(run))
+ check(
+ "the pane under the pointer keeps its drop highlight",
+ run and run["fg"] == "#ff5fd7" and "bold" in run["attrs"],
+ str(run),
+ )
release(s, right["x"] + 5, right["y"] + 3)
s.settle(120)
@@ -358,7 +423,7 @@ def test_a_drag_that_ends_off_a_pane_still_clears():
press(s, left["x"] + 4, left["y"])
motion(s, right["x"] + 5, right["y"] + 3)
s.settle(120)
- motion(s, 0, 0) # out into the margin
+ motion(s, 0, 0) # out into the margin
release(s, 0, 0)
check_at_rest(s, "a drop into nowhere")
@@ -378,6 +443,7 @@ def test_a_keystroke_mid_drag_clears_the_greying():
# ---- the config block ------------------------------------------------------
+
def test_config_shaders_reach_the_screen():
"""`shaders { ... }` is the surface these are actually driven from."""
conf = 'shaders {\n ruler amount=255 at=4 color="#ff0000"\n}\n'
@@ -387,47 +453,64 @@ def test_config_shaders_reach_the_screen():
snap = s.snapshot()
on = snap.style_at(p["content_x"] + 4, p["content_y"])
off = snap.style_at(p["content_x"] + 3, p["content_y"])
- check("a configured ruler marks its column",
- (on or {}).get("bg") == "#ff0000", str(on))
- check("and leaves the neighbouring column alone",
- (off or {}).get("bg") != "#ff0000", str(off))
+ check(
+ "a configured ruler marks its column",
+ (on or {}).get("bg") == "#ff0000",
+ str(on),
+ )
+ check(
+ "and leaves the neighbouring column alone",
+ (off or {}).get("bg") != "#ff0000",
+ str(off),
+ )
def test_config_shaders_run_in_written_order():
"""Order in the block is order in the chain."""
a = 'shaders {\n tint amount=255 color="#ff0000"\n tint amount=255 color="#0000ff"\n}\n'
b = 'shaders {\n tint amount=255 color="#0000ff"\n tint amount=255 color="#ff0000"\n}\n'
+
def last(conf):
with Session(SH, cols=50, rows=10, config=cfg(conf)) as s:
s.settle(200)
p = s.pane()
return content_fg(s.snapshot(), p)
- check("the last shader written is the last one applied",
- last(a) == "#0000ff" and last(b) == "#ff0000",
- f"{last(a)} / {last(b)}")
+
+ check(
+ "the last shader written is the last one applied",
+ last(a) == "#0000ff" and last(b) == "#ff0000",
+ f"{last(a)} / {last(b)}",
+ )
def test_an_unknown_shader_is_refused_not_guessed():
- with Session(SH, cols=50, rows=10,
- config=cfg('shaders {\n bloom amount=255\n}\n')) as s:
+ with Session(
+ SH, cols=50, rows=10, config=cfg("shaders {\n bloom amount=255\n}\n")
+ ) as s:
s.until_text("BLOCK")
- check("an unknown shader leaves the session running and unshaded",
- s.alive() and content_fg(s.snapshot(), s.pane()) == GREEN,
- str(content_fg(s.snapshot(), s.pane())))
+ check(
+ "an unknown shader leaves the session running and unshaded",
+ s.alive() and content_fg(s.snapshot(), s.pane()) == GREEN,
+ str(content_fg(s.snapshot(), s.pane())),
+ )
def test_positional_shaders_stay_off_the_chrome():
- conf = 'shaders {\n vignette amount=255\n zebra amount=255 band=1\n}\n'
+ conf = "shaders {\n vignette amount=255\n zebra amount=255 band=1\n}\n"
with Session(SH, cols=60, rows=12, config=cfg(conf)) as s:
s.until_text("BLOCK")
p = s.pane()
snap = s.snapshot()
- check("a full-strength positional stack still leaves the frame alone",
- frame_fg(snap, p) == "#ff5fd7", str(frame_fg(snap, p)))
+ check(
+ "a full-strength positional stack still leaves the frame alone",
+ frame_fg(snap, p) == "#ff5fd7",
+ str(frame_fg(snap, p)),
+ )
# ---- states as a table -----------------------------------------------------
+
def lay(text):
f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
f.write(text)
@@ -443,17 +526,22 @@ def test_the_dragged_pane_can_have_a_state_of_its_own():
press(s, left["x"] + 4, left["y"])
motion(s, right["x"] + 5, right["y"] + 3)
snap = s.snapshot()
- check("the pane in your hand takes the dragging state",
- content_fg(snap, left) == "#0000ff", str(content_fg(snap, left)))
+ check(
+ "the pane in your hand takes the dragging state",
+ content_fg(snap, left) == "#0000ff",
+ str(content_fg(snap, left)),
+ )
release(s, right["x"] + 5, right["y"] + 3)
check_at_rest(s, "after the drop")
def test_the_hovered_pane_is_a_different_state_from_the_rest():
- conf = ('states {\n'
- ' drop_hover { tint amount=255 color="#ff0000" }\n'
- ' drop_target { tint amount=255 color="#0000ff" }\n'
- '}\n')
+ conf = (
+ "states {\n"
+ ' drop_hover { tint amount=255 color="#ff0000" }\n'
+ ' drop_target { tint amount=255 color="#0000ff" }\n'
+ "}\n"
+ )
with Session(SH, cols=120, rows=20, config=cfg(conf)) as s:
s.settle(200)
s.key("\\\\")
@@ -468,18 +556,26 @@ def test_the_hovered_pane_is_a_different_state_from_the_rest():
press(s, a["x"] + 4, a["y"])
motion(s, b["x"] + 3, b["y"] + 3)
snap = s.snapshot()
- check("the pane under the pointer is the hover state",
- content_fg(snap, b) == "#ff0000", str(content_fg(snap, b)))
- check("the other candidates are the plain target state",
- content_fg(snap, c) == "#0000ff", str(content_fg(snap, c)))
+ check(
+ "the pane under the pointer is the hover state",
+ content_fg(snap, b) == "#ff0000",
+ str(content_fg(snap, b)),
+ )
+ check(
+ "the other candidates are the plain target state",
+ content_fg(snap, c) == "#0000ff",
+ str(content_fg(snap, c)),
+ )
release(s, b["x"] + 3, b["y"] + 3)
s.settle(150)
def test_a_suspended_pane_is_a_state():
"""It draws a label rather than a program, and that is still contents."""
- l = lay('layout {\n tab name="t" {\n pane\n'
- ' pane suspended=true command="sleep 1"\n }\n}\n')
+ l = lay(
+ 'layout {\n tab name="t" {\n pane\n'
+ ' pane suspended=true command="sleep 1"\n }\n}\n'
+ )
conf = 'states {\n suspended { tint amount=255 color="#00aaff" }\n}\n'
with Session(SH, cols=90, rows=14, config=cfg(conf), layout=l) as s:
s.settle(250)
@@ -489,62 +585,85 @@ def test_a_suspended_pane_is_a_state():
if not susp:
return
snap = s.snapshot()
- run = snap.style_at(susp[0]["content_x"] + susp[0]["content_w"] // 2,
- susp[0]["content_y"] + susp[0]["content_h"] // 2)
- check("a suspended pane reaches the shader pass at all",
- (run or {}).get("fg") == "#00aaff", str(run))
+ run = snap.style_at(
+ susp[0]["content_x"] + susp[0]["content_w"] // 2,
+ susp[0]["content_y"] + susp[0]["content_h"] // 2,
+ )
+ check(
+ "a suspended pane reaches the shader pass at all",
+ (run or {}).get("fg") == "#00aaff",
+ str(run),
+ )
def test_scrollback_is_a_state():
- noisy = ["/bin/sh", "-c",
- 'printf "\\033]2;p\\007"; i=0; while [ $i -lt 200 ]; do '
- 'printf "\\033[38;2;0;255;0mline %d\\033[0m\\n" $i; i=$((i+1)); done; '
- 'stty raw -echo; cat']
+ noisy = [
+ "/bin/sh",
+ "-c",
+ 'printf "\\033]2;p\\007"; i=0; while [ $i -lt 200 ]; do '
+ 'printf "\\033[38;2;0;255;0mline %d\\033[0m\\n" $i; i=$((i+1)); done; '
+ "stty raw -echo; cat",
+ ]
conf = 'states {\n scrolled { tint amount=255 color="#ffaa00" }\n}\n'
with Session(noisy, cols=80, rows=14, config=cfg(conf)) as s:
s.settle(300)
p = s.pane()
- check("not scrolled yet, so no state colour",
- content_fg(s.snapshot(), p) == GREEN,
- str(content_fg(s.snapshot(), p)))
+ check(
+ "not scrolled yet, so no state colour",
+ content_fg(s.snapshot(), p) == GREEN,
+ str(content_fg(s.snapshot(), p)),
+ )
# No scroll command exists, so drive it the way a person does: the
# wheel. SGR button 64 is wheel-up.
for _ in range(6):
s.send(rf"\e[<64;{p['content_x'] + 2};{p['content_y'] + 2}M")
- check("looking at the past is a state you can colour",
- content_fg(s.snapshot(), p) == "#ffaa00",
- str(content_fg(s.snapshot(), p)))
+ check(
+ "looking at the past is a state you can colour",
+ content_fg(s.snapshot(), p) == "#ffaa00",
+ str(content_fg(s.snapshot(), p)),
+ )
def test_exactly_one_state_wins():
"""A pane is in several at once; the ranking decides, and does not stack."""
- l = lay('layout {\n tab name="t" {\n pane\n'
- ' pane suspended=true command="sleep 1"\n }\n}\n')
- conf = ('states {\n'
- ' suspended { tint amount=255 color="#00aaff" }\n'
- ' unfocused { tint amount=255 color="#ff0000" }\n'
- '}\n')
+ l = lay(
+ 'layout {\n tab name="t" {\n pane\n'
+ ' pane suspended=true command="sleep 1"\n }\n}\n'
+ )
+ conf = (
+ "states {\n"
+ ' suspended { tint amount=255 color="#00aaff" }\n'
+ ' unfocused { tint amount=255 color="#ff0000" }\n'
+ "}\n"
+ )
with Session(SH, cols=90, rows=14, config=cfg(conf), layout=l) as s:
s.settle(250)
susp = [p for p in s.panes() if p["suspended"]]
if not susp or susp[0]["focused"]:
- check("a suspended, unfocused pane to test with", bool(susp),
- str(s.panes()))
+ check(
+ "a suspended, unfocused pane to test with", bool(susp), str(s.panes())
+ )
return
run = s.snapshot().style_at(
susp[0]["content_x"] + susp[0]["content_w"] // 2,
- susp[0]["content_y"] + susp[0]["content_h"] // 2)
- check("suspended outranks unfocused, and they do not mix",
- (run or {}).get("fg") == "#00aaff", str(run))
+ susp[0]["content_y"] + susp[0]["content_h"] // 2,
+ )
+ check(
+ "suspended outranks unfocused, and they do not mix",
+ (run or {}).get("fg") == "#00aaff",
+ str(run),
+ )
def test_an_unknown_state_is_refused():
- conf = 'states {\n haunted { dim amount=255 }\n}\n'
+ conf = "states {\n haunted { dim amount=255 }\n}\n"
with Session(SH, cols=60, rows=12, config=cfg(conf)) as s:
s.until_text("BLOCK")
- check("an unknown state name leaves the session running",
- s.alive() and content_fg(s.snapshot(), s.pane()) == GREEN,
- str(content_fg(s.snapshot(), s.pane())))
+ check(
+ "an unknown state name leaves the session running",
+ s.alive() and content_fg(s.snapshot(), s.pane()) == GREEN,
+ str(content_fg(s.snapshot(), s.pane())),
+ )
if __name__ == "__main__":
diff --git a/tests/test_shadertoy.py b/tests/test_shadertoy.py
index 99e724a..90937d9 100644
--- a/tests/test_shadertoy.py
+++ b/tests/test_shadertoy.py
@@ -13,6 +13,7 @@ if someone edits the page, this tests what the page actually contains.
Skipped, loudly, when node is missing: it is a checker for a contrib toy, not
a reason nobody can run the suite.
"""
+
import json
import os
import random
@@ -29,25 +30,64 @@ SCRIPT = open(HTML).read().split("<script>")[1].split("</script>")[0]
# Environments to evaluate at: corners, the middle, and a moved cursor.
ENVS = [
- dict(x=0, y=0, cols=80, rows=24, curx=0, cury=0, cursor=1, focused=1, t=0, since=0),
- dict(x=13, y=7, cols=80, rows=24, curx=40, cury=12, cursor=1, focused=1, t=1234, since=120),
- dict(x=79, y=23, cols=80, rows=24, curx=3, cury=21, cursor=0, focused=0, t=99999, since=8000),
- dict(x=40, y=12, cols=100, rows=40, curx=99, cury=39, cursor=1, focused=1, t=7, since=250),
+ dict(x=0, y=0, cols=80, rows=24, curx=0, cury=0, cursor=1, focused=1, t=0, since=0),
+ dict(
+ x=13,
+ y=7,
+ cols=80,
+ rows=24,
+ curx=40,
+ cury=12,
+ cursor=1,
+ focused=1,
+ t=1234,
+ since=120,
+ ),
+ dict(
+ x=79,
+ y=23,
+ cols=80,
+ rows=24,
+ curx=3,
+ cury=21,
+ cursor=0,
+ focused=0,
+ t=99999,
+ since=8000,
+ ),
+ dict(
+ x=40,
+ y=12,
+ cols=100,
+ rows=40,
+ curx=99,
+ cury=39,
+ cursor=1,
+ focused=1,
+ t=7,
+ since=250,
+ ),
]
+
def page_presets():
"""Every preset the page ships, lifted out of the page.
Read rather than listed, so a preset added later is checked without anyone
remembering to add it here -- which nobody would."""
- prefix = SCRIPT[:SCRIPT.index("/* ---- the chain UI")]
- out = subprocess.run(["node", "-e", prefix + "\nconsole.log(JSON.stringify(PRESETS));"],
- capture_output=True, text=True)
+ prefix = SCRIPT[: SCRIPT.index("/* ---- the chain UI")]
+ out = subprocess.run(
+ ["node", "-e", prefix + "\nconsole.log(JSON.stringify(PRESETS));"],
+ capture_output=True,
+ text=True,
+ )
groups = json.loads(out.stdout)
- return [(f"{group}/{name}", i, p["amount"])
- for group, entries in groups.items()
- for name, chain in entries.items()
- for i, p in enumerate(chain)]
+ return [
+ (f"{group}/{name}", i, p["amount"])
+ for group, entries in groups.items()
+ for name, chain in entries.items()
+ for i, p in enumerate(chain)
+ ]
EXPRS = [
@@ -83,8 +123,8 @@ EXPRS = [
"0 ? 1 / 0 : 7",
"t % 255",
"since",
- "(since < 250) * 255", # a flash
- "abs(since / 8 % 510 - 255)", # ...and a breathe
+ "(since < 250) * 255", # a flash
+ "abs(since / 8 % 510 - 255)", # ...and a breathe
"(x + y) % 7 * 30",
"cols - x",
"rows * 2 - y",
@@ -115,18 +155,40 @@ EXPRS = [
"t >> 6 & 15",
"(x * 7 ^ y * 13) & 31",
# trig: degrees in, -255..255 out, from a table both sides share
- "sin(0)", "sin(30)", "sin(45)", "sin(90)", "sin(180)", "sin(270)",
- "sin(359)", "sin(360)", "sin(450)", "sin(0 - 30)", "sin(0 - 400)",
- "cos(0)", "cos(90)", "cos(180)", "cos(270)", "cos(0 - 90)",
- "sin(x * 40)", "cos(y * 37)", "sin(t / 4)", "sin(t) + cos(t)",
+ "sin(0)",
+ "sin(30)",
+ "sin(45)",
+ "sin(90)",
+ "sin(180)",
+ "sin(270)",
+ "sin(359)",
+ "sin(360)",
+ "sin(450)",
+ "sin(0 - 30)",
+ "sin(0 - 400)",
+ "cos(0)",
+ "cos(90)",
+ "cos(180)",
+ "cos(270)",
+ "cos(0 - 90)",
+ "sin(x * 40)",
+ "cos(y * 37)",
+ "sin(t / 4)",
+ "sin(t) + cos(t)",
"128 + sin(t / 4) / 2",
"abs(sin(x * 9 + t / 6))",
"clamp(sin(t / 5) + 128, 0, 255)",
"sin(sin(t / 3))",
# pi as an angle: a half turn, in the degrees this language counts in
- "PI", "TAU", "PI * 2 == TAU",
- "sin(PI)", "sin(PI / 2)", "sin(PI / 6)", "cos(TAU)",
- "sin(TAU * x / cols)", "sin(TAU * y / rows + t / 5)",
+ "PI",
+ "TAU",
+ "PI * 2 == TAU",
+ "sin(PI)",
+ "sin(PI / 2)",
+ "sin(PI / 6)",
+ "cos(TAU)",
+ "sin(TAU * x / cols)",
+ "sin(TAU * y / rows + t / 5)",
"128 + sin(TAU * t / 4000) / 2",
]
@@ -134,25 +196,41 @@ EXPRS = [
def rand_exprs(n, seed=20260814):
"""A few generated ones too, so the checks are not only what I thought of."""
rng = random.Random(seed)
- atoms = ["x", "y", "cols", "rows", "curx", "cury", "t", "since", "3", "7",
- "40", "255"]
+ atoms = [
+ "x",
+ "y",
+ "cols",
+ "rows",
+ "curx",
+ "cury",
+ "t",
+ "since",
+ "3",
+ "7",
+ "40",
+ "255",
+ ]
out = []
for _ in range(n):
a, b, c = (rng.choice(atoms) for _ in range(3))
- out.append(rng.choice([
- f"({a} {rng.choice(['+', '-', '*', '/', '%', '&', '|', '^'])} {b}) * 2",
- f"{a} {rng.choice(['<<', '>>'])} ({b} % 6)",
- f"({a} ^ {b}) & {c}",
- f"sin({a} * 3 + {b})",
- f"cos({a} - {b})",
- f"128 + sin({a} * {rng.choice(['2', '5', '11'])}) / 2",
- f"clamp({a} * {rng.choice([2, 3, 9])} - {b}, 0, 255)",
- f"({a} {rng.choice(['<', '>', '==', '<=', '!='])} {b}) * {c}",
- f"min({a}, {b}) + max({b}, {c})",
- f"{a} ? {b} : {c}",
- f"dist({a}, {b}, curx, cury) * 4",
- f"abs({a} - {b}) % ({c} + 1)",
- ]))
+ out.append(
+ rng.choice(
+ [
+ f"({a} {rng.choice(['+', '-', '*', '/', '%', '&', '|', '^'])} {b}) * 2",
+ f"{a} {rng.choice(['<<', '>>'])} ({b} % 6)",
+ f"({a} ^ {b}) & {c}",
+ f"sin({a} * 3 + {b})",
+ f"cos({a} - {b})",
+ f"128 + sin({a} * {rng.choice(['2', '5', '11'])}) / 2",
+ f"clamp({a} * {rng.choice([2, 3, 9])} - {b}, 0, 255)",
+ f"({a} {rng.choice(['<', '>', '==', '<=', '!='])} {b}) * {c}",
+ f"min({a}, {b}) + max({b}, {c})",
+ f"{a} ? {b} : {c}",
+ f"dist({a}, {b}, curx, cury) * 4",
+ f"abs({a} - {b}) % ({c} + 1)",
+ ]
+ )
+ )
return out
@@ -160,12 +238,25 @@ def c_values(pairs):
"""Every (env, expr) through the compiler sl0ppty actually uses."""
lines = []
for env, expr in pairs:
- vals = " ".join(str(env[k]) for k in
- ("x", "y", "cols", "rows", "curx", "cury", "cursor",
- "focused", "t", "since"))
+ vals = " ".join(
+ str(env[k])
+ for k in (
+ "x",
+ "y",
+ "cols",
+ "rows",
+ "curx",
+ "cury",
+ "cursor",
+ "focused",
+ "t",
+ "since",
+ )
+ )
lines.append(f"{vals}\t{expr}")
- out = subprocess.run([EVAL], input="\n".join(lines) + "\n",
- capture_output=True, text=True)
+ out = subprocess.run(
+ [EVAL], input="\n".join(lines) + "\n", capture_output=True, text=True
+ )
return out.stdout.strip().split("\n")
@@ -206,6 +297,7 @@ def main():
pairs = [(env, e) for e in exprs for env in ENVS]
import tempfile
+
cases = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False)
json.dump([[env, e] for env, e in pairs], cases)
cases.close()
@@ -213,8 +305,9 @@ def main():
harness.write(JS_HARNESS)
harness.close()
- js = subprocess.run(["node", harness.name, HTML, cases.name],
- capture_output=True, text=True)
+ js = subprocess.run(
+ ["node", harness.name, HTML, cases.name], capture_output=True, text=True
+ )
os.unlink(cases.name)
os.unlink(harness.name)
if js.returncode != 0:
@@ -224,32 +317,52 @@ def main():
got = js.stdout.strip().split("\n")
want = c_values(pairs)
- check("both sides answered every case",
- len(got) == len(pairs) and len(want) == len(pairs),
- f"js {len(got)}, c {len(want)}, cases {len(pairs)}")
+ check(
+ "both sides answered every case",
+ len(got) == len(pairs) and len(want) == len(pairs),
+ f"js {len(got)}, c {len(want)}, cases {len(pairs)}",
+ )
bad = [(e, env, w, g) for (env, e), w, g in zip(pairs, want, got) if w != g]
- check(f"the toy agrees with the compiler on all {len(pairs)} evaluations",
- not bad,
- "\n".join(f" {e!r} at {env} -> C {w}, JS {g}" for e, env, w, g in bad[:8]))
+ check(
+ f"the toy agrees with the compiler on all {len(pairs)} evaluations",
+ not bad,
+ "\n".join(f" {e!r} at {env} -> C {w}, JS {g}" for e, env, w, g in bad[:8]),
+ )
# A preset that computes 0 everywhere is a preset that does nothing, which
# is a broken example rather than a subtle one: it looks like the shader
# system is not working. Checked over a whole pane and four points in time,
# cell by cell -- sampling every other row made three `y % 2` effects look
# dead when they were fine.
- grid = [dict(x=x, y=y, cols=64, rows=24, curx=30, cury=12, cursor=1,
- focused=1, t=t, since=t)
- for t in (0, 500, 1500, 4000) for y in range(24) for x in range(64)]
+ grid = [
+ dict(
+ x=x,
+ y=y,
+ cols=64,
+ rows=24,
+ curx=30,
+ cury=12,
+ cursor=1,
+ focused=1,
+ t=t,
+ since=t,
+ )
+ for t in (0, 500, 1500, 4000)
+ for y in range(24)
+ for x in range(64)
+ ]
flat = [(env, e) for _, _, e in presets for env in grid]
vals = c_values(flat)
per = len(grid)
for idx, (label, i, expr) in enumerate(presets):
- window = vals[idx * per:(idx + 1) * per]
+ window = vals[idx * per : (idx + 1) * per]
nums = [int(v) for v in window if not v.startswith("error")]
- check(f"{label} pass {i} does something",
- nums and max(0, min(255, max(nums))) > 0,
- f"{expr!r} is 0 everywhere")
+ check(
+ f"{label} pass {i} does something",
+ nums and max(0, min(255, max(nums))) > 0,
+ f"{expr!r} is 0 everywhere",
+ )
# And that a refusal is a refusal on both sides, since a page that quietly
# accepts what the config rejects teaches the wrong language.
@@ -262,13 +375,21 @@ def main():
harness = tempfile.NamedTemporaryFile("w", suffix=".js", delete=False)
harness.write(JS_HARNESS)
harness.close()
- rjs = subprocess.run(["node", harness.name, HTML, cases.name],
- capture_output=True, text=True).stdout.strip().split("\n")
+ rjs = (
+ subprocess.run(
+ ["node", harness.name, HTML, cases.name], capture_output=True, text=True
+ )
+ .stdout.strip()
+ .split("\n")
+ )
os.unlink(cases.name)
os.unlink(harness.name)
for expr, c, j in zip(refused, cw, rjs):
- check(f"both refuse {expr!r}",
- c.startswith("error") and j.startswith("error"), f"C {c!r}, JS {j!r}")
+ check(
+ f"both refuse {expr!r}",
+ c.startswith("error") and j.startswith("error"),
+ f"C {c!r}, JS {j!r}",
+ )
return report()
diff --git a/tests/test_skill.py b/tests/test_skill.py
index edb3303..43380a4 100755
--- a/tests/test_skill.py
+++ b/tests/test_skill.py
@@ -9,6 +9,7 @@ thing it was told it could. So every verb, every environment variable and every
It is deliberately not checked for prose. It is checked for claims that can rot.
"""
+
import json
import os
import re
@@ -48,7 +49,7 @@ def test_every_verb_it_names_is_a_verb_the_program_has():
that fails in front of somebody."""
body = text()
named = set(re.findall(r'\{"cmd":"([a-z-]+)"', body))
- named |= set(re.findall(r'^\| `([a-z-]+)`(?: `([a-z-]+)`)*', body, re.M))
+ named |= set(re.findall(r"^\| `([a-z-]+)`(?: `([a-z-]+)`)*", body, re.M))
# the verb table lists several per row, backticked
for row in re.findall(r"^\| ((?:`[a-z-]+` ?)+)\|", body, re.M):
named |= set(re.findall(r"`([a-z-]+)`", row))
@@ -63,8 +64,7 @@ def test_every_verb_it_names_is_a_verb_the_program_has():
r = s.api(verb)
if r.get("ok") is False and "unknown cmd" in r.get("error", ""):
unknown.append(verb)
- check("every verb it names exists", not unknown,
- "unknown: " + str(unknown))
+ check("every verb it names exists", not unknown, "unknown: " + str(unknown))
def test_the_environment_it_promises_is_the_environment_panes_get():
@@ -80,22 +80,34 @@ def test_the_environment_it_promises_is_the_environment_panes_get():
# somewhere else. Passing the lie in makes the difference visible from any
# shell, rather than only from one that happens to have them set.
lie = {"SL0PPTY_SESSION": "somebody-elses-session", "SL0PPTY_BIN": "/nope/x"}
- probe = ["/bin/sh", "-c",
- 'echo "V=[$SL0PPTY] S=[$SL0PPTY_SESSION] B=[$SL0PPTY_BIN]"; read x']
+ probe = [
+ "/bin/sh",
+ "-c",
+ 'echo "V=[$SL0PPTY] S=[$SL0PPTY_SESSION] B=[$SL0PPTY_BIN]"; read x',
+ ]
with Session(probe, cols=120, rows=8, env=lie) as s:
s.until_text("V=[")
screen = s.snapshot().screen()
check("a pane really is told it is in one", "V=[1]" in screen, screen)
- check("--script has no socket, so it names no session",
- "S=[]" in screen, screen)
- check("...and clears an inherited one rather than passing it on",
- "somebody-elses-session" not in screen, screen)
- check("the binary is the one that made the pane, not an inherited path",
- "/nope/x" not in screen and "sl0ppty" in screen.split("B=[")[1],
- screen)
- flat = " ".join(body.split()) # the file is wrapped; the claim is not
- check("which the skill says out loud",
- "unset under `--script`" in flat, "not documented")
+ check(
+ "--script has no socket, so it names no session", "S=[]" in screen, screen
+ )
+ check(
+ "...and clears an inherited one rather than passing it on",
+ "somebody-elses-session" not in screen,
+ screen,
+ )
+ check(
+ "the binary is the one that made the pane, not an inherited path",
+ "/nope/x" not in screen and "sl0ppty" in screen.split("B=[")[1],
+ screen,
+ )
+ flat = " ".join(body.split()) # the file is wrapped; the claim is not
+ check(
+ "which the skill says out loud",
+ "unset under `--script`" in flat,
+ "not documented",
+ )
def test_a_real_session_names_itself_to_its_panes():
@@ -107,9 +119,22 @@ def test_a_real_session_names_itself_to_its_panes():
env = dict(os.environ)
env["SL0PPTY_SESSION"] = "somebody-elses-session"
try:
- subprocess.run([BIN, "-s", name, "--", "/bin/sh", "-c",
- 'echo "S=[$SL0PPTY_SESSION]" > %s; read x' % out],
- capture_output=True, text=True, timeout=30, input="", env=env)
+ subprocess.run(
+ [
+ BIN,
+ "-s",
+ name,
+ "--",
+ "/bin/sh",
+ "-c",
+ 'echo "S=[$SL0PPTY_SESSION]" > %s; read x' % out,
+ ],
+ capture_output=True,
+ text=True,
+ timeout=30,
+ input="",
+ env=env,
+ )
deadline = time.time() + 5
seen = ""
while time.time() < deadline:
@@ -118,11 +143,15 @@ def test_a_real_session_names_itself_to_its_panes():
if seen:
break
time.sleep(0.05)
- check("a pane in a real session is told which session that is",
- seen == "S=[%s]" % name, repr(seen))
+ check(
+ "a pane in a real session is told which session that is",
+ seen == "S=[%s]" % name,
+ repr(seen),
+ )
finally:
- subprocess.run([BIN, "-s", name, "cmd", '{"cmd":"quit"}'],
- capture_output=True, text=True)
+ subprocess.run(
+ [BIN, "-s", name, "cmd", '{"cmd":"quit"}'], capture_output=True, text=True
+ )
def test_the_panes_fields_it_tells_agents_to_poll_are_real():
@@ -131,8 +160,17 @@ def test_the_panes_fields_it_tells_agents_to_poll_are_real():
body = text()
with Session(SH, cols=80, rows=12) as s:
pane = s.panes()[0]
- for field in ("id", "purpose", "alive", "exit_code", "tab_id",
- "content_x", "content_y", "content_w", "content_h"):
+ for field in (
+ "id",
+ "purpose",
+ "alive",
+ "exit_code",
+ "tab_id",
+ "content_x",
+ "content_y",
+ "content_w",
+ "content_h",
+ ):
check("panes reports " + field, field in pane, str(pane))
check("and the skill names it", field in body, "missing " + field)
@@ -142,8 +180,11 @@ def test_the_recipe_in_the_skill_actually_works():
completion, exit status read off `panes`. Copied from the file rather than
paraphrased, so a change to the file is a change to what is tested."""
with Session(SH, cols=90, rows=14) as s:
- s.api("apply-layout", kdl='layout { tab name="build" '
- '{ pane purpose="task:build" command="sh -c \'echo built; exit 3\'" } }')
+ s.api(
+ "apply-layout",
+ kdl='layout { tab name="build" '
+ '{ pane purpose="task:build" command="sh -c \'echo built; exit 3\'" } }',
+ )
target = None
for _ in range(200):
s.settle(30)
@@ -151,25 +192,40 @@ def test_the_recipe_in_the_skill_actually_works():
if got and not got[0]["alive"]:
target = got[0]
break
- check("the tagged pane is findable by purpose", target is not None,
- str(s.panes()))
- check("it reports the real exit status once it stops",
- target and target["exit_code"] == 3, str(target))
- check("and its output is still readable afterwards",
- "built" in s.snapshot().screen(), s.snapshot().screen())
+ check(
+ "the tagged pane is findable by purpose", target is not None, str(s.panes())
+ )
+ check(
+ "it reports the real exit status once it stops",
+ target and target["exit_code"] == 3,
+ str(target),
+ )
+ check(
+ "and its output is still readable afterwards",
+ "built" in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
def test_it_is_free_of_this_machine():
- """"Plug and play" is a property somebody has to keep. A path out of the
+ """ "Plug and play" is a property somebody has to keep. A path out of the
author's home directory in a skill is a path an end user cannot use."""
body = text()
bad = []
- for pat in (r"/home/[a-z]", r"/Users/[a-z]", r"/tmp/[a-z0-9]",
- r"\bclank\b", re.escape(os.path.expanduser("~"))):
+ for pat in (
+ r"/home/[a-z]",
+ r"/Users/[a-z]",
+ r"/tmp/[a-z0-9]",
+ r"\bclank\b",
+ re.escape(os.path.expanduser("~")),
+ ):
bad += [m.group(0) for m in re.finditer(pat, body)]
check("no path from the machine it was written on", not bad, str(set(bad)))
- check("it reaches the binary through the environment",
- "SL0PPTY_BIN" in body and "not on your `PATH`" in body, "no PATH note")
+ check(
+ "it reaches the binary through the environment",
+ "SL0PPTY_BIN" in body and "not on your `PATH`" in body,
+ "no PATH note",
+ )
def test_settle_is_not_promised_over_the_socket():
@@ -180,14 +236,19 @@ def test_settle_is_not_promised_over_the_socket():
with Session(SH, cols=80, rows=10) as s:
r = s.api("settle", ms=10)
check("settle is not a socket verb", r.get("ok") is False, str(r))
- check("and the skill says there is no wait primitive",
- "no wait primitive" in text(), "not documented")
+ check(
+ "and the skill says there is no wait primitive",
+ "no wait primitive" in text(),
+ "not documented",
+ )
def test_the_skill_is_reachable_from_the_docs_people_read():
"""An agent finds it by convention. A human has to be told it is there."""
- for path, what in ((os.path.join(ROOT, "contrib", "README.md"), "contrib"),
- (os.path.join(ROOT, "docs", "scripting.md"), "scripting")):
+ for path, what in (
+ (os.path.join(ROOT, "contrib", "README.md"), "contrib"),
+ (os.path.join(ROOT, "docs", "scripting.md"), "scripting"),
+ ):
with open(path) as f:
body = f.read()
check("%s points at the skill" % what, "driving-sl0ppty" in body, path)
diff --git a/tests/test_split_ux.py b/tests/test_split_ux.py
index 398ca7e..e6847b8 100644
--- a/tests/test_split_ux.py
+++ b/tests/test_split_ux.py
@@ -5,6 +5,7 @@ The old per-pane `+` was one glyph for a verb that has four directions, and it
taxed every frame three columns forever. A border encodes the direction by
being on that side, costs nothing when idle, and shows a preview on hover.
"""
+
import os
import sys
import tempfile
@@ -12,7 +13,6 @@ import time
from harness import Session, check, report
-
FAST_TEXT = "hover_delay_ms 20\n"
@@ -56,24 +56,37 @@ def test_no_more_plus():
s.settle()
snap = s.snapshot()
p = s.pane()
- check("the frame has no split button any more",
- "+" not in snap.line(p["y"]), repr(snap.line(p["y"])))
- check("the status bar still has its own new-tab button",
- any(h["action"] == "newtab" and h["y"] == 1 for h in snap.hits),
- repr(snap.line(1)))
+ check(
+ "the frame has no split button any more",
+ "+" not in snap.line(p["y"]),
+ repr(snap.line(p["y"])),
+ )
+ check(
+ "the status bar still has its own new-tab button",
+ any(h["action"] == "newtab" and h["y"] == 1 for h in snap.hits),
+ repr(snap.line(1)),
+ )
# The frame does spend columns again -- six of them, on the zoom and
# close buttons -- but on two verbs that a single glyph *can* encode.
# The split button was removed because one glyph cannot mean four
# directions, not because buttons are forbidden. With them off, the row
# is all title and rule again, and that is the stronger claim.
- check("the title gets most of the row back",
- snap.line(p["y"]).count("─") > p["w"] - 20, repr(snap.line(p["y"])))
+ check(
+ "the title gets most of the row back",
+ snap.line(p["y"]).count("─") > p["w"] - 20,
+ repr(snap.line(p["y"])),
+ )
- with Session(SH, cols=60, rows=14, config=cfg(FAST_TEXT + "pane_buttons false\n")) as s:
+ with Session(
+ SH, cols=60, rows=14, config=cfg(FAST_TEXT + "pane_buttons false\n")
+ ) as s:
s.settle()
snap, p = s.snapshot(), s.pane()
- check("and all of it when the buttons are off",
- snap.line(p["y"]).count("─") > p["w"] - 12, repr(snap.line(p["y"])))
+ check(
+ "and all of it when the buttons are off",
+ snap.line(p["y"]).count("─") > p["w"] - 12,
+ repr(snap.line(p["y"])),
+ )
def test_each_border_splits_toward_itself():
@@ -90,8 +103,9 @@ def test_each_border_splits_toward_itself():
click(s, x, y)
s.settle(80)
panes = s.panes()
- check(f"clicking the {side} border splits", len(panes) == 2,
- str(len(panes)))
+ check(
+ f"clicking the {side} border splits", len(panes) == 2, str(len(panes))
+ )
if len(panes) != 2:
continue
@@ -105,14 +119,21 @@ def test_each_border_splits_toward_itself():
ok = new["y"] > old["y"] and new["x"] == old["x"]
else:
ok = new["y"] < old["y"] and new["x"] == old["x"]
- check(f"the new pane lands {expect}", ok,
- f"old={old['x']},{old['y']} new={new['x']},{new['y']}")
+ check(
+ f"the new pane lands {expect}",
+ ok,
+ f"old={old['x']},{old['y']} new={new['x']},{new['y']}",
+ )
check("and takes focus", new["focused"], str(new))
- check("and it says which way it went",
- f"split {side if side != 'bottom' else 'down'}"
- .replace("split top", "split up") in s.snapshot().screen()
- or "split" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-80:]))
+ check(
+ "and it says which way it went",
+ f"split {side if side != 'bottom' else 'down'}".replace(
+ "split top", "split up"
+ )
+ in s.snapshot().screen()
+ or "split" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-80:]),
+ )
def test_the_guide():
@@ -120,29 +141,45 @@ def test_the_guide():
s.settle()
p = s.pane()
snap = s.snapshot()
- check("no guide when the pointer is elsewhere",
- "┃" not in snap.screen() and "╎" not in snap.screen(),
- repr(snap.screen()[:120]))
+ check(
+ "no guide when the pointer is elsewhere",
+ "┃" not in snap.screen() and "╎" not in snap.screen(),
+ repr(snap.screen()[:120]),
+ )
rest(s, p["x"], p["y"] + 2) # left border
snap = s.snapshot()
- check("hovering a side border arms it", "┃" in snap.screen(),
- repr(snap.screen()[:200]))
- check("and previews where the split would land", "╎" in snap.screen(),
- repr(snap.screen()[:200]))
+ check(
+ "hovering a side border arms it",
+ "┃" in snap.screen(),
+ repr(snap.screen()[:200]),
+ )
+ check(
+ "and previews where the split would land",
+ "╎" in snap.screen(),
+ repr(snap.screen()[:200]),
+ )
rest(s, p["x"] + 4, p["y"] + p["h"] - 1) # bottom border
snap = s.snapshot()
- check("hovering the bottom border arms that instead",
- "━" in snap.screen() and "╌" in snap.screen(), repr(snap.screen()[:200]))
- check("and the side guide is gone", "┃" not in snap.screen(),
- repr(snap.screen()[:200]))
+ check(
+ "hovering the bottom border arms that instead",
+ "━" in snap.screen() and "╌" in snap.screen(),
+ repr(snap.screen()[:200]),
+ )
+ check(
+ "and the side guide is gone",
+ "┃" not in snap.screen(),
+ repr(snap.screen()[:200]),
+ )
rest(s, p["content_x"] + 3, p["content_y"] + 2) # into the content
snap = s.snapshot()
- check("moving off a border puts the guide away",
- "━" not in snap.screen() and "┃" not in snap.screen(),
- repr(snap.screen()[:200]))
+ check(
+ "moving off a border puts the guide away",
+ "━" not in snap.screen() and "┃" not in snap.screen(),
+ repr(snap.screen()[:200]),
+ )
def test_guide_follows_the_new_layout():
@@ -166,17 +203,24 @@ def test_guide_follows_the_new_layout():
panes = s.panes()
check("the split happened", len(panes) == 2, str(len(panes)))
- armed = [q for q in panes
- if "┃" in "".join(row[q["x"]:q["x"] + q["w"]] for row in snap.text)]
+ armed = [
+ q
+ for q in panes
+ if "┃" in "".join(row[q["x"] : q["x"] + q["w"]] for row in snap.text)
+ ]
check("exactly one pane shows a guide", len(armed) == 1, str(armed))
if len(armed) != 1:
return
- check("and it is the one the pointer is actually over",
- armed[0]["x"] <= edge_x < armed[0]["x"] + armed[0]["w"],
- f"pointer at {edge_x}, guide on {armed[0]['x']}..{armed[0]['x'] + armed[0]['w']}")
- check("on the edge the pointer is on",
- snap.hit_at(edge_x, edge_y) == f"border:{armed[0]['id']}:r",
- str(snap.hit_at(edge_x, edge_y)))
+ check(
+ "and it is the one the pointer is actually over",
+ armed[0]["x"] <= edge_x < armed[0]["x"] + armed[0]["w"],
+ f"pointer at {edge_x}, guide on {armed[0]['x']}..{armed[0]['x'] + armed[0]['w']}",
+ )
+ check(
+ "on the edge the pointer is on",
+ snap.hit_at(edge_x, edge_y) == f"border:{armed[0]['id']}:r",
+ str(snap.hit_at(edge_x, edge_y)),
+ )
def test_the_guide_waits_for_a_rest():
@@ -189,28 +233,38 @@ def test_the_guide_waits_for_a_rest():
hover(s, left, row)
snap = s.snapshot() # no dwell: this is the moment of contact
- check("touching a border does not arm it immediately",
- "┃" not in snap.screen(), repr(snap.screen()[:160]))
+ check(
+ "touching a border does not arm it immediately",
+ "┃" not in snap.screen(),
+ repr(snap.screen()[:160]),
+ )
# keep moving: across the pane and onto the other border
hover(s, left + 6, row)
hover(s, right, row)
snap = s.snapshot()
- check("passing over borders arms nothing", "┃" not in snap.screen(),
- repr(snap.screen()[:160]))
+ check(
+ "passing over borders arms nothing",
+ "┃" not in snap.screen(),
+ repr(snap.screen()[:160]),
+ )
time.sleep(0.4) # now rest
snap = s.snapshot()
- check("resting on one arms it", "┃" in snap.screen(),
- repr(snap.screen()[:200]))
- check("and it is the border being rested on",
- snap.hit_at(right, row) == f"border:{p['id']}:r",
- str(snap.hit_at(right, row)))
+ check("resting on one arms it", "┃" in snap.screen(), repr(snap.screen()[:200]))
+ check(
+ "and it is the border being rested on",
+ snap.hit_at(right, row) == f"border:{p['id']}:r",
+ str(snap.hit_at(right, row)),
+ )
hover(s, p["content_x"] + 2, row)
snap = s.snapshot()
- check("moving away disarms it at once", "┃" not in snap.screen(),
- repr(snap.screen()[:160]))
+ check(
+ "moving away disarms it at once",
+ "┃" not in snap.screen(),
+ repr(snap.screen()[:160]),
+ )
def test_press_skips_the_wait():
@@ -220,13 +274,18 @@ def test_press_skips_the_wait():
x, y = p["x"] + p["w"] - 1, p["y"] + 3
s.send(rf"\e[<0;{x + 1};{y + 1}M") # press and hold: that is intent
snap = s.snapshot()
- check("holding the button on a border arms it immediately",
- "┃" in snap.screen(), repr(snap.screen()[:200]))
+ check(
+ "holding the button on a border arms it immediately",
+ "┃" in snap.screen(),
+ repr(snap.screen()[:200]),
+ )
s.send(rf"\e[<0;{x + 1};{y + 1}m")
def test_delay_is_configurable():
- import os, tempfile
+ import os
+ import tempfile
+
f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
f.write("hover_delay_ms 0\n")
f.close()
@@ -235,8 +294,11 @@ def test_delay_is_configurable():
p = s.pane()
hover(s, p["x"], p["y"] + 3)
snap = s.snapshot()
- check("with no delay configured it arms on contact",
- "┃" in snap.screen(), repr(snap.screen()[:160]))
+ check(
+ "with no delay configured it arms on contact",
+ "┃" in snap.screen(),
+ repr(snap.screen()[:160]),
+ )
os.unlink(f.name)
@@ -249,10 +311,16 @@ def test_guide_is_per_pane():
rest(s, left["x"], left["y"] + 2)
snap = s.snapshot()
row = snap.line(left["y"] + 2)
- check("the hovered pane is armed", "┃" in row[left["x"]:left["x"] + left["w"]],
- repr(row))
- check("the other one is not",
- "┃" not in row[right["x"]:right["x"] + right["w"]], repr(row))
+ check(
+ "the hovered pane is armed",
+ "┃" in row[left["x"] : left["x"] + left["w"]],
+ repr(row),
+ )
+ check(
+ "the other one is not",
+ "┃" not in row[right["x"] : right["x"] + right["w"]],
+ repr(row),
+ )
def test_drag_still_moves():
@@ -268,18 +336,23 @@ def test_drag_still_moves():
s.send(rf"\e[<32;{right['x'] + 5};{right['y'] + 3}M")
s.send(rf"\e[<0;{right['x'] + 5};{right['y'] + 3}m")
s.settle(80)
- check("dragging the top border still moves the pane",
- len(s.panes()) == 2, str(len(s.panes())))
+ check(
+ "dragging the top border still moves the pane",
+ len(s.panes()) == 2,
+ str(len(s.panes())),
+ )
panes = {q["id"]: q for q in s.panes()}
- check("and swaps them", panes[left["id"]]["x"] > panes[right["id"]]["x"],
- str(s.panes()))
+ check(
+ "and swaps them",
+ panes[left["id"]]["x"] > panes[right["id"]]["x"],
+ str(s.panes()),
+ )
# click: press, release, no motion -> split up
s.send(rf"\e[<0;{left['x'] + 5};{left['y'] + 1}M")
s.send(rf"\e[<0;{left['x'] + 5};{left['y'] + 1}m")
s.settle(80)
- check("clicking it splits instead", len(s.panes()) == 3,
- str(len(s.panes())))
+ check("clicking it splits instead", len(s.panes()) == 3, str(len(s.panes())))
def test_tiny_panes_have_no_targets():
@@ -290,18 +363,26 @@ def test_tiny_panes_have_no_targets():
s.resize(30, 8) # collapses to a stack
s.settle(80)
snap = s.snapshot()
- check("a collapsed layout still draws and does not crash",
- s.api("alive")["alive"], "")
+ check(
+ "a collapsed layout still draws and does not crash",
+ s.api("alive")["alive"],
+ "",
+ )
# Say what this means rather than pattern-matching on shape: a *header*
# offers no split targets. The old form forbade any 1x1-ish border hit,
# which also catches the legitimate side border of a very short pane
# that is merely small, not collapsed.
header_rows = {p["y"] for p in s.panes() if p["hidden"]}
- on_headers = [h for h in snap.hits
- if h["action"].startswith("border:")
- and h["y"] in header_rows]
- check("a collapsed header is a focus target, not a split one",
- not on_headers, str(on_headers))
+ on_headers = [
+ h
+ for h in snap.hits
+ if h["action"].startswith("border:") and h["y"] in header_rows
+ ]
+ check(
+ "a collapsed header is a focus target, not a split one",
+ not on_headers,
+ str(on_headers),
+ )
# ---- the floor: a split that is not worth making is not offered ------------
@@ -331,12 +412,18 @@ def test_a_split_below_the_floor_is_not_offered():
rest(s, x, y)
snap = s.snapshot()
body = snap.screen()
- check(f"resting on the {side} border arms nothing below the floor",
- "\u2503" not in body and "\u2501" not in body, repr(body[:80]))
+ check(
+ f"resting on the {side} border arms nothing below the floor",
+ "\u2503" not in body and "\u2501" not in body,
+ repr(body[:80]),
+ )
click(s, x, y)
s.settle(80)
- check(f"and clicking the {side} border splits nothing",
- len(s.panes()) == 1, str(len(s.panes())))
+ check(
+ f"and clicking the {side} border splits nothing",
+ len(s.panes()) == 1,
+ str(len(s.panes())),
+ )
os.unlink(conf)
@@ -349,13 +436,19 @@ def test_the_floor_is_configurable_in_both_axes():
x, y = edge_of(p, "b")
click(s, x, y)
s.settle(80)
- check("a row split below the row floor is refused",
- len(s.panes()) == 1, str(len(s.panes())))
+ check(
+ "a row split below the row floor is refused",
+ len(s.panes()) == 1,
+ str(len(s.panes())),
+ )
x, y = edge_of(p, "r")
click(s, x, y)
s.settle(80)
- check("while a column split above the column floor still happens",
- len(s.panes()) == 2, str(len(s.panes())))
+ check(
+ "while a column split above the column floor still happens",
+ len(s.panes()) == 2,
+ str(len(s.panes())),
+ )
os.unlink(conf)
@@ -365,13 +458,19 @@ def test_the_keyboard_obeys_the_same_floor():
s.settle()
s.key("\\\\")
s.settle(80)
- check("the keyboard will not split below the floor either",
- len(s.panes()) == 1, str(len(s.panes())))
+ check(
+ "the keyboard will not split below the floor either",
+ len(s.panes()) == 1,
+ str(len(s.panes())),
+ )
# ...but a script asking for a pane is declaring, not being offered.
s.api("split", dir="cols")
s.settle(80)
- check("the control API is not bound by it", len(s.panes()) == 2,
- str(len(s.panes())))
+ check(
+ "the control API is not bound by it",
+ len(s.panes()) == 2,
+ str(len(s.panes())),
+ )
os.unlink(conf)
@@ -383,11 +482,17 @@ def test_the_guide_says_which_way_it_goes():
x, y = edge_of(p, side)
rest(s, x, y)
body = s.snapshot().screen()
- check(f"the {side} guide carries its own arrow",
- ARROWS[side] in body, repr(ARROWS[side]))
+ check(
+ f"the {side} guide carries its own arrow",
+ ARROWS[side] in body,
+ repr(ARROWS[side]),
+ )
others = [a for k, a in ARROWS.items() if k != side]
- check(f"and only that one", not any(o in body for o in others),
- repr(body[:80]))
+ check(
+ f"and only that one",
+ not any(o in body for o in others),
+ repr(body[:80]),
+ )
def test_the_arrow_sits_on_the_new_boundary():
@@ -398,21 +503,29 @@ def test_the_arrow_sits_on_the_new_boundary():
rest(s, x, y)
snap = s.snapshot()
pos = snap.find(ARROWS["r"])
- check("the arrow is on the dashed line, not the border",
- pos is not None and pos[0] == p["x"] + p["w"] // 2, str(pos))
+ check(
+ "the arrow is on the dashed line, not the border",
+ pos is not None and pos[0] == p["x"] + p["w"] // 2,
+ str(pos),
+ )
# ---- `C-a Enter`: split whichever way there is room for -----------------------
def geometry(s):
- return [(p["x"], p["y"], p["w"], p["h"])
- for p in sorted(s.panes(), key=lambda q: (q["y"], q["x"]))]
+ return [
+ (p["x"], p["y"], p["w"], p["h"])
+ for p in sorted(s.panes(), key=lambda q: (q["y"], q["x"]))
+ ]
def toast_line(s):
- got = [l.strip() for l in s.snapshot().text
- if "split right" in l or "split down" in l or "no room" in l]
+ got = [
+ l.strip()
+ for l in s.snapshot().text
+ if "split right" in l or "split down" in l or "no room" in l
+ ]
return got[-1] if got else ""
@@ -425,19 +538,26 @@ def test_it_splits_across_the_longer_side():
s.key(r"\r")
s.settle(60)
g = geometry(s)
- check("a wide pane splits into columns", len(g) == 2 and g[0][1] == g[1][1],
- str(g))
+ check(
+ "a wide pane splits into columns",
+ len(g) == 2 and g[0][1] == g[1][1],
+ str(g),
+ )
check("side by side, evenly", g[0][2] == g[1][2], str(g))
- check("and it says which way it went", toast_line(s) == "split right",
- repr(toast_line(s)))
+ check(
+ "and it says which way it went",
+ toast_line(s) == "split right",
+ repr(toast_line(s)),
+ )
with Session(SH, cols=60, rows=40) as s:
s.settle()
s.key(r"\r")
s.settle(60)
g = geometry(s)
- check("a tall pane splits into rows", len(g) == 2 and g[0][0] == g[1][0],
- str(g))
+ check(
+ "a tall pane splits into rows", len(g) == 2 and g[0][0] == g[1][0], str(g)
+ )
check("one above the other", g[1][1] > g[0][1], str(g))
check("and says so", toast_line(s) == "split down", repr(toast_line(s)))
@@ -457,15 +577,21 @@ def test_the_aspect_is_the_configured_one():
s.key(r"\r")
s.settle(60)
g = geometry(s)
- check("with square cells, 72x40 splits into columns",
- len(g) == 2 and g[0][1] == g[1][1], str(g))
+ check(
+ "with square cells, 72x40 splits into columns",
+ len(g) == 2 and g[0][1] == g[1][1],
+ str(g),
+ )
with Session(SH, cols=74, rows=44) as s:
s.settle()
s.key(r"\r")
s.settle(60)
g = geometry(s)
- check("...and with the default aspect, the same pane splits into rows",
- len(g) == 2 and g[0][0] == g[1][0], str(g))
+ check(
+ "...and with the default aspect, the same pane splits into rows",
+ len(g) == 2 and g[0][0] == g[1][0],
+ str(g),
+ )
os.unlink(square)
@@ -489,8 +615,11 @@ def test_it_falls_back_to_the_axis_that_fits():
s.settle(60)
g = geometry(s)
check("narrow and tall splits into rows", g[0][0] == g[1][0], str(g))
- check("...even though columns is what a naive cell count would pick",
- g[1][1] > g[0][1], str(g))
+ check(
+ "...even though columns is what a naive cell count would pick",
+ g[1][1] > g[0][1],
+ str(g),
+ )
def test_no_room_says_so_rather_than_collapsing_the_tab():
@@ -503,10 +632,14 @@ def test_no_room_says_so_rather_than_collapsing_the_tab():
before = geometry(s)
s.key(r"\r")
s.settle(60)
- check("nothing was split", geometry(s) == before,
- "%s -> %s" % (before, geometry(s)))
- check("and it says why", toast_line(s) == "no room to split",
- repr(toast_line(s)))
+ check(
+ "nothing was split",
+ geometry(s) == before,
+ "%s -> %s" % (before, geometry(s)),
+ )
+ check(
+ "and it says why", toast_line(s) == "no room to split", repr(toast_line(s))
+ )
# The explicit keys answer the same way, because whether there is room cannot
# depend on which key you asked with.
@@ -516,8 +649,11 @@ def test_no_room_says_so_rather_than_collapsing_the_tab():
s.key("\\\\")
s.settle(60)
check("`C-a \\` refuses it too", geometry(s) == before, str(geometry(s)))
- check("...in its own words", "no room to split across" in s.snapshot().screen(),
- repr(s.snapshot().screen()[-100:]))
+ check(
+ "...in its own words",
+ "no room to split across" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[-100:]),
+ )
def test_it_is_on_the_sheet_and_in_the_palette():
@@ -526,8 +662,11 @@ def test_it_is_on_the_sheet_and_in_the_palette():
s.send(r"\x01?")
s.settle(60)
sheet = s.snapshot().screen()
- check("the sheet lists it", "split the longer way" in sheet,
- repr([l.strip() for l in s.snapshot().text if "longer" in l]))
+ check(
+ "the sheet lists it",
+ "split the longer way" in sheet,
+ repr([l.strip() for l in s.snapshot().text if "longer" in l]),
+ )
check("under Enter", "enter" in sheet.lower(), "")
s.send(r"\e")
s.settle(40)
@@ -535,8 +674,11 @@ def test_it_is_on_the_sheet_and_in_the_palette():
s.settle(40)
s.send("longer")
s.settle(60)
- check("and the palette finds it", "split the longer way" in s.snapshot().screen(),
- repr([l.strip() for l in s.snapshot().text if "longer" in l]))
+ check(
+ "and the palette finds it",
+ "split the longer way" in s.snapshot().screen(),
+ repr([l.strip() for l in s.snapshot().text if "longer" in l]),
+ )
if __name__ == "__main__":
diff --git a/tests/test_states.py b/tests/test_states.py
index fa1426e..bd2ab5d 100644
--- a/tests/test_states.py
+++ b/tests/test_states.py
@@ -11,6 +11,7 @@ dragged. Those are ambient contrast, which is a taste, and a taste shipped as
a default is how a tool gets a reputation for fighting you. This file asserts
that line in both directions: the first three are coloured, the fourth is not.
"""
+
import os
import sys
import tempfile
@@ -18,10 +19,13 @@ import tempfile
from harness import Session, check, report
# A pane with a screenful of known-coloured text.
-SH = ["/bin/sh", "-c",
- 'printf "\\033]2;p\\007"; i=0; '
- 'while [ $i -lt 40 ]; do printf "line %s ABCDEFGHIJKLMNOP\\n" $i; i=$((i+1)); done; '
- 'read x']
+SH = [
+ "/bin/sh",
+ "-c",
+ 'printf "\\033]2;p\\007"; i=0; '
+ 'while [ $i -lt 40 ]; do printf "line %s ABCDEFGHIJKLMNOP\\n" $i; i=$((i+1)); done; '
+ "read x",
+]
# Only the theme, so "what colour is this cell" has one answer. No states: the
# point is what sl0ppty does when you have said nothing.
@@ -47,30 +51,39 @@ def test_a_dead_pane_is_coloured_by_default():
# A *commanded* pane: only those keep their corpse by default, which is
# the only kind there is to colour.
lay = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
- lay.write('layout {\n tab {\n'
- ' pane command="printf \'\\\\033]2;p\\\\007\'; echo DONE; exit 3"\n'
- ' }\n}\n')
+ lay.write(
+ "layout {\n tab {\n"
+ " pane command=\"printf '\\\\033]2;p\\\\007'; echo DONE; exit 3\"\n"
+ " }\n}\n"
+ )
lay.close()
with Session(SH, cols=50, rows=10, config=cfg(), layout=lay.name) as s:
snap = s.until_text("[process exited")
- check("its contents are not left looking live",
- fg(snap, s.pane()) not in (None, "#ffffff"), str(fg(snap, s.pane())))
+ check(
+ "its contents are not left looking live",
+ fg(snap, s.pane()) not in (None, "#ffffff"),
+ str(fg(snap, s.pane())),
+ )
os.unlink(lay.name)
def test_a_suspended_pane_is_coloured_by_default():
layout = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
- layout.write('layout {\n tab name="t" {\n'
- ' pane command="echo hello; read x" suspended=true\n'
- ' }\n}\n')
+ layout.write(
+ 'layout {\n tab name="t" {\n'
+ ' pane command="echo hello; read x" suspended=true\n'
+ " }\n}\n"
+ )
layout.close()
with Session(SH, cols=60, rows=12, config=cfg(), layout=layout.name) as s:
s.settle(60)
pane = s.pane()
snap = s.snapshot()
- check("a pane that never started does not look like one that did",
- fg(snap, pane) not in (None, "#ffffff"),
- f"{fg(snap, pane)} — {snap.pane_text(pane)[:40]!r}")
+ check(
+ "a pane that never started does not look like one that did",
+ fg(snap, pane) not in (None, "#ffffff"),
+ f"{fg(snap, pane)} — {snap.pane_text(pane)[:40]!r}",
+ )
os.unlink(layout.name)
@@ -80,18 +93,27 @@ def test_scrollback_is_coloured_by_default():
pane = s.pane()
live = fg(s.snapshot(), pane)
- s.send(r"\x01\x1b[5~") # C-a PgUp: into the scrollback
+ s.send(r"\x01\x1b[5~") # C-a PgUp: into the scrollback
s.settle(20)
past = fg(s.snapshot(), pane)
- check("the pane is scrolled", s.panes() and "▲" in s.snapshot().screen(),
- s.snapshot().screen()[:200])
- check("looking at the past looks different from the present",
- past != live, f"live {live}, scrolled {past}")
+ check(
+ "the pane is scrolled",
+ s.panes() and "▲" in s.snapshot().screen(),
+ s.snapshot().screen()[:200],
+ )
+ check(
+ "looking at the past looks different from the present",
+ past != live,
+ f"live {live}, scrolled {past}",
+ )
- s.send(r"\x01\x1b[F") # C-a End: back to the bottom
+ s.send(r"\x01\x1b[F") # C-a End: back to the bottom
s.settle(20)
- check("and it goes back when you do", fg(s.snapshot(), pane) == live,
- f"{fg(s.snapshot(), pane)} vs {live}")
+ check(
+ "and it goes back when you do",
+ fg(s.snapshot(), pane) == live,
+ f"{fg(s.snapshot(), pane)} vs {live}",
+ )
def test_an_unfocused_pane_is_dimmed_by_default():
@@ -102,20 +124,19 @@ def test_an_unfocused_pane_is_dimmed_by_default():
with Session(SH, cols=90, rows=14, config=cfg()) as s:
s.until_text("line 39")
before = fg(s.snapshot(), s.pane())
- s.key("\\\\") # split; the left pane loses focus
+ s.key("\\\\") # split; the left pane loses focus
s.settle(40)
unfocused = [p for p in s.panes() if not p["focused"]][0]
after = fg(s.snapshot(), unfocused)
- check("the pane you left is pushed back", after != before,
- f"{before} -> {after}")
- check("but still plainly readable", after not in (None, "#000000"),
- str(after))
+ check(
+ "the pane you left is pushed back", after != before, f"{before} -> {after}"
+ )
+ check("but still plainly readable", after not in (None, "#000000"), str(after))
def test_dim_unfocused_zero_turns_it_off():
conf = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
- conf.write('theme { default_fg "#ffffff" default_bg "#000000" }\n'
- 'dim_unfocused 0\n')
+ conf.write('theme { default_fg "#ffffff" default_bg "#000000" }\ndim_unfocused 0\n')
conf.close()
with Session(SH, cols=90, rows=14, config=conf.name) as s:
s.until_text("line 39")
@@ -123,8 +144,11 @@ def test_dim_unfocused_zero_turns_it_off():
s.key("\\\\")
s.settle(40)
unfocused = [p for p in s.panes() if not p["focused"]][0]
- check("nothing is done to it", fg(s.snapshot(), unfocused) == before,
- f"{before} -> {fg(s.snapshot(), unfocused)}")
+ check(
+ "nothing is done to it",
+ fg(s.snapshot(), unfocused) == before,
+ f"{before} -> {fg(s.snapshot(), unfocused)}",
+ )
os.unlink(conf.name)
@@ -134,11 +158,17 @@ def test_a_states_block_beats_the_knob():
than "you did not say". (A grayscale chain makes a poor probe here — it
leaves white text white, so it looks like nothing happened.)"""
cases = [
- ('dim_unfocused 60\nstates { unfocused { tint amount=255 color="#00ff00" } }\n',
- "#00ff00", "a chain replaces the knob"),
- ('dim_unfocused 60\nstates { unfocused { } }\n',
- None, "an empty block means nothing at all"),
- ('dim_unfocused 60\n', "#c3c3c3", "and the knob applies when nobody said"),
+ (
+ 'dim_unfocused 60\nstates { unfocused { tint amount=255 color="#00ff00" } }\n',
+ "#00ff00",
+ "a chain replaces the knob",
+ ),
+ (
+ "dim_unfocused 60\nstates { unfocused { } }\n",
+ None,
+ "an empty block means nothing at all",
+ ),
+ ("dim_unfocused 60\n", "#c3c3c3", "and the knob applies when nobody said"),
]
for text, want, label in cases:
conf = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
@@ -159,18 +189,19 @@ def test_a_flattened_tab_still_says_which_panes_are_not_live():
(D13) — so the words have to carry what the colour carries everywhere
else. Collapsing a tab must not hide the fact."""
layout = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
- layout.write('layout {\n tab name="t" {\n'
- ' pane command="stty raw -echo; cat"\n'
- ' pane command="echo hi; read x" suspended=true\n'
- ' }\n}\n')
+ layout.write(
+ 'layout {\n tab name="t" {\n'
+ ' pane command="stty raw -echo; cat"\n'
+ ' pane command="echo hi; read x" suspended=true\n'
+ " }\n}\n"
+ )
layout.close()
with Session(SH, cols=100, rows=14, config=cfg(), layout=layout.name) as s:
s.settle(60)
- s.resize(40, 12) # too small for two: the tab flattens
+ s.resize(40, 12) # too small for two: the tab flattens
s.settle(40)
screen = s.snapshot().screen()
- check("the header says the pane never started",
- "not started" in screen, screen)
+ check("the header says the pane never started", "not started" in screen, screen)
os.unlink(layout.name)
diff --git a/tests/test_tabs.py b/tests/test_tabs.py
index a05cef1..3ae570a 100644
--- a/tests/test_tabs.py
+++ b/tests/test_tabs.py
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
"""M3: tabs, purposes and the JSON control API."""
+
import os
import sys
import tempfile
@@ -23,19 +24,26 @@ def test_tabs():
tabs = s.tabs()
check("C-a c makes a tab", len(tabs) == 2, str(tabs))
check("the new tab is active", tabs[1]["active"] and not tabs[0]["active"])
- check("each tab has its own pane", [t["panes"] for t in tabs] == [1, 1],
- str(tabs))
+ check(
+ "each tab has its own pane", [t["panes"] for t in tabs] == [1, 1], str(tabs)
+ )
snap = s.snapshot()
check("the new tab's pane is empty", "in-tab-one" not in snap.screen())
- check("the tab strip is drawn", "1" in snap.line(1) and "2" in snap.line(1),
- repr(snap.line(1)))
+ check(
+ "the tab strip is drawn",
+ "1" in snap.line(1) and "2" in snap.line(1),
+ repr(snap.line(1)),
+ )
s.key(PREV)
s.settle()
check("C-a shift+tab goes back", s.tabs()[0]["active"])
- check("the first tab kept its content", "in-tab-one" in s.snapshot().screen(),
- repr(s.snapshot().screen()[:200]))
+ check(
+ "the first tab kept its content",
+ "in-tab-one" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[:200]),
+ )
s.key("2")
s.settle()
@@ -47,14 +55,18 @@ def test_tabs():
active = [t["index"] for t in s.tabs() if t["active"]]
s.key("n")
s.settle()
- check("C-a n no longer cycles tabs",
- [t["index"] for t in s.tabs() if t["active"]] == active,
- str(s.tabs()))
+ check(
+ "C-a n no longer cycles tabs",
+ [t["index"] for t in s.tabs() if t["active"]] == active,
+ str(s.tabs()),
+ )
s.key("p")
s.settle()
- check("nor does C-a p, which opens the palette",
- [t["index"] for t in s.tabs() if t["active"]] == active,
- str(s.tabs()))
+ check(
+ "nor does C-a p, which opens the palette",
+ [t["index"] for t in s.tabs() if t["active"]] == active,
+ str(s.tabs()),
+ )
s.send(r"\e") # put the palette away again
@@ -71,13 +83,18 @@ def test_background_tabs_keep_running():
s.settle()
s.key(NEXT) # away again
s.settle()
- check("a background pane still accepts input",
- any(p["tab"] == 1 for p in s.panes()), str(s.panes()))
+ check(
+ "a background pane still accepts input",
+ any(p["tab"] == 1 for p in s.panes()),
+ str(s.panes()),
+ )
s.api("select-tab", index=1)
s.settle()
- check("its output survived being off-screen",
- "still-alive" in s.snapshot().screen(),
- repr(s.snapshot().screen()[:200]))
+ check(
+ "its output survived being off-screen",
+ "still-alive" in s.snapshot().screen(),
+ repr(s.snapshot().screen()[:200]),
+ )
def test_tab_click():
@@ -94,8 +111,7 @@ def test_tab_click():
check("the tab label is clickable", action == "tab:1", str(action))
s.click(x, y)
s.settle()
- check("clicking a tab switches to it", s.tabs()[0]["active"],
- str(s.tabs()))
+ check("clicking a tab switches to it", s.tabs()[0]["active"], str(s.tabs()))
def test_closing_a_tab():
@@ -103,7 +119,7 @@ def test_closing_a_tab():
s.settle()
s.key("c")
s.settle()
- s.key("\\\\") # two panes in tab 2
+ s.key("\\\\") # two panes in tab 2
s.settle()
check("tab 2 has two panes", s.tabs()[1]["panes"] == 2, str(s.tabs()))
s.key("x")
@@ -111,10 +127,11 @@ def test_closing_a_tab():
check("closing one pane keeps the tab", len(s.tabs()) == 2, str(s.tabs()))
s.key("x")
s.settle()
- check("closing the last pane closes the tab", len(s.tabs()) == 1,
- str(s.tabs()))
- check("the surviving tab is active and usable",
- s.tabs()[0]["active"] and s.api("alive")["alive"])
+ check("closing the last pane closes the tab", len(s.tabs()) == 1, str(s.tabs()))
+ check(
+ "the surviving tab is active and usable",
+ s.tabs()[0]["active"] and s.api("alive")["alive"],
+ )
def test_closing_a_whole_tab_is_one_verb():
@@ -136,8 +153,11 @@ def test_closing_a_whole_tab_is_one_verb():
s.settle()
check("one press closes the whole tab", len(s.tabs()) == 1, str(s.tabs()))
screen = s.snapshot().screen()
- check("and says how much went with it",
- "closed tab" in screen and "3 panes" in screen, screen)
+ check(
+ "and says how much went with it",
+ "closed tab" in screen and "3 panes" in screen,
+ screen,
+ )
check("the session is still running", s.api("alive")["alive"])
@@ -151,8 +171,11 @@ def test_the_last_tab_is_refused_rather_than_obeyed():
s.settle()
check("the tab is still there", len(s.tabs()) == 1, str(s.tabs()))
check("the session is still running", s.api("alive")["alive"])
- check("and it says what to press instead", "quit" in s.snapshot().screen(),
- s.snapshot().screen())
+ check(
+ "and it says what to press instead",
+ "quit" in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
def test_close_tab_is_reachable_without_the_key():
@@ -160,14 +183,20 @@ def test_close_tab_is_reachable_without_the_key():
s.settle()
s.send(r"\x01?")
s.settle()
- check("the cheatsheet lists it", "close this tab" in s.snapshot().screen(),
- s.snapshot().screen())
+ check(
+ "the cheatsheet lists it",
+ "close this tab" in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
s.send(r"\x01q") # any key dismisses the sheet; this one is swallowed
s.settle()
s.key("p")
s.send("close this tab")
- check("so does the palette", "close this tab" in s.snapshot().screen(),
- s.snapshot().screen())
+ check(
+ "so does the palette",
+ "close this tab" in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
def test_purpose_trust_model():
@@ -176,35 +205,61 @@ def test_purpose_trust_model():
s.settle()
pane = s.pane()
- r = s.api("set-purpose", target="pane", id=pane["id"],
- purpose="agent:main", declared=True)
+ r = s.api(
+ "set-purpose",
+ target="pane",
+ id=pane["id"],
+ purpose="agent:main",
+ declared=True,
+ )
check("a declared purpose is accepted", r["ok"], str(r))
p = s.pane()
check("the purpose is reported", p["purpose"] == "agent:main", str(p))
check("and marked declared", p["purpose_declared"], str(p))
- r = s.api("set-purpose", target="pane", id=pane["id"],
- purpose="evil:overridden", declared=False)
- check("an in-band purpose cannot override a declared one", not r["ok"],
- str(r))
- check("the declared purpose is intact",
- s.pane()["purpose"] == "agent:main", str(s.pane()))
+ r = s.api(
+ "set-purpose",
+ target="pane",
+ id=pane["id"],
+ purpose="evil:overridden",
+ declared=False,
+ )
+ check("an in-band purpose cannot override a declared one", not r["ok"], str(r))
+ check(
+ "the declared purpose is intact",
+ s.pane()["purpose"] == "agent:main",
+ str(s.pane()),
+ )
- r = s.api("set-purpose", target="tab", id=s.tabs()[0]["id"],
- purpose="project:foo bar/../$(rm -rf)", declared=True)
- check("a tab purpose is sanitised on ingest",
- s.tabs()[0]["purpose"] == "project:foobar/../rm-rf",
- str(s.tabs()[0]["purpose"]))
+ r = s.api(
+ "set-purpose",
+ target="tab",
+ id=s.tabs()[0]["id"],
+ purpose="project:foo bar/../$(rm -rf)",
+ declared=True,
+ )
+ check(
+ "a tab purpose is sanitised on ingest",
+ s.tabs()[0]["purpose"] == "project:foobar/../rm-rf",
+ str(s.tabs()[0]["purpose"]),
+ )
with Session(SH, cols=60, rows=12) as s:
s.settle()
pane = s.pane()
- r = s.api("set-purpose", target="pane", id=pane["id"],
- purpose="guessed:by-the-pane", declared=False)
- check("an in-band purpose is allowed when nothing is declared", r["ok"],
- str(r))
- check("and does not lock the slot",
- not s.pane()["purpose_declared"], str(s.pane()))
+ r = s.api(
+ "set-purpose",
+ target="pane",
+ id=pane["id"],
+ purpose="guessed:by-the-pane",
+ declared=False,
+ )
+ check("an in-band purpose is allowed when nothing is declared", r["ok"], str(r))
+ check(
+ "and does not lock the slot",
+ not s.pane()["purpose_declared"],
+ str(s.pane()),
+ )
def test_json_api():
@@ -213,55 +268,68 @@ def test_json_api():
base = s.pane()["id"]
r = s.api("split", dir="rows", id=base)
- check("split answers with the new pane id", r["ok"] and r["id"] != base,
- str(r))
+ check("split answers with the new pane id", r["ok"] and r["id"] != base, str(r))
new = r["id"]
r = s.api("focus", id=base)
check("focus by id works", r["ok"] and r["id"] == base, str(r))
- check("focus by id is reflected in the layout",
- [p for p in s.panes() if p["focused"]][0]["id"] == base)
+ check(
+ "focus by id is reflected in the layout",
+ [p for p in s.panes() if p["focused"]][0]["id"] == base,
+ )
r = s.api("new-tab", name="build", purpose="project:x.deadbeef")
check("new-tab answers with an id", r["ok"] and r["id"], str(r))
t = [t for t in s.tabs() if t["id"] == r["id"]][0]
- check("new-tab records name and declared purpose",
- t["name"] == "build" and t["purpose"] == "project:x.deadbeef"
- and t["purpose_declared"], str(t))
+ check(
+ "new-tab records name and declared purpose",
+ t["name"] == "build"
+ and t["purpose"] == "project:x.deadbeef"
+ and t["purpose_declared"],
+ str(t),
+ )
r = s.api("snapshot", format="text")
- check("snapshot text comes back as JSON", r["ok"] and "text" in r,
- str(r)[:80])
+ check("snapshot text comes back as JSON", r["ok"] and "text" in r, str(r)[:80])
r = s.api("close", id=new)
check("close by id works", r["ok"], str(r))
- check("unknown commands are refused, not fatal",
- s.api("nope")["error"] == "unknown cmd")
+ check(
+ "unknown commands are refused, not fatal",
+ s.api("nope")["error"] == "unknown cmd",
+ )
r = s.api("focus", id=9999)
- check("bad ids are refused, not fatal", not r["ok"] and s.api("alive")["ok"],
- str(r))
+ check(
+ "bad ids are refused, not fatal",
+ not r["ok"] and s.api("alive")["ok"],
+ str(r),
+ )
# ---- hovering the strip ----------------------------------------------------
-ACCENT = "#ff5fd7" # the fill of the tab you are in, and hover on the rest
-INK = "#141418" # text on that fill
-BRIGHT = "#ffffff" # that text while the pointer is on it
-DIM = "#45454a" # a tab you are not in
+ACCENT = "#ff5fd7" # the fill of the tab you are in, and hover on the rest
+INK = "#141418" # text on that fill
+BRIGHT = "#ffffff" # that text while the pointer is on it
+DIM = "#45454a" # a tab you are not in
def two_tab_layout():
f = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
- f.write('layout {\n tab name="api" {\n pane\n }\n'
- ' tab name="notes" {\n pane\n }\n}\n')
+ f.write(
+ 'layout {\n tab name="api" {\n pane\n }\n tab name="notes" {\n pane\n }\n}\n'
+ )
f.close()
return f.name
def strip_hits(snap):
- return {h["action"]: h for h in snap.hits
- if h["action"].startswith("tab:") or h["action"] == "newtab"}
+ return {
+ h["action"]: h
+ for h in snap.hits
+ if h["action"].startswith("tab:") or h["action"] == "newtab"
+ }
def hover(s, x, y):
@@ -294,11 +362,17 @@ def test_hovering_the_strip_lights_what_is_under_the_pointer():
# The tab you are in keeps its fill and brightens its text; the
# others have no fill to keep and take the accent.
want = BRIGHT if target == active_id else ACCENT
- check(f"hovering {target} lights it",
- fg_of(snap, h) == want, f"{fg_of(snap, h)} wanted {want}")
+ check(
+ f"hovering {target} lights it",
+ fg_of(snap, h) == want,
+ f"{fg_of(snap, h)} wanted {want}",
+ )
others = [(k, fg_of(snap, o)) for k, o in hits.items() if k != target]
- check("and nothing else on the strip",
- all(c in (INK, DIM) for _, c in others), str(others))
+ check(
+ "and nothing else on the strip",
+ all(c in (INK, DIM) for _, c in others),
+ str(others),
+ )
os.unlink(lay)
@@ -312,24 +386,38 @@ def test_weight_says_active_and_colour_says_pointer():
active = hits["tab:" + str(s.tabs()[0]["id"])]
snap = s.snapshot()
- check("the tab you are in is filled, not merely bold",
- fg_of(snap, active) == INK and bg_of(snap, active) == ACCENT,
- f"{fg_of(snap, active)} on {bg_of(snap, active)}")
- check("and still bold", "bold" in attrs_of(snap, active),
- str(attrs_of(snap, active)))
+ check(
+ "the tab you are in is filled, not merely bold",
+ fg_of(snap, active) == INK and bg_of(snap, active) == ACCENT,
+ f"{fg_of(snap, active)} on {bg_of(snap, active)}",
+ )
+ check(
+ "and still bold",
+ "bold" in attrs_of(snap, active),
+ str(attrs_of(snap, active)),
+ )
- inactive = [h for k, h in hits.items()
- if k.startswith("tab:") and h is not active][0]
- check("a tab you are not in has no fill at all",
- bg_of(snap, inactive) is None and fg_of(snap, inactive) == DIM,
- f"{fg_of(snap, inactive)} on {bg_of(snap, inactive)}")
+ inactive = [
+ h for k, h in hits.items() if k.startswith("tab:") and h is not active
+ ][0]
+ check(
+ "a tab you are not in has no fill at all",
+ bg_of(snap, inactive) is None and fg_of(snap, inactive) == DIM,
+ f"{fg_of(snap, inactive)} on {bg_of(snap, inactive)}",
+ )
hover(s, active["x"] + 1, active["y"])
snap = s.snapshot()
- check("hovering it brightens the text",
- fg_of(snap, active) == BRIGHT, str(fg_of(snap, active)))
- check("but it keeps the fill that says you are in it",
- bg_of(snap, active) == ACCENT, str(bg_of(snap, active)))
+ check(
+ "hovering it brightens the text",
+ fg_of(snap, active) == BRIGHT,
+ str(fg_of(snap, active)),
+ )
+ check(
+ "but it keeps the fill that says you are in it",
+ bg_of(snap, active) == ACCENT,
+ str(bg_of(snap, active)),
+ )
os.unlink(lay)
@@ -343,23 +431,29 @@ def test_leaving_the_strip_puts_it_out_and_hover_switches_nothing():
hover(s, other["x"] + 1, other["y"])
s.settle(20)
- check("hovering a tab does not switch to it",
- [t["active"] for t in s.tabs()] ==
- [t["active"] for t in before], str(s.tabs()))
+ check(
+ "hovering a tab does not switch to it",
+ [t["active"] for t in s.tabs()] == [t["active"] for t in before],
+ str(s.tabs()),
+ )
p = s.pane()
hover(s, p["content_x"] + 2, p["content_y"] + 1)
snap = s.snapshot()
- check("nothing on the strip is lit once the pointer leaves it",
- all(fg_of(snap, h) in (INK, DIM) for h in hits.values()),
- str([fg_of(snap, h) for h in hits.values()]))
+ check(
+ "nothing on the strip is lit once the pointer leaves it",
+ all(fg_of(snap, h) in (INK, DIM) for h in hits.values()),
+ str([fg_of(snap, h) for h in hits.values()]),
+ )
# ...but clicking still switches.
s.click(other["x"] + 1, other["y"])
s.settle(20)
- check("clicking the tab you lit is what switches to it",
- [t["active"] for t in s.tabs()] !=
- [t["active"] for t in before], str(s.tabs()))
+ check(
+ "clicking the tab you lit is what switches to it",
+ [t["active"] for t in s.tabs()] != [t["active"] for t in before],
+ str(s.tabs()),
+ )
os.unlink(lay)
@@ -402,8 +496,9 @@ def test_dragging_a_tab_reorders_the_strip():
with Session(SH, cols=80, rows=12) as s:
s.settle()
ids = named_tabs(s, "one", "two", "three")
- check("the order to start", order(s) == ["", "one", "two", "three"],
- str(order(s)))
+ check(
+ "the order to start", order(s) == ["", "one", "two", "three"], str(order(s))
+ )
hits = strip_hits(s.snapshot())
src = hits[f"tab:{ids[3]}"]
@@ -412,8 +507,11 @@ def test_dragging_a_tab_reorders_the_strip():
motion(s, dst["x"] + 1, dst["y"])
release(s, dst["x"] + 1, dst["y"])
- check("the dragged tab lands where it was dropped",
- order(s) == ["", "three", "one", "two"], str(order(s)))
+ check(
+ "the dragged tab lands where it was dropped",
+ order(s) == ["", "three", "one", "two"],
+ str(order(s)),
+ )
check("and you are still in it", active(s) == "three", active(s))
@@ -427,8 +525,11 @@ def test_dragging_past_the_last_tab_puts_it_last():
press(s, src["x"] + 1, src["y"])
motion(s, end["x"] + 1, end["y"])
release(s, end["x"] + 1, end["y"])
- check("dropping past the end means last",
- order(s) == ["one", "two", ""], str(order(s)))
+ check(
+ "dropping past the end means last",
+ order(s) == ["one", "two", ""],
+ str(order(s)),
+ )
def test_dragging_off_the_strip_moves_nothing():
@@ -442,8 +543,7 @@ def test_dragging_off_the_strip_moves_nothing():
press(s, src["x"] + 1, src["y"])
motion(s, 20, 6) # into a pane, well below the strip
release(s, 20, 6)
- check("the order is untouched", order(s) == ["", "one", "two"],
- str(order(s)))
+ check("the order is untouched", order(s) == ["", "one", "two"], str(order(s)))
def test_a_click_that_never_moves_only_switches():
@@ -470,15 +570,17 @@ def test_move_tab_over_the_api_keeps_you_where_you_are():
r = s.api("move-tab", id=ids[3], index=1) # move "c" to the front
check("the api reports ok", r.get("ok") is True, str(r))
- check("the strip is reordered", order(s) == ["c", "", "a", "b"],
- str(order(s)))
- check("and you are still in the tab you were in", active(s) == "a",
- active(s))
+ check("the strip is reordered", order(s) == ["c", "", "a", "b"], str(order(s)))
+ check("and you are still in the tab you were in", active(s) == "a", active(s))
- check("an unknown tab is refused",
- s.api("move-tab", id=999, index=1).get("ok") is False)
- check("and so is an index off the end",
- s.api("move-tab", id=ids[0], index=99).get("ok") is False)
+ check(
+ "an unknown tab is refused",
+ s.api("move-tab", id=999, index=1).get("ok") is False,
+ )
+ check(
+ "and so is an index off the end",
+ s.api("move-tab", id=ids[0], index=99).get("ok") is False,
+ )
def test_double_clicking_a_tab_renames_it():
@@ -494,18 +596,26 @@ def test_double_clicking_a_tab_renames_it():
dbl(s, strip_hits(s.snapshot())[tab2])
snap = s.snapshot()
- check("the editor opens in the tab's own cell",
- "\u2588" in snap.line(1), repr(snap.line(1)))
+ check(
+ "the editor opens in the tab's own cell",
+ "\u2588" in snap.line(1),
+ repr(snap.line(1)),
+ )
s.send("logs")
- check("typing goes into it", "logs\u2588" in s.snapshot().line(1),
- repr(s.snapshot().line(1)))
+ check(
+ "typing goes into it",
+ "logs\u2588" in s.snapshot().line(1),
+ repr(s.snapshot().line(1)),
+ )
s.send(r"\r")
- check("enter keeps it", s.tabs()[1]["name"] == "logs",
- str(s.tabs()))
- check("and the strip shows it", "2:logs" in s.snapshot().line(1),
- repr(s.snapshot().line(1)))
+ check("enter keeps it", s.tabs()[1]["name"] == "logs", str(s.tabs()))
+ check(
+ "and the strip shows it",
+ "2:logs" in s.snapshot().line(1),
+ repr(s.snapshot().line(1)),
+ )
def test_escape_abandons_and_clicking_away_keeps():
@@ -518,23 +628,24 @@ def test_escape_abandons_and_clicking_away_keeps():
dbl(s, strip_hits(s.snapshot())[tab2])
s.send("throwaway")
s.send(r"\x1b")
- check("escape leaves the name alone", s.tabs()[1]["name"] == "",
- str(s.tabs()))
+ check("escape leaves the name alone", s.tabs()[1]["name"] == "", str(s.tabs()))
check("and closes the editor", "\u2588" not in s.snapshot().screen())
dbl(s, strip_hits(s.snapshot())[tab2])
s.send("kept")
s.click(2, 6) # somewhere that is not this tab
- check("clicking away commits, the way leaving a field does",
- s.tabs()[1]["name"] == "kept", str(s.tabs()))
+ check(
+ "clicking away commits, the way leaving a field does",
+ s.tabs()[1]["name"] == "kept",
+ str(s.tabs()),
+ )
# And an empty name gives the tab back to its number.
dbl(s, strip_hits(s.snapshot())[tab2])
for _ in range(len("kept")):
s.send(r"\x7f")
s.send(r"\r")
- check("an empty name clears it", s.tabs()[1]["name"] == "",
- str(s.tabs()))
+ check("an empty name clears it", s.tabs()[1]["name"] == "", str(s.tabs()))
def test_a_tab_and_a_pane_with_the_same_id_are_not_the_same_thing():
@@ -546,26 +657,33 @@ def test_a_tab_and_a_pane_with_the_same_id_are_not_the_same_thing():
with Session(titled, cols=70, rows=12) as s:
s.settle()
s.key("c")
- s.until(lambda sn: any(h["action"].startswith("panetitle:")
- for h in sn.hits))
+ s.until(lambda sn: any(h["action"].startswith("panetitle:") for h in sn.hits))
snap = s.snapshot()
pane_id = s.focused()["id"] # the new tab's pane, not tab 1's
tab_id = s.tabs()[1]["id"]
- check("the ids really do collide", pane_id == tab_id,
- f"pane {pane_id} tab {tab_id}")
+ check(
+ "the ids really do collide",
+ pane_id == tab_id,
+ f"pane {pane_id} tab {tab_id}",
+ )
- title = [h for h in snap.hits
- if h["action"] == f"panetitle:{pane_id}"][0]
+ title = [h for h in snap.hits if h["action"] == f"panetitle:{pane_id}"][0]
strip = strip_hits(snap)[f"tab:{tab_id}"]
s.click(strip["x"] + 1, strip["y"])
s.click(title["x"] + 1, title["y"])
- check("one click on each opens no editor",
- "\u2588" not in s.snapshot().screen(), s.snapshot().screen())
+ check(
+ "one click on each opens no editor",
+ "\u2588" not in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
dbl(s, title)
- check("but two on the pane still open its own",
- "\u2588" in s.snapshot().screen(), s.snapshot().screen())
+ check(
+ "but two on the pane still open its own",
+ "\u2588" in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
if __name__ == "__main__":
diff --git a/tests/test_themes.py b/tests/test_themes.py
index 5e21bc7..9843d89 100644
--- a/tests/test_themes.py
+++ b/tests/test_themes.py
@@ -5,6 +5,7 @@ Mostly a guard: a theme file that has fallen behind the config silently keeps
the compiled-in default for whatever it forgot, which looks like a bug in the
theme rather than a missing line in it.
"""
+
import glob
import os
import re
@@ -34,21 +35,27 @@ def test_each_sets_every_colour_the_config_knows():
want = declared_in_config()
for t in THEMES:
missing = want - set_by(t)
- check(f"{os.path.basename(t)} sets all {len(want)} of them",
- not missing, "missing: " + ", ".join(sorted(missing)))
+ check(
+ f"{os.path.basename(t)} sets all {len(want)} of them",
+ not missing,
+ "missing: " + ", ".join(sorted(missing)),
+ )
def test_each_one_parses_and_reaches_the_screen():
for t in THEMES:
- declared = dict(re.findall(r'^ ([a-z_]+) "(#[0-9a-fA-F]{6})"',
- open(t).read(), re.M))
+ declared = dict(
+ re.findall(r'^ ([a-z_]+) "(#[0-9a-fA-F]{6})"', open(t).read(), re.M)
+ )
with Session(SH, cols=50, rows=10, config=os.path.abspath(t)) as s:
s.settle(30)
snap, p = s.snapshot(), s.pane()
drawn = (snap.style_at(p["x"], p["y"] + 1) or {}).get("fg")
- check(f"{os.path.basename(t)} draws its own frame colour",
- drawn == declared["frame_focus"],
- f"{drawn} != {declared['frame_focus']}")
+ check(
+ f"{os.path.basename(t)} draws its own frame colour",
+ drawn == declared["frame_focus"],
+ f"{drawn} != {declared['frame_focus']}",
+ )
check(f"{os.path.basename(t)} leaves the session alive", s.alive(), "")
diff --git a/tests/test_wide_marks.py b/tests/test_wide_marks.py
index c53df57..91169b7 100644
--- a/tests/test_wide_marks.py
+++ b/tests/test_wide_marks.py
@@ -10,6 +10,7 @@ Assertions here are on *columns*, taken from style runs rather than the text
dump: the dump collapses a wide cell into one character, so measuring a python
string is measuring the wrong thing and would have passed before the fix.
"""
+
import json
import os
import subprocess
@@ -26,8 +27,8 @@ LAYOUT = """layout {
}
"""
-NARROW = "\u2022" # one column
-WIDE = "\U0001f514" # bell emoji, two columns
+NARROW = "\u2022" # one column
+WIDE = "\U0001f514" # bell emoji, two columns
CLUSTER = "\U0001f468\u200d\U0001f469\u200d\U0001f467" # ZWJ family: 18 bytes
@@ -40,11 +41,25 @@ def run(mark, cols=100, rows=14):
# mark claim", which means finding those columns without guessing.
cfg.write('bell_mark "%s"\ntheme { bell "#00ff00" }\n' % mark)
cfg.close()
- p = subprocess.run([BIN, "--script", "--cols", str(cols), "--rows", str(rows),
- "--layout", lay.name, "--", "/bin/sh", "-c", "read x"],
- input=b"settle 250\npanes\nsnapshot json\n",
- capture_output=True, env={**os.environ,
- "SL0PPTY_CONFIG": cfg.name})
+ p = subprocess.run(
+ [
+ BIN,
+ "--script",
+ "--cols",
+ str(cols),
+ "--rows",
+ str(rows),
+ "--layout",
+ lay.name,
+ "--",
+ "/bin/sh",
+ "-c",
+ "read x",
+ ],
+ input=b"settle 250\npanes\nsnapshot json\n",
+ capture_output=True,
+ env={**os.environ, "SL0PPTY_CONFIG": cfg.name},
+ )
os.unlink(lay.name)
os.unlink(cfg.name)
return p.stdout
@@ -70,11 +85,17 @@ def columns_claimed_by_the_mark(mark):
def test_a_mark_claims_exactly_the_columns_it_draws_in():
- for label, mark, want in (("narrow", NARROW, 1), ("wide", WIDE, 2),
- ("two characters", "!!", 2)):
+ for label, mark, want in (
+ ("narrow", NARROW, 1),
+ ("wide", WIDE, 2),
+ ("two characters", "!!", 2),
+ ):
got = columns_claimed_by_the_mark(mark)
- check("%s mark claims %d column(s)" % (label, want), got == want,
- "claimed %d" % got)
+ check(
+ "%s mark claims %d column(s)" % (label, want),
+ got == want,
+ "claimed %d" % got,
+ )
def test_the_mark_is_actually_drawn():
@@ -94,18 +115,19 @@ def test_no_mark_can_put_invalid_utf8_on_the_wire():
ok = True
except UnicodeDecodeError as e:
ok = False
- detail = repr(raw[max(0, e.start - 20):e.start + 12])
- check("%s: output is valid utf-8" % label, ok,
- detail if not ok else "")
+ detail = repr(raw[max(0, e.start - 20) : e.start + 12])
+ check("%s: output is valid utf-8" % label, ok, detail if not ok else "")
def test_widths_come_from_the_terminals_own_table():
"""Not a table of our own: the same one lib-vt uses for pane content, so
chrome and content cannot disagree about how wide something is."""
src = open(os.path.join(os.path.dirname(__file__), "..", "src", "screen.c")).read()
- check("width comes from lib-vt",
- "ghostty_unicode_grapheme_width" in src,
- "screen.c is measuring text some other way")
+ check(
+ "width comes from lib-vt",
+ "ghostty_unicode_grapheme_width" in src,
+ "screen.c is measuring text some other way",
+ )
for name, fn in sorted(list(globals().items())):
diff --git a/tests/test_workspaces.py b/tests/test_workspaces.py
index 8567fba..825f7f7 100644
--- a/tests/test_workspaces.py
+++ b/tests/test_workspaces.py
@@ -9,6 +9,7 @@ session restores membership for free.
The list is derived when it is asked for rather than watched: an answer nobody
remembered cannot be stale.
"""
+
import json
import os
import re
@@ -68,6 +69,7 @@ def session(cfg, argv=None, **kw):
# ---- discovery -------------------------------------------------------------
+
def test_a_project_is_a_layout_file_or_a_git():
"""Both, because `~/dev` with forty checkouts and three layout files would
otherwise be a picker with three rows -- and a `.git` is exactly the project
@@ -77,15 +79,22 @@ def test_a_project_is_a_layout_file_or_a_git():
got = {w["name"]: w for w in s.api("workspaces")["workspaces"]}
check("a directory with a layout file is a project", "api" in got, str(got))
check("and one with a .git is too", "web" in got, str(got))
- check("a directory that is neither is not listed", "notes" not in got,
- str(got))
- check("the declared one names its file",
- got["api"]["layout"].endswith("sl0ppty.layout.kdl"),
- str(got["api"]))
- check("the inferred one has no file to name", got["web"]["layout"] == "",
- str(got["web"]))
- check("and it reports the file's mtime, so drift is derivable outside",
- got["api"]["mtime"] > 0 and got["web"]["mtime"] == 0, str(got))
+ check("a directory that is neither is not listed", "notes" not in got, str(got))
+ check(
+ "the declared one names its file",
+ got["api"]["layout"].endswith("sl0ppty.layout.kdl"),
+ str(got["api"]),
+ )
+ check(
+ "the inferred one has no file to name",
+ got["web"]["layout"] == "",
+ str(got["web"]),
+ )
+ check(
+ "and it reports the file's mtime, so drift is derivable outside",
+ got["api"]["mtime"] > 0 and got["web"]["mtime"] == 0,
+ str(got),
+ )
def test_it_looks_below_the_root_but_never_inside_a_project():
@@ -112,20 +121,26 @@ def test_two_projects_of_the_same_name_are_two_workspaces():
slugs = {w["purpose"] for w in ws}
check("both are listed", len(ws) == 2, str(ws))
check("with different identities", len(slugs) == 2, str(slugs))
- check("and both are named for their directory",
- all(w["purpose"].startswith("project:api.") for w in ws), str(slugs))
+ check(
+ "and both are named for their directory",
+ all(w["purpose"].startswith("project:api.") for w in ws),
+ str(slugs),
+ )
def test_saying_nothing_about_roots_is_said_out_loud():
- """"You have no projects" and "you never said where they are" are different
+ """ "You have no projects" and "you never said where they are" are different
facts, and answering both with an empty list makes the feature look broken."""
with Session(SH) as s:
r = s.api("workspaces")
check("the list is empty", r["workspaces"] == [], str(r))
check("and it says why", r["roots"] is False, str(r))
bad = s.api("open-workspace", name="api")
- check("opening says so too", bad["ok"] is False and "project_roots" in
- bad["error"], str(bad))
+ check(
+ "opening says so too",
+ bad["ok"] is False and "project_roots" in bad["error"],
+ str(bad),
+ )
def test_a_path_outside_every_root_is_refused():
@@ -139,28 +154,40 @@ def test_a_path_outside_every_root_is_refused():
# ---- opening ---------------------------------------------------------------
+
def test_opening_a_project_builds_its_layout_and_tags_the_tab():
base, dev, cfg = roots(api=API_LAYOUT)
with session(cfg) as s:
r = s.api("open-workspace", name="api")
check("it says which tab you landed in", r["tab"] > 0, str(r))
check("and that it made one", r["created"] is True, str(r))
- check("the tab carries the workspace's identity",
- r["purpose"].startswith("project:api."), str(r))
+ check(
+ "the tab carries the workspace's identity",
+ r["purpose"].startswith("project:api."),
+ str(r),
+ )
tab = [t for t in s.tabs() if t["id"] == r["tab"]][0]
check("the tab is named for the project", tab["name"] == "api", str(tab))
- check("and its purpose is declared, so no program can relabel it",
- tab["purpose"] == r["purpose"] and tab["purpose_declared"] is True,
- str(tab))
+ check(
+ "and its purpose is declared, so no program can relabel it",
+ tab["purpose"] == r["purpose"] and tab["purpose_declared"] is True,
+ str(tab),
+ )
panes = [p for p in s.panes() if p["tab_id"] == r["tab"]]
check("the layout's panes are there", len(panes) == 3, str(panes))
tags = {p["purpose"] for p in panes}
- check("carrying the tags the project declared",
- {"agent:main", "shell:scratch"} <= tags, str(tags))
- check("and the expensive one has not started",
- sum(1 for p in panes if p["suspended"]) == 1, str(panes))
+ check(
+ "carrying the tags the project declared",
+ {"agent:main", "shell:scratch"} <= tags,
+ str(tags),
+ )
+ check(
+ "and the expensive one has not started",
+ sum(1 for p in panes if p["suspended"]) == 1,
+ str(panes),
+ )
def test_opening_the_same_project_twice_is_one_workspace():
@@ -172,8 +199,7 @@ def test_opening_the_same_project_twice_is_one_workspace():
before = len(s.tabs())
again = s.api("open-workspace", name="api")
check("the same tab comes back", again["tab"] == first["tab"], str(again))
- check("and it says it did not make one", again["created"] is False,
- str(again))
+ check("and it says it did not make one", again["created"] is False, str(again))
check("no second tab appeared", len(s.tabs()) == before, str(s.tabs()))
@@ -191,13 +217,19 @@ def test_a_project_with_no_layout_opens_as_your_project_layout():
r = s.api("open-workspace", name="web")
panes = [p for p in s.panes() if p["tab_id"] == r["tab"]]
check("the shared layout was used", len(panes) == 2, str(panes))
- check("and its tags came with it",
- any(p["purpose"] == "agent:main" for p in panes), str(panes))
+ check(
+ "and its tags came with it",
+ any(p["purpose"] == "agent:main" for p in panes),
+ str(panes),
+ )
kdl = s.api("dump-layout", tab=r["tab"])["kdl"]
want = os.path.join(dev, "web")
- check("its panes are in the project, not beside the shared file",
- kdl.count(want) == 2, kdl + " want " + want)
+ check(
+ "its panes are in the project, not beside the shared file",
+ kdl.count(want) == 2,
+ kdl + " want " + want,
+ )
def test_a_project_with_nothing_at_all_opens_as_one_shell_in_it():
@@ -217,32 +249,37 @@ def test_opening_suspended_starts_nothing():
with session(cfg) as s:
r = s.api("open-workspace", name="api", suspended=True)
panes = [p for p in s.panes() if p["tab_id"] == r["tab"]]
- check("every pane is laid out and asleep",
- all(p["suspended"] for p in panes) and len(panes) == 3, str(panes))
+ check(
+ "every pane is laid out and asleep",
+ all(p["suspended"] for p in panes) and len(panes) == 3,
+ str(panes),
+ )
def test_a_tab_that_declared_its_own_purpose_keeps_it():
"""Overwriting a declared purpose is the one thing D8 forbids, so such a tab is
honoured and is simply not a member -- and the reply counts it rather than
leaving it to be a surprise later."""
- base, dev, cfg = roots(api='layout {\n'
- ' tab name="a" { pane }\n'
- ' tab name="b" purpose="notes" { pane }\n'
- '}\n')
+ base, dev, cfg = roots(
+ api="layout {\n"
+ ' tab name="a" { pane }\n'
+ ' tab name="b" purpose="notes" { pane }\n'
+ "}\n"
+ )
with session(cfg) as s:
r = s.api("open-workspace", name="api")
check("one tab was adopted", r["tabs"] == 1, str(r))
check("and one was left alone", r["honoured"] == 1, str(r))
purposes = {t["name"]: t["purpose"] for t in s.tabs()}
- check("the declared purpose survived", purposes.get("b") == "notes",
- str(purposes))
+ check(
+ "the declared purpose survived", purposes.get("b") == "notes", str(purposes)
+ )
def test_closing_a_workspace_closes_its_tabs():
- base, dev, cfg = roots(api='layout {\n'
- ' tab name="one" { pane }\n'
- ' tab name="two" { pane }\n'
- '}\n')
+ base, dev, cfg = roots(
+ api='layout {\n tab name="one" { pane }\n tab name="two" { pane }\n}\n'
+ )
with session(cfg) as s:
s.api("open-workspace", name="api")
before = len(s.tabs())
@@ -253,30 +290,44 @@ def test_closing_a_workspace_closes_its_tabs():
# ---- saving ----------------------------------------------------------------
+
def test_saving_a_tab_writes_a_portable_layout_file():
"""The whole of onboarding: arrange a tab in a checkout, press one key, and the
project owns a layout that works on any machine."""
base, dev, cfg = roots(web="git")
with session(cfg) as s:
r = s.api("open-workspace", name="web")
- s.key("\\\\") # split into columns
- s.key("P") # ...and tag the new pane
+ s.key("\\\\") # split into columns
+ s.key("P") # ...and tag the new pane
s.send("service:web\\r")
saved = s.api("save-workspace")
- check("it says where it wrote", saved["path"] ==
- os.path.join(dev, "web", "sl0ppty.layout.kdl"), str(saved))
- check("and that there was nothing there before",
- saved["replaced"] is False, str(saved))
+ check(
+ "it says where it wrote",
+ saved["path"] == os.path.join(dev, "web", "sl0ppty.layout.kdl"),
+ str(saved),
+ )
+ check(
+ "and that there was nothing there before",
+ saved["replaced"] is False,
+ str(saved),
+ )
text = open(saved["path"]).read()
- check("every directory is relative to the project",
- "cwd=\".\"" in text and dev not in text, text)
+ check(
+ "every directory is relative to the project",
+ 'cwd="."' in text and dev not in text,
+ text,
+ )
check("the tag came with it", 'purpose="service:web"' in text, text)
- check("the derived workspace purpose did not",
- "project:" not in text, text)
- check("and it lints as a layout",
- subprocess.run([BIN, "--check", saved["path"]],
- capture_output=True).returncode == 0, text)
+ check("the derived workspace purpose did not", "project:" not in text, text)
+ check(
+ "and it lints as a layout",
+ subprocess.run(
+ [BIN, "--check", saved["path"]], capture_output=True
+ ).returncode
+ == 0,
+ text,
+ )
def test_saving_adopts_an_ad_hoc_tab():
@@ -285,14 +336,19 @@ def test_saving_adopts_an_ad_hoc_tab():
base, dev, cfg = roots(web="git")
with session(cfg) as s:
r = s.api("save-workspace", path=os.path.join(dev, "web"))
- check("it wrote the project's file",
- r["path"].startswith(os.path.join(dev, "web")), str(r))
- check("and the tab now carries the workspace",
- r["purpose"].startswith("project:web."), str(r))
+ check(
+ "it wrote the project's file",
+ r["path"].startswith(os.path.join(dev, "web")),
+ str(r),
+ )
+ check(
+ "and the tab now carries the workspace",
+ r["purpose"].startswith("project:web."),
+ str(r),
+ )
listed = {w["name"]: w for w in s.api("workspaces")["workspaces"]}
check("so the picker shows it open", listed["web"]["tab"] > 0, str(listed))
- check("and it has a layout now", listed["web"]["layout"] != "",
- str(listed))
+ check("and it has a layout now", listed["web"]["layout"] != "", str(listed))
def test_saving_will_not_quietly_replace_a_checked_in_file():
@@ -300,13 +356,13 @@ def test_saving_will_not_quietly_replace_a_checked_in_file():
with session(cfg) as s:
s.api("open-workspace", name="api")
first = s.api("save-workspace")
- check("a project that has a layout is refused", first["ok"] is False,
- str(first))
+ check(
+ "a project that has a layout is refused", first["ok"] is False, str(first)
+ )
check("and told how to mean it", "force" in first["error"], str(first))
forced = s.api("save-workspace", force=True)
check("with force it writes", forced["ok"] is True, str(forced))
- check("and says it replaced something", forced["replaced"] is True,
- str(forced))
+ check("and says it replaced something", forced["replaced"] is True, str(forced))
def test_a_saved_project_defaults_to_suspending_its_commands():
@@ -321,11 +377,11 @@ def test_a_saved_project_defaults_to_suspending_its_commands():
s.api("apply-layout", kdl='layout { tab name="t" { pane command="sleep 60" } }')
t = [x for x in s.tabs() if x["name"] == "t"][0]
r = s.api("save-workspace", path=os.path.join(dev, "web"), tab=t["id"])
- check("the pane with a command is written asleep", r["suspended"] == 1,
- str(r))
+ check("the pane with a command is written asleep", r["suspended"] == 1, str(r))
text = open(r["path"]).read()
- check("so opening it will not start a dev server",
- "suspended=true" in text, text)
+ check(
+ "so opening it will not start a dev server", "suspended=true" in text, text
+ )
def test_setting_a_project_up_by_hand_and_writing_it_down():
@@ -347,33 +403,49 @@ def test_setting_a_project_up_by_hand_and_writing_it_down():
saved = s.api("save-workspace")
text = open(saved["path"]).read()
- check("the dev server is in the file",
- 'command="sleep 300"' in text, text)
+ check("the dev server is in the file", 'command="sleep 300"' in text, text)
check("so is the tailer", 'command="tail -f /dev/null"' in text, text)
- check("both are tagged", 'purpose="service:web"' in text and
- 'purpose="logs:web"' in text, text)
- check("and both are written asleep, so tomorrow lays them out",
- text.count("suspended=true") == 2, text)
+ check(
+ "both are tagged",
+ 'purpose="service:web"' in text and 'purpose="logs:web"' in text,
+ text,
+ )
+ check(
+ "and both are written asleep, so tomorrow lays them out",
+ text.count("suspended=true") == 2,
+ text,
+ )
check("counted in the reply", saved["suspended"] == 2, str(saved))
- check("it still lints", subprocess.run(
- [BIN, "--check", saved["path"]], capture_output=True).returncode == 0,
- text)
+ check(
+ "it still lints",
+ subprocess.run(
+ [BIN, "--check", saved["path"]], capture_output=True
+ ).returncode
+ == 0,
+ text,
+ )
# ...and opening it tomorrow gives the shape with nothing running.
with session(cfg, argv=["/bin/sh"]) as s2:
r = s2.api("open-workspace", name="web")
panes = [p for p in s2.panes() if p["tab_id"] == r["tab"]]
check("the panes come back", len(panes) == 2, str(panes))
- check("carrying their tags",
- {p["purpose"] for p in panes} == {"service:web", "logs:web"},
- str(panes))
- check("laid out and not running",
- all(p["suspended"] for p in panes), str(panes))
+ check(
+ "carrying their tags",
+ {p["purpose"] for p in panes} == {"service:web", "logs:web"},
+ str(panes),
+ )
+ check(
+ "laid out and not running", all(p["suspended"] for p in panes), str(panes)
+ )
s2.api("rerun", id=panes[0]["id"])
s2.settle(150)
again = s2.api("dump-layout", tab=r["tab"])["kdl"]
- check("and starting one runs what was saved, not a shell",
- 'command="sleep 300"' in again, again)
+ check(
+ "and starting one runs what was saved, not a shell",
+ 'command="sleep 300"' in again,
+ again,
+ )
def test_a_saved_workspace_round_trips():
@@ -389,20 +461,29 @@ def test_a_saved_workspace_round_trips():
s.key("P")
s.send("shell:scratch\\r")
saved = s.api("save-workspace")
- before = s.api("dump-layout", tab=first["tab"],
- relative_to=os.path.join(dev, "web"),
- suspend="commands")["kdl"]
+ before = s.api(
+ "dump-layout",
+ tab=first["tab"],
+ relative_to=os.path.join(dev, "web"),
+ suspend="commands",
+ )["kdl"]
s.api("close-workspace", name="web")
again = s.api("open-workspace", name="web")
- after = s.api("dump-layout", tab=again["tab"],
- relative_to=os.path.join(dev, "web"),
- suspend="commands")["kdl"]
+ after = s.api(
+ "dump-layout",
+ tab=again["tab"],
+ relative_to=os.path.join(dev, "web"),
+ suspend="commands",
+ )["kdl"]
# The reopened tab carries the workspace purpose the file does not name.
before_body = re.sub(r' purpose="project:[^"]*"', "", before)
after_body = re.sub(r' purpose="project:[^"]*"', "", after)
- check("what was saved is what comes back", before_body == after_body,
- before_body + "\n--- became ---\n" + after_body)
+ check(
+ "what was saved is what comes back",
+ before_body == after_body,
+ before_body + "\n--- became ---\n" + after_body,
+ )
check("and it says it built it", again["created"] is True, str(again))
@@ -437,38 +518,67 @@ def test_every_verb_has_a_bare_form():
base, dev, cfg = roots(api=API_LAYOUT, web="git")
with session(cfg) as s:
listed = bare_list(s, "workspaces")
- check("workspaces lists every project with its marker",
- "api" in listed and "layout" in listed and ".git" in listed,
- repr(listed))
+ check(
+ "workspaces lists every project with its marker",
+ "api" in listed and "layout" in listed and ".git" in listed,
+ repr(listed),
+ )
- check("open-workspace says what it did",
- "opened tab" in bare1(s, "open-workspace api"), "open")
- check("and it really opened one",
- any(t["name"] == "api" for t in s.tabs()), str(s.tabs()))
- check("asking again focuses rather than building",
- "focused tab" in bare1(s, "open-workspace api"), "again")
+ check(
+ "open-workspace says what it did",
+ "opened tab" in bare1(s, "open-workspace api"),
+ "open",
+ )
+ check(
+ "and it really opened one",
+ any(t["name"] == "api" for t in s.tabs()),
+ str(s.tabs()),
+ )
+ check(
+ "asking again focuses rather than building",
+ "focused tab" in bare1(s, "open-workspace api"),
+ "again",
+ )
# Saving `web` from `api`'s tab would leave one tab named for one project
# and carrying the other's purpose. Refused, not obeyed.
wrong = bare1(s, "save-workspace " + os.path.join(dev, "web"))
- check("a workspace tab cannot be saved into another project",
- "another project's workspace" in wrong, repr(wrong))
+ check(
+ "a workspace tab cannot be saved into another project",
+ "another project's workspace" in wrong,
+ repr(wrong),
+ )
# ...and `web`, whose own tab is not open, is saved by naming its tab.
bare1(s, "open-workspace web")
wrote = bare1(s, "save-workspace")
- check("save-workspace names the file it wrote",
- "sl0ppty.layout.kdl" in wrote, repr(wrote))
- check("and it wrote the project it was in",
- os.path.join(dev, "web") in wrote, repr(wrote))
+ check(
+ "save-workspace names the file it wrote",
+ "sl0ppty.layout.kdl" in wrote,
+ repr(wrote),
+ )
+ check(
+ "and it wrote the project it was in",
+ os.path.join(dev, "web") in wrote,
+ repr(wrote),
+ )
- check("close-workspace counts what went",
- "closed 1 tab" in bare1(s, "close-workspace api"), "close")
- check("and the tab is gone",
- not any(t["name"] == "api" for t in s.tabs()), str(s.tabs()))
+ check(
+ "close-workspace counts what went",
+ "closed 1 tab" in bare1(s, "close-workspace api"),
+ "close",
+ )
+ check(
+ "and the tab is gone",
+ not any(t["name"] == "api" for t in s.tabs()),
+ str(s.tabs()),
+ )
- check("a project nobody has says so",
- "no project" in bare1(s, "open-workspace nope"), "missing")
+ check(
+ "a project nobody has says so",
+ "no project" in bare1(s, "open-workspace nope"),
+ "missing",
+ )
def test_a_dumped_session_restores_workspace_membership():
@@ -483,20 +593,25 @@ def test_a_dumped_session_restores_workspace_membership():
f.write(whole)
with Session(SH, config=cfg, layout=restored, cols=100, rows=30) as s2:
listed = {w["name"]: w for w in s2.api("workspaces")["workspaces"]}
- check("the restored session knows the workspace is open",
- listed["api"]["tab"] > 0, str(listed))
+ check(
+ "the restored session knows the workspace is open",
+ listed["api"]["tab"] > 0,
+ str(listed),
+ )
# ---- the picker ------------------------------------------------------------
+
def test_the_picker_lists_projects_and_opens_one():
base, dev, cfg = roots(api=API_LAYOUT, web="git")
with session(cfg, cols=96, rows=24) as s:
s.key("w")
screen = s.snapshot().screen()
check("it is titled as projects", "projects" in screen, screen)
- check("a declared project shows its file",
- "sl0ppty.layout.kdl" in screen, screen)
+ check(
+ "a declared project shows its file", "sl0ppty.layout.kdl" in screen, screen
+ )
check("and one without says so", "no layout" in screen, screen)
s.send("web")
@@ -505,8 +620,11 @@ def test_the_picker_lists_projects_and_opens_one():
s.send("\\r")
tabs = s.tabs()
check("enter opens it", any(t["name"] == "web" for t in tabs), str(tabs))
- check("the picker is gone", "projects" not in s.snapshot().screen(),
- s.snapshot().screen())
+ check(
+ "the picker is gone",
+ "projects" not in s.snapshot().screen(),
+ s.snapshot().screen(),
+ )
def test_the_picker_says_when_there_is_nowhere_to_look():
@@ -524,8 +642,7 @@ def test_the_actions_are_in_the_palette():
s.send("project")
screen = s.snapshot().screen()
check("going to one is listed", "go to a project" in screen, screen)
- check("and saving one is too", "save this tab as a layout" in screen,
- screen)
+ check("and saving one is too", "save this tab as a layout" in screen, screen)
def test_saving_from_the_keyboard_says_what_it_did():
@@ -536,9 +653,11 @@ def test_saving_from_the_keyboard_says_what_it_did():
screen = s.snapshot().screen()
check("the toast names the file", "sl0ppty.layout.kdl" in screen, screen)
check("and counts what it wrote", "1 pane" in screen, screen)
- check("the file is there",
- os.path.exists(os.path.join(dev, "web", "sl0ppty.layout.kdl")),
- screen)
+ check(
+ "the file is there",
+ os.path.exists(os.path.join(dev, "web", "sl0ppty.layout.kdl")),
+ screen,
+ )
if __name__ == "__main__":
diff --git a/tests/test_zoom.py b/tests/test_zoom.py
index d4dca17..43cc545 100644
--- a/tests/test_zoom.py
+++ b/tests/test_zoom.py
@@ -6,6 +6,7 @@ its tab, you said so. So the things worth checking are that it survives the
layout being recomputed, that it cannot outlive the pane it names, and that it
is per tab.
"""
+
import os
import re
import subprocess
@@ -22,8 +23,7 @@ def mark(name):
`--dump-config`, which is generated from the code, rather than written down
here -- a test that pins a glyph starts failing the day somebody picks a
nicer one, and says nothing useful when it does."""
- out = subprocess.run([BIN, "--dump-config"], capture_output=True,
- text=True).stdout
+ out = subprocess.run([BIN, "--dump-config"], capture_output=True, text=True).stdout
m = re.search(name + r' "(.+)"', out)
return m.group(1) if m else "?"
@@ -66,17 +66,17 @@ def test_zoom_fills_the_tab_and_puts_it_back():
s.send(r"\x01z")
s.settle(20)
- check("zooming leaves one pane on screen", len(visible(s)) == 1,
- str(s.panes()))
- check("but does not close the other", len(s.panes()) == 2,
- str(s.panes()))
- check("and the zoomed pane has the whole tab",
- visible(s)[0]["w"] > 80, str(visible(s)[0]))
+ check("zooming leaves one pane on screen", len(visible(s)) == 1, str(s.panes()))
+ check("but does not close the other", len(s.panes()) == 2, str(s.panes()))
+ check(
+ "and the zoomed pane has the whole tab",
+ visible(s)[0]["w"] > 80,
+ str(visible(s)[0]),
+ )
s.send(r"\x01z")
s.settle(20)
- check("unzooming puts them both back", len(visible(s)) == 2,
- str(s.panes()))
+ check("unzooming puts them both back", len(visible(s)) == 2, str(s.panes()))
def test_the_button_toggles_and_says_which_way():
@@ -92,17 +92,22 @@ def test_the_button_toggles_and_says_which_way():
s.click(b["x"] + 1, b["y"])
s.settle(20)
check("clicking it zooms", len(visible(s)) == 1, str(s.panes()))
- check("and the mark says so", ZOOM_ON in s.snapshot().line(2),
- repr(s.snapshot().line(2)[-14:]))
+ check(
+ "and the mark says so",
+ ZOOM_ON in s.snapshot().line(2),
+ repr(s.snapshot().line(2)[-14:]),
+ )
# The button moved: the pane it belongs to just became the whole tab.
b = button(s, "zoom")
s.click(b["x"] + 1, b["y"])
s.settle(20)
- check("clicking it again puts it back", len(visible(s)) == 2,
- str(s.panes()))
- check("and the mark goes back too", ZOOM in s.snapshot().line(2),
- repr(s.snapshot().line(2)[-14:]))
+ check("clicking it again puts it back", len(visible(s)) == 2, str(s.panes()))
+ check(
+ "and the mark goes back too",
+ ZOOM in s.snapshot().line(2),
+ repr(s.snapshot().line(2)[-14:]),
+ )
def test_the_close_button_closes_that_pane():
@@ -119,8 +124,11 @@ def test_the_close_button_closes_that_pane():
s.click(b["x"] + 1, b["y"])
s.settle(20)
left = [p["id"] for p in s.panes()]
- check("it closes the pane whose frame it is on",
- victim not in left and len(left) == 1, str(left))
+ check(
+ "it closes the pane whose frame it is on",
+ victim not in left and len(left) == 1,
+ str(left),
+ )
def test_a_zoom_cannot_outlive_its_pane():
@@ -134,16 +142,24 @@ def test_a_zoom_cannot_outlive_its_pane():
s.api("close", id=[p for p in s.panes() if p["focused"]][0]["id"])
s.settle(20)
- check("closing the zoomed pane leaves the tab usable",
- len(visible(s)) == 1 and len(s.panes()) == 1, str(s.panes()))
- check("and the survivor is not stuck hidden",
- not s.panes()[0]["hidden"], str(s.panes()))
+ check(
+ "closing the zoomed pane leaves the tab usable",
+ len(visible(s)) == 1 and len(s.panes()) == 1,
+ str(s.panes()),
+ )
+ check(
+ "and the survivor is not stuck hidden",
+ not s.panes()[0]["hidden"],
+ str(s.panes()),
+ )
def test_zoom_is_per_tab():
lay = tempfile.NamedTemporaryFile("w", suffix=".kdl", delete=False)
- lay.write('layout {\n tab name="a" {\n pane\n pane\n }\n'
- ' tab name="b" {\n pane\n pane\n }\n}\n')
+ lay.write(
+ 'layout {\n tab name="a" {\n pane\n pane\n }\n'
+ ' tab name="b" {\n pane\n pane\n }\n}\n'
+ )
lay.close()
with Session(SH, cols=90, rows=18, layout=lay.name) as s:
s.settle(20)
@@ -154,13 +170,19 @@ def test_zoom_is_per_tab():
tabs = s.tabs()
s.api("select-tab", id=tabs[1]["id"])
s.settle(20)
- check("tab b is not, because zoom belongs to a tab",
- len(visible(s)) == 2, str(s.panes()))
+ check(
+ "tab b is not, because zoom belongs to a tab",
+ len(visible(s)) == 2,
+ str(s.panes()),
+ )
s.api("select-tab", id=tabs[0]["id"])
s.settle(20)
- check("and tab a is still zoomed when you come back",
- len(visible(s)) == 1, str(s.panes()))
+ check(
+ "and tab a is still zoomed when you come back",
+ len(visible(s)) == 1,
+ str(s.panes()),
+ )
os.unlink(lay.name)
@@ -169,27 +191,37 @@ def test_the_status_line_says_zoomed():
s.settle(20)
s.api("split", dir="cols")
s.settle(20)
- check("nothing said while it is not", "zoomed" not in s.snapshot().text[-2],
- repr(s.snapshot().text[-2]))
+ check(
+ "nothing said while it is not",
+ "zoomed" not in s.snapshot().text[-2],
+ repr(s.snapshot().text[-2]),
+ )
s.send(r"\x01z")
s.settle(20)
- check("a zoomed pane says so", "zoomed" in s.snapshot().text[-2],
- repr(s.snapshot().text[-2]))
+ check(
+ "a zoomed pane says so",
+ "zoomed" in s.snapshot().text[-2],
+ repr(s.snapshot().text[-2]),
+ )
def test_the_buttons_can_be_turned_off():
conf = cfg("pane_buttons false\n")
with Session(SH, cols=90, rows=18, config=conf) as s:
s.settle(20)
- hits = [h for h in s.snapshot().hits
- if h["action"].startswith(("zoom:", "close:"))]
+ hits = [
+ h for h in s.snapshot().hits if h["action"].startswith(("zoom:", "close:"))
+ ]
check("pane_buttons false draws none", hits == [], str(hits))
# ...but the verb is still there from the keyboard: the setting is
# about the affordance, not about zooming.
s.send(r"\x01z")
s.settle(20)
- check("while the keybinding still zooms",
- "zoomed" in s.snapshot().text[-2], repr(s.snapshot().text[-2]))
+ check(
+ "while the keybinding still zooms",
+ "zoomed" in s.snapshot().text[-2],
+ repr(s.snapshot().text[-2]),
+ )
os.unlink(conf)