diff options
| author | user <user@clank> | 2026-08-17 14:36:59 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-17 14:36:59 +0200 |
| commit | c0022776d0912042819e4ea330cf7491a8196b45 (patch) | |
| tree | 3c5d547f273d8304e7cd6834fd5a2c730a495064 /tests/test_clipboard.py | |
| parent | sl0ppty: ruff formats the Python, and sorts its imports (diff) | |
| download | slosh-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/test_clipboard.py')
| -rw-r--r-- | tests/test_clipboard.py | 287 |
1 files changed, 193 insertions, 94 deletions
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__": |
