summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/e2e_harness.zig32
-rw-r--r--test/snapshot.zig399
-rw-r--r--test/snapshots/chordcut.snap4
-rw-r--r--test/snapshots/cursorline.snap2
-rw-r--r--test/snapshots/dump.snap9
-rw-r--r--test/snapshots/edit.snap4
-rw-r--r--test/snapshots/exec-bigdrag.snap6
-rw-r--r--test/snapshots/exec.snap2
-rw-r--r--test/snapshots/fileedit.snap6
-rw-r--r--test/snapshots/find.snap2
-rw-r--r--test/snapshots/fsearch.snap4
-rw-r--r--test/snapshots/hscroll.snap2
-rw-r--r--test/snapshots/images.snap2
-rw-r--r--test/snapshots/layout-close.snap4
-rw-r--r--test/snapshots/layout-open.snap4
-rw-r--r--test/snapshots/leader.snap4
-rw-r--r--test/snapshots/load.snap4
-rw-r--r--test/snapshots/look-center.snap2
-rw-r--r--test/snapshots/look-dir.snap2
-rw-r--r--test/snapshots/look-file-1col.snap2
-rw-r--r--test/snapshots/look-file.snap2
-rw-r--r--test/snapshots/lookloaded.snap2
-rw-r--r--test/snapshots/lookpanes.snap2
-rw-r--r--test/snapshots/modal.snap4
-rw-r--r--test/snapshots/reflow.snap2
-rw-r--r--test/snapshots/restore.golden2
-rw-r--r--test/snapshots/rsearch.snap2
-rw-r--r--test/snapshots/shrinktag.snap2
-rw-r--r--test/snapshots/syntax.snap2
-rw-r--r--test/snapshots/tag.snap6
-rw-r--r--test/snapshots/tagcmd.snap6
-rw-r--r--test/snapshots/tagnav.golden4
-rw-r--r--test/snapshots/tagnav.snap4
-rw-r--r--test/snapshots/tagpath.snap2
-rw-r--r--test/snapshots/tinywin.snap10
-rw-r--r--test/snapshots/ttylook.snap2
-rw-r--r--test/snapshots/yankpaste.snap2
37 files changed, 445 insertions, 107 deletions
diff --git a/test/e2e_harness.zig b/test/e2e_harness.zig
index 9099f13e..eb4c7a2f 100644
--- a/test/e2e_harness.zig
+++ b/test/e2e_harness.zig
@@ -15,7 +15,9 @@ extern "c" fn forkpty(
extern "c" fn execvp(file: [*:0]const u8, argv: [*:null]const ?[*:0]const u8) c_int;
extern "c" fn _exit(status: c_int) noreturn;
-fn nowMs() i64 {
+/// Monotonic milliseconds; the harness's only clock (std.time lost
+/// milliTimestamp in 0.16).
+pub fn nowMs() i64 {
var ts: libc.timespec = undefined;
_ = libc.clock_gettime(.MONOTONIC, &ts);
return @as(i64, @intCast(ts.sec)) * 1000 + @divFloor(@as(i64, @intCast(ts.nsec)), 1_000_000);
@@ -133,6 +135,34 @@ pub const Harness = struct {
if (self.trace) self.traceScreen("pump");
}
+ /// Wait up to `ms` for the app to say something, feed whatever arrived to
+ /// the emulator, and report whether anything did. `ms` = 0 polls. This is
+ /// the event-driven half of `pump`: a caller that only cares about the
+ /// SETTLED grid sleeps in poll until the app actually writes, instead of
+ /// burning a fixed window per check (see snapshot.zig's waitStable).
+ pub fn pumpOnce(self: *Harness, ms: i64) !bool {
+ var fds = [_]posix.pollfd{.{ .fd = self.master, .events = posix.POLL.IN, .revents = 0 }};
+ const ready = posix.poll(&fds, @intCast(@max(0, ms))) catch 0;
+ if (ready == 0 or (fds[0].revents & posix.POLL.IN) == 0) return false;
+ var buf: [4096]u8 = undefined;
+ const n = posix.read(self.master, &buf) catch return false;
+ if (n == 0) return false;
+ self.raw.appendSlice(self.gpa, buf[0..n]) catch {};
+ self.stream.nextSlice(buf[0..n]);
+ return true;
+ }
+
+ /// Bytes sitting in the pty that nobody has picked up — keystrokes the app
+ /// has not read yet, or output we have not read yet (the master counts both
+ /// directions). Zero is the proof that the app has actually CONSUMED the
+ /// input just sent: without it, an app still asleep in its event loop looks
+ /// exactly like an app that has finished. See snapshot.zig's waitStable.
+ pub fn pending(self: *Harness) usize {
+ var n: c_int = 0;
+ if (posix.system.ioctl(self.master, posix.T.FIONREAD, @intFromPtr(&n)) != 0) return 0;
+ return if (n > 0) @intCast(n) else 0;
+ }
+
/// Resize both the pty (SIGWINCH to the app) and our terminal (grid reflows
/// to the new size). Harness terminal first so it's ready for the app's
/// re-render at the new size.
diff --git a/test/snapshot.zig b/test/snapshot.zig
index f1ebd941..31f2c8ea 100644
--- a/test/snapshot.zig
+++ b/test/snapshot.zig
@@ -6,6 +6,23 @@
// zig build snap run all snapshots/*.snap, diff goldens
// zig build snap -- --update regenerate goldens from this binary
// zig build snap -- snapshots/boot.snap one script
+// zig build snap -- --jobs=1 serial (default: 1.25 per cpu)
+// zig build snap -- --trace-stable show what every `stable` waited on
+//
+// Scripts run one per forked child, in parallel; each child's report is held
+// in a temp file and printed in script order at the end. The runner takes its
+// own lockfile, so `zig build snap` never needs an `flock` wrapper: a second
+// run waits for the first instead of trampling the fixed /tmp paths that the
+// goldens record verbatim. A script that fails in the parallel pass is re-run
+// alone before it counts as a failure.
+//
+// `stable` no longer burns its wall-clock window: it returns as soon as the
+// app is provably done — screen unchanged, pty empty, and every process it
+// forked asleep (see waitStable). That, the fan-out, and building the vendored
+// C optimized (build.zig's c_optimize: 90% of a Debug startup used to be
+// tree-sitter's query analyser at -O0) took the suite from 7m54s to ~4s. The
+// app now reaches first paint in ~25ms, so what is left is mostly real work:
+// bash spawns, their output, and the 40 `settle`s after `key esc`.
//
// Script commands (line-oriented, # comments):
// file <name> <content> create file in the script's cwd (before start)
@@ -18,8 +35,11 @@
// text <literal> send literal bytes
// send <escaped> send with \e \r \n \t \\ \xNN escapes
// key <names...> enter esc tab bs space up down left right home end
-// pgup pgdn c-<ch> a-<ch> (after esc: settle >=600ms,
-// or vaxis parses the next byte as alt-<ch>)
+// pgup pgdn c-<ch> a-<ch> (after esc: settle, or
+// vaxis reads the next byte in the same batch and
+// parses it as alt-<ch>. The scripts said 700ms and
+// it cost the suite 28s; there is no timer in vaxis
+// to outlast — 20ms passes, they use 100ms)
// press|release <left|middle|right> <col> <row> SGR mouse, 1-based
// drag <btn> <col> <row> motion with button held
// motion <col> <row> button-less motion (hover)
@@ -44,6 +64,9 @@ extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int
extern "c" fn execvp(file: [*:0]const u8, argv: [*:null]const ?[*:0]const u8) c_int;
const SNAP_BASE = "/tmp/pardes-snap";
+var trace_stable = false;
+/// set for the serial re-run pass; children widen their probes (see beCareful)
+var careful = false;
const gpa = std.heap.page_allocator;
pub fn main(init: std.process.Init) !void {
@@ -60,17 +83,27 @@ pub fn main(init: std.process.Init) !void {
var exe: ?[]const u8 = null;
var update = false;
+ // Slight oversubscription: a script spends most of its life waiting on a
+ // shell, not computing. Measured on 16 cores: 16 jobs 4.4s, 20 jobs 3.9s,
+ // 24 jobs 3.7s but losing the idle-probe bet often enough that the re-run
+ // pass costs more than the extra parallelism saves.
+ const cpus = std.Thread.getCpuCount() catch 4;
+ var jobs: usize = cpus + cpus / 4;
var scripts: std.ArrayList([]const u8) = .empty;
for (args[1..]) |a| {
if (std.mem.eql(u8, a, "--update")) {
update = true;
+ } else if (std.mem.eql(u8, a, "--trace-stable")) {
+ trace_stable = true;
+ } else if (std.mem.startsWith(u8, a, "--jobs=")) {
+ jobs = @max(1, try std.fmt.parseInt(usize, a["--jobs=".len..], 10));
} else if (exe == null) {
exe = try absPath(arena, orig_cwd, a);
} else {
try scripts.append(arena, try absPath(arena, orig_cwd, a));
}
}
- if (exe == null) fatal("usage: pardes-snap <exe> [scripts...] [--update]", .{});
+ if (exe == null) fatal("usage: pardes-snap <exe> [scripts...] [--update] [--jobs=N]", .{});
const exe_z = try arena.dupeZ(u8, exe.?);
if (scripts.items.len == 0) {
@@ -83,34 +116,49 @@ pub fn main(init: std.process.Init) !void {
}
if (scripts.items.len == 0) fatal("no .snap scripts found", .{});
+ try mkdir(arena, SNAP_BASE, true);
+ // one run at a time (this is the flock that used to be typed by hand)
+ _ = lockOrWait(SNAP_BASE ++ "/.run.lock", "another pardes-snap run");
+
+ const codes = try arena.alloc(u8, scripts.items.len);
+ try runBatch(arena, exe_z, toBatch(arena, scripts.items), update, jobs, codes);
+
+ // A failure in the parallel pass is more often contention than regression:
+ // the app can lose a race with a kworker while 20 siblings fight for the
+ // cpu. So a failure is re-run alone before it counts — first at the normal
+ // probe settings, which is usually all an idle machine needs, and only if
+ // it fails AGAIN with the probes widened (beCareful), which is slow but
+ // decides. A wholesale failure is not flakiness, so past a handful there is
+ // nothing to re-check.
+ if (jobs > 1) {
+ var round: usize = 0;
+ while (round < 2) : (round += 1) {
+ var retry: std.ArrayList(usize) = .empty;
+ for (codes, 0..) |c, i| if (c != 0) try retry.append(arena, i);
+ if (retry.items.len == 0 or retry.items.len > 10) break;
+ const paths = try arena.alloc(Idx, retry.items.len);
+ for (retry.items, paths, 0..) |i, *p, n| p.* = .{ .path = scripts.items[i], .code_idx = n };
+ std.debug.print("re-running {d} failed script(s) serially{s}:", .{
+ paths.len,
+ if (round == 0) "" else " with wider settle probes",
+ });
+ for (paths) |p| std.debug.print(" {s}", .{std.fs.path.stem(p.path)});
+ std.debug.print("\n", .{});
+ careful = round > 0;
+ const recodes = try arena.alloc(u8, paths.len);
+ try runBatch(arena, exe_z, paths, update, 1, recodes);
+ for (retry.items, recodes) |i, c| codes[i] = c;
+ }
+ }
+
var failed: usize = 0;
- for (scripts.items) |script_path| {
- const stem = std.fs.path.stem(script_path);
- const base_path = script_path[0 .. script_path.len - ".snap".len];
- const golden_path = try std.fmt.allocPrint(arena, "{s}.golden", .{base_path});
- const out = runScript(arena, exe_z, script_path, stem) catch |e| {
- std.debug.print("FAIL {s}: script error {s}\n", .{ stem, @errorName(e) });
+ for (scripts.items, codes) |script_path, code| {
+ const report = readFileAlloc(arena, reportPath(arena, script_path) catch "") catch "";
+ std.debug.print("{s}", .{report});
+ if (code != 0) {
failed += 1;
- continue;
- };
- if (update) {
- try eh.writeFile(try arena.dupeZ(u8, golden_path), out);
- std.debug.print("UPDATED {s} ({d} bytes)\n", .{ stem, out.len });
- } else {
- const golden = readFileAlloc(arena, golden_path) catch {
- std.debug.print("FAIL {s}: no golden (run with -- --update)\n", .{stem});
- failed += 1;
- continue;
- };
- if (std.mem.eql(u8, golden, out)) {
- std.debug.print("PASS {s}\n", .{stem});
- } else {
- const actual_path = try std.fmt.allocPrint(arena, "{s}.actual", .{base_path});
- try eh.writeFile(try arena.dupeZ(u8, actual_path), out);
- std.debug.print("FAIL {s}: differs from golden (actual written to {s})\n", .{ stem, actual_path });
- printFirstDiff(golden, out);
- failed += 1;
- }
+ if (report.len == 0)
+ std.debug.print("FAIL {s}: child died with no report\n", .{std.fs.path.stem(script_path)});
}
}
if (failed > 0) {
@@ -120,6 +168,127 @@ pub fn main(init: std.process.Init) !void {
std.debug.print("all {d} snapshot scripts ok\n", .{scripts.items.len});
}
+/// Run `scripts` `jobs` at a time, one forked child each, filling `codes` with
+/// their exit codes. Forking rather than threading because runScript owns
+/// process-global state — chdir into its workdir, HOME/TERM/PARDES_DUMP in the
+/// environment — which siblings must not see. Each child's report goes to its
+/// own file and is replayed in script order by the caller, so parallelism
+/// never interleaves a diff.
+// Scripts run in the order given — alphabetical. Sorting longest-first (from
+// the durations the previous run left in its reports) is the textbook makespan
+// fix and it measured WORSE here: 3.0s when clean, but bunching the heavy
+// scripts at t=0 makes them contend, the idle probes lose their bet, and the
+// re-run pass costs more than the tail ever did (13-20s runs). Left alone.
+const Idx = struct { path: []const u8, code_idx: usize };
+
+fn toBatch(arena: std.mem.Allocator, scripts: []const []const u8) []const Idx {
+ const out = arena.alloc(Idx, scripts.len) catch return &.{};
+ for (scripts, out, 0..) |path, *o, i| o.* = .{ .path = path, .code_idx = i };
+ return out;
+}
+
+fn runBatch(
+ arena: std.mem.Allocator,
+ exe_z: [:0]const u8,
+ scripts: []const Idx,
+ update: bool,
+ jobs: usize,
+ codes: []u8,
+) !void {
+ const Job = struct { pid: libc.pid_t, idx: usize };
+ var running: std.ArrayList(Job) = .empty;
+ defer running.deinit(arena);
+ var next: usize = 0;
+ while (next < scripts.len or running.items.len > 0) {
+ while (next < scripts.len and running.items.len < jobs) : (next += 1) {
+ const idx = next;
+ const pid = libc.fork();
+ if (pid == 0) std.process.exit(runOne(arena, exe_z, scripts[idx].path, update));
+ if (pid < 0) fatal("fork failed", .{});
+ try running.append(arena, .{ .pid = pid, .idx = scripts[idx].code_idx });
+ }
+ var status: c_int = 0;
+ const pid = libc.waitpid(-1, &status, 0);
+ for (running.items, 0..) |j, i| {
+ if (j.pid != pid) continue;
+ // a killed child (crash in the harness itself) counts as a failure
+ codes[j.idx] = if (status == 0) 0 else 1;
+ _ = running.swapRemove(i);
+ break;
+ }
+ }
+}
+
+/// One script, in its own process: run it, then diff or update its golden.
+/// Everything it prints lands in its report file (dup2'd over stdout/stderr).
+/// Returns the child's exit code.
+fn runOne(arena: std.mem.Allocator, exe_z: [:0]const u8, script_path: []const u8, update: bool) u8 {
+ if (careful) beCareful();
+ const stem = std.fs.path.stem(script_path);
+ const report_z = arena.dupeZ(u8, reportPath(arena, script_path) catch return 1) catch return 1;
+ const fd = libc.open(report_z, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, @as(libc.mode_t, 0o644));
+ if (fd >= 0) {
+ _ = libc.dup2(fd, 1);
+ _ = libc.dup2(fd, 2);
+ _ = libc.close(fd);
+ }
+ const base_path = script_path[0 .. script_path.len - ".snap".len];
+ const golden_path = std.fmt.allocPrint(arena, "{s}.golden", .{base_path}) catch return 1;
+ const golden: ?[]u8 = readFileAlloc(arena, golden_path) catch null;
+ const started = eh.nowMs();
+ const out = runScript(arena, exe_z, script_path, stem) catch |e| {
+ std.debug.print("FAIL {s}: script error {s}\n", .{ stem, @errorName(e) });
+ return 1;
+ };
+ const took = eh.nowMs() - started;
+ if (update) {
+ eh.writeFile(arena.dupeZ(u8, golden_path) catch return 1, out) catch return 1;
+ std.debug.print("UPDATED {s} ({d} bytes)\n", .{ stem, out.len });
+ return 0;
+ }
+ if (golden == null) {
+ std.debug.print("FAIL {s}: no golden (run with -- --update)\n", .{stem});
+ return 1;
+ }
+ if (std.mem.eql(u8, golden.?, out)) {
+ std.debug.print("PASS {s} ({d}ms)\n", .{ stem, took });
+ return 0;
+ }
+ const actual_path = std.fmt.allocPrint(arena, "{s}.actual", .{base_path}) catch return 1;
+ eh.writeFile(arena.dupeZ(u8, actual_path) catch return 1, out) catch {};
+ std.debug.print("FAIL {s}: differs from golden (actual written to {s})\n", .{ stem, actual_path });
+ printFirstDiff(golden.?, out);
+ return 1;
+}
+
+fn reportPath(arena: std.mem.Allocator, script_path: []const u8) ![]const u8 {
+ return std.fmt.allocPrint(arena, "{s}/{s}.report", .{ SNAP_BASE, std.fs.path.stem(script_path) });
+}
+
+/// Exclusive flock on `path`, announcing the wait if someone else holds it.
+/// The fd is deliberately leaked: the lock lives until the process exits.
+///
+/// CLOEXEC is load-bearing. flock lives on the open file description, so an
+/// inherited fd keeps the lock — and this process forks a pty full of app and
+/// shell processes that can outlive it. Without CLOEXEC an orphaned bash holds
+/// the lock forever and every later script blocks in flock().
+fn lockOrWait(path: [*:0]const u8, what: []const u8) libc.fd_t {
+ const fd = libc.open(path, .{ .ACCMODE = .WRONLY, .CREAT = true, .CLOEXEC = true }, @as(libc.mode_t, 0o644));
+ if (fd < 0) fatal("cannot open lockfile {s}", .{path});
+ if (libc.flock(fd, std.posix.LOCK.EX | std.posix.LOCK.NB) == 0) return fd;
+ // polled rather than a blocking flock so a stuck holder is reported instead
+ // of hanging the run until someone notices. The cap clears a full serial
+ // run (--jobs=1 is ~6 minutes) with room to spare.
+ std.debug.print("waiting for {s}...\n", .{what});
+ var waited_ms: i64 = 0;
+ while (waited_ms < 600_000) : (waited_ms += 100) {
+ var none: [0]std.posix.pollfd = .{};
+ _ = std.posix.poll(&none, 100) catch {};
+ if (libc.flock(fd, std.posix.LOCK.EX | std.posix.LOCK.NB) == 0) return fd;
+ }
+ fatal("gave up after 10 minutes waiting for {s} ({s})", .{ what, path });
+}
+
fn runScript(arena: std.mem.Allocator, exe_z: [:0]const u8, script_path: []const u8, stem: []const u8) ![]u8 {
const src = try readFileAlloc(arena, script_path);
@@ -138,10 +307,14 @@ fn runScript(arena: std.mem.Allocator, exe_z: [:0]const u8, script_path: []const
_ = setenv("HOME", try arena.dupeZ(u8, home), 1);
_ = setenv("TERM", "xterm-256color", 1);
_ = setenv("LC_ALL", "C", 1);
- // pin the dump path: live runs use a timestamped file under the user data
+ // Pin the dump path: live runs use a timestamped file under the user data
// dir (dump.outPath), which would make the dump/load/restore goldens
- // nondeterministic
- _ = setenv("PARDES_DUMP", "/tmp/pardes-dump.zon", 1);
+ // nondeterministic. Per-SCRIPT, not one shared /tmp/pardes-dump.zon: four
+ // scripts write it and read it back, and sharing it meant they could only
+ // take turns — which cost more wall clock than the other 51 scripts put
+ // together. Scripts name it `$DUMP`.
+ const dump_path = try std.fmt.allocPrint(arena, "{s}/dump.zon", .{base});
+ _ = setenv("PARDES_DUMP", try arena.dupeZ(u8, dump_path), 1);
if (chdirRc(try arena.dupeZ(u8, work)) < 0) return error.ChdirFailed;
var out: std.ArrayList(u8) = .empty;
@@ -152,8 +325,14 @@ fn runScript(arena: std.mem.Allocator, exe_z: [:0]const u8, script_path: []const
var lineno: usize = 0;
while (lines_it.next()) |raw_line| {
lineno += 1;
- const line = std.mem.trim(u8, raw_line, " \t\r");
- if (line.len == 0 or line[0] == '#') continue;
+ const trimmed = std.mem.trim(u8, raw_line, " \t\r");
+ if (trimmed.len == 0 or trimmed[0] == '#') continue;
+ // `$DUMP` is the script's own PARDES_DUMP file (see above)
+ const line = if (std.mem.indexOf(u8, trimmed, "$DUMP") == null) trimmed else blk: {
+ const buf = try arena.alloc(u8, std.mem.replacementSize(u8, trimmed, "$DUMP", dump_path));
+ _ = std.mem.replace(u8, trimmed, "$DUMP", dump_path, buf);
+ break :blk buf;
+ };
var tok = std.mem.tokenizeScalar(u8, line, ' ');
const cmd = tok.next().?;
errdefer std.debug.print(" at {s}:{d}: {s}\n", .{ script_path, lineno, line });
@@ -275,25 +454,153 @@ fn live(h: *?eh.Harness) *eh.Harness {
fatal("script used the app before `start`", .{});
}
-/// Pump until the grid is unchanged for `quiet` ms (bounded by `timeout`).
-/// The one sync primitive that needs no needle — used before every snap so
-/// old and new binaries are compared at rest, not mid-repaint.
+/// Pump until the captured state — grid text AND cursor — has not changed for
+/// `quiet` ms (bounded by `timeout`). The one sync primitive that needs no
+/// needle: used before every snap so old and new binaries are compared at
+/// rest, not mid-repaint.
+///
+/// Same oracle as before, timed continuously instead of in fixed windows. The
+/// old loop pumped a whole `quiet` window, compared, and pumped another — so
+/// even an app that fell silent immediately cost 2 × 700ms, and 446 `stable`s
+/// cost the suite ten minutes. This one sleeps in poll until the app writes,
+/// and returns `quiet` ms after the LAST change. Waiting on the change rather
+/// than on silence is what keeps it honest: pardes finishes a repaint with a
+/// late cursor move, hundreds of ms after its last burst of text.
fn waitStable(h: *eh.Harness, quiet: i64, timeout: i64) !void {
- var total: i64 = 0;
- var prev = try h.screenText();
+ const t0 = eh.nowMs();
+ const deadline = t0 + timeout;
+ var prev = try stateKey(h);
defer h.gpa.free(prev);
- while (total < timeout) {
- try h.pump(quiet);
- total += quiet;
- const cur = try h.screenText();
- if (std.mem.eql(u8, prev, cur)) {
- h.gpa.free(cur);
+ var last_change = t0;
+ var asleep_since: i64 = 0;
+ while (true) {
+ const now = eh.nowMs();
+ const since = now - last_change;
+ if (since >= quiet) {
+ if (trace_stable) std.debug.print("[stable q={d}] window: last change +{d}ms, cost {d}ms\n", .{ quiet, last_change - t0, now - t0 });
return;
}
+ // Fast path: the screen has held still, the pty is empty in both
+ // directions (so the app has CONSUMED everything the script just sent
+ // — otherwise an app still asleep in its event loop looks finished),
+ // and nothing in its process tree is runnable. Held across two probes,
+ // because a pane's shell can write and sleep a scheduler tick before
+ // the app wakes to read it.
+ if (since >= probe_after_ms and h.pending() == 0 and treeAsleep(h.pid)) {
+ if (asleep_since == 0) {
+ asleep_since = now;
+ } else if (now - asleep_since >= probe_hold_ms) {
+ if (trace_stable) std.debug.print("[stable q={d}] asleep: last change +{d}ms, cost {d}ms\n", .{ quiet, last_change - t0, now - t0 });
+ return;
+ }
+ } else if (since >= probe_after_ms) {
+ asleep_since = 0;
+ }
+ if (now >= deadline) return error.NeverStable;
+ const nap = if (since >= probe_after_ms) probe_every_ms else probe_after_ms - since;
+ if (!try h.pumpOnce(@min(nap, deadline - now))) continue;
+ while (try h.pumpOnce(0)) {} // drain the burst before comparing
+ const cur = try stateKey(h);
+ if (std.mem.eql(u8, cur, prev)) {
+ h.gpa.free(cur);
+ continue;
+ }
h.gpa.free(prev);
prev = cur;
+ if (trace_stable) std.debug.print("[stable q={d}] change at +{d}ms (gap {d}ms)\n", .{ quiet, eh.nowMs() - t0, eh.nowMs() - last_change });
+ last_change = eh.nowMs();
+ asleep_since = 0;
+ }
+}
+
+// How long the screen must hold still before the sleep probe is trusted at all,
+// how often it is taken, and how long it must stay true.
+//
+// The hold is the one number that cannot be reasoned down to zero. When the app
+// writes to a pane's pty the kernel parks the bytes in a flip buffer and leaves
+// the hand-off to a kworker: until that runs, the app is asleep, the shell is
+// asleep, nothing is runnable and no /proc or ioctl query can see the round trip
+// in flight (a pty master cannot even be reopened through /proc/<pid>/fd — it
+// mints a fresh pty). So the hold is a bet on kworker latency, and a loaded
+// machine loses it. The parallel pass bets small; the serial re-run that decides
+// the verdict bets big.
+var probe_after_ms: i64 = 10;
+const probe_every_ms: i64 = 5;
+var probe_hold_ms: i64 = 50;
+
+/// Widen the probes for the re-run pass: nothing else is competing then, so the
+/// extra wait is cheap, and its verdict is the one that gets reported.
+fn beCareful() void {
+ probe_after_ms = 40;
+ probe_hold_ms = 300;
+}
+
+/// Is every process in the app's tree asleep? Walks /proc from the app's pid
+/// through `children`, checking every thread's run state: `R` (runnable) or
+/// `D` (uninterruptible) anywhere means work is still in flight — a repaint,
+/// a fork, a shell mid-command — so a quiet pty proves nothing. Anything else
+/// (`S`, or a zombie waiting to be reaped) is idle.
+///
+/// This is what replaces the wall-clock window: the app finishes a keystroke
+/// in ~10ms, and the scripts' 400-700ms `stable` windows were insurance
+/// against not being able to tell "finished" from "paused mid-fork".
+fn treeAsleep(root: libc.pid_t) bool {
+ var stack: [64]libc.pid_t = undefined;
+ stack[0] = root;
+ var n: usize = 1;
+ var path_buf: [64:0]u8 = undefined;
+ var buf: [4096]u8 = undefined;
+ while (n > 0) {
+ n -= 1;
+ const pid = stack[n];
+ const dir_path = std.fmt.bufPrintZ(&path_buf, "/proc/{d}/task", .{pid}) catch continue;
+ const dir = libc.opendir(dir_path) orelse continue; // exited between steps
+ defer _ = libc.closedir(dir);
+ while (libc.readdir(dir)) |d| {
+ const tid = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(&d.name)), 0);
+ if (tid[0] == '.') continue;
+ const stat = readProc(&buf, std.fmt.bufPrintZ(&path_buf, "/proc/{d}/task/{s}/stat", .{ pid, tid }) catch continue) orelse continue;
+ // "pid (comm) S ..." — comm can hold spaces and parens, so the
+ // state is the char two past the LAST ')'
+ const close = std.mem.lastIndexOfScalar(u8, stat, ')') orelse continue;
+ if (close + 2 >= stat.len) continue;
+ switch (stat[close + 2]) {
+ // R/D: still working. Z: just exited, so its parent is about to
+ // be woken to reap it and carry on — a pane's shell sitting in
+ // wait4 over a zombie `ls` looks asleep, but the prompt it is
+ // about to print is what hides the echoed command line in the
+ // goldens.
+ 'R', 'D', 'Z' => return false,
+ else => {},
+ }
+ const kids = readProc(&buf, std.fmt.bufPrintZ(&path_buf, "/proc/{d}/task/{s}/children", .{ pid, tid }) catch continue) orelse continue;
+ var it = std.mem.tokenizeAny(u8, kids, " \n");
+ while (it.next()) |k| {
+ if (n >= stack.len) return false; // deeper than we can walk: assume busy
+ stack[n] = std.fmt.parseInt(libc.pid_t, k, 10) catch continue;
+ n += 1;
+ }
+ }
}
- return error.NeverStable;
+ return true;
+}
+
+/// Read a small /proc file into `buf` (procfs needs one read, no stat).
+fn readProc(buf: []u8, path: [*:0]const u8) ?[]const u8 {
+ const fd = libc.open(path, .{ .ACCMODE = .RDONLY, .CLOEXEC = true });
+ if (fd < 0) return null;
+ defer _ = libc.close(fd);
+ const got = libc.read(fd, buf.ptr, buf.len);
+ if (got <= 0) return null;
+ return buf[0..@intCast(got)];
+}
+
+/// What a `snap` capture is made of: the grid text plus the cursor cell.
+fn stateKey(h: *eh.Harness) ![]u8 {
+ const text = try h.screenText();
+ defer h.gpa.free(text);
+ const c = h.cursor();
+ return std.fmt.allocPrint(h.gpa, "{d},{d}\n{s}", .{ c.x, c.y, text });
}
fn snapText(arena: std.mem.Allocator, out: *std.ArrayList(u8), h: *eh.Harness, label: []const u8) !void {
diff --git a/test/snapshots/chordcut.snap b/test/snapshots/chordcut.snap
index fc9ccb40..a412b57e 100644
--- a/test/snapshots/chordcut.snap
+++ b/test/snapshots/chordcut.snap
@@ -17,7 +17,7 @@ stable 400 5000
key i
text f.txt
key esc
-settle 700
+settle 100
stable 400 5000
press right 6 8
release right 6 8
@@ -65,7 +65,7 @@ stable 400 5000
key i
text echo B''OO g.txt
key esc
-settle 700
+settle 100
stable 400 5000
# 2-3 cancel (acme textselect2): tap right during the middle execute-drag
# over `echo B''OO` — the command never runs (no BOO anywhere)
diff --git a/test/snapshots/cursorline.snap b/test/snapshots/cursorline.snap
index a7e38423..5519287e 100644
--- a/test/snapshots/cursorline.snap
+++ b/test/snapshots/cursorline.snap
@@ -10,7 +10,7 @@ stable 400 5000
key i
text cur.txt
key esc
-settle 700
+settle 100
stable 400 5000
press right 6 8
release right 6 8
diff --git a/test/snapshots/dump.snap b/test/snapshots/dump.snap
index 2cd2ace0..9ec0b457 100644
--- a/test/snapshots/dump.snap
+++ b/test/snapshots/dump.snap
@@ -1,4 +1,5 @@
-# the Dump builtin serializes the session to /tmp/pardes-dump.zon; the ZON must
+# the Dump builtin serializes the session to $DUMP (this script's own dump
+# file under /tmp/pardes-snap/dump/); the ZON must
# be byte-identical between old and new (golden generated from the OLD binary)
file d.txt x
start 30 100 -n 3
@@ -10,9 +11,9 @@ stable 400 5000
key i
text typed-marker
key esc
-settle 700
+settle 100
stable 400 5000
press middle 35 1
release middle 35 1
-settle 800
-snapfile dump /tmp/pardes-dump.zon
+stable 600 8000
+snapfile dump $DUMP
diff --git a/test/snapshots/edit.snap b/test/snapshots/edit.snap
index 52e287d8..5a0b610e 100644
--- a/test/snapshots/edit.snap
+++ b/test/snapshots/edit.snap
@@ -5,11 +5,11 @@ wait 8000 e.txt
stable 700 20000
press left 5 10
release left 5 10
-settle 400
+stable 400 8000
key i
text hello-insert
key esc
-settle 700
+settle 100
stable 500 8000
snap inserted
snapstyle inserted
diff --git a/test/snapshots/exec-bigdrag.snap b/test/snapshots/exec-bigdrag.snap
index 9158f08d..91ff1727 100644
--- a/test/snapshots/exec-bigdrag.snap
+++ b/test/snapshots/exec-bigdrag.snap
@@ -12,7 +12,7 @@ stable 300 5000
key i
text tr -d '\n' < big.txt
key esc
-settle 400
+settle 100
press middle 105 8
drag middle 126 8
release middle 126 8
@@ -22,7 +22,7 @@ press middle 105 4
drag middle 150 30
drag middle 198 58
release middle 198 58
-settle 500
+stable 500 8000
stable 800 30000
# still alive: type and execute a marker command
press left 105 40
@@ -31,7 +31,7 @@ stable 300 5000
key i
text echo alive-o''k
key esc
-settle 300
+settle 100
press middle 105 40
drag middle 119 40
release middle 119 40
diff --git a/test/snapshots/exec.snap b/test/snapshots/exec.snap
index a66285d5..fcbc0746 100644
--- a/test/snapshots/exec.snap
+++ b/test/snapshots/exec.snap
@@ -11,7 +11,7 @@ stable 400 5000
key i
text echo MID''OK
key esc
-settle 700
+settle 100
stable 400 5000
press middle 5 8
drag middle 16 8
diff --git a/test/snapshots/fileedit.snap b/test/snapshots/fileedit.snap
index 8bbe4eac..8a29b66b 100644
--- a/test/snapshots/fileedit.snap
+++ b/test/snapshots/fileedit.snap
@@ -9,7 +9,7 @@ stable 400 5000
key i
text f.txt
key esc
-settle 700
+settle 100
press right 6 8
release right 6 8
wait 10000 line 3
@@ -21,7 +21,7 @@ stable 400 5000
key i
text EDIT''ME
key esc
-settle 700
+settle 100
stable 400 5000
snap edited
# x-select line 2 and delete it
@@ -36,5 +36,5 @@ snap undone
# Save via the tag: middle-click the Save word (after "NOR <path> ")
press middle 43 2
release middle 43 2
-settle 800
+stable 600 8000
snapfile saved /tmp/pardes-snap/fileedit/cwd/f.txt
diff --git a/test/snapshots/find.snap b/test/snapshots/find.snap
index bad36bb6..8be0e511 100644
--- a/test/snapshots/find.snap
+++ b/test/snapshots/find.snap
@@ -63,7 +63,7 @@ release middle 8 3
stable 700 10000
snap word
key esc
-settle 700
+settle 100
stable 400 5000
snap cancel
# a no-match walk empties the buffer; n then has nowhere to go. The list is
diff --git a/test/snapshots/fsearch.snap b/test/snapshots/fsearch.snap
index 7953b408..c4633cda 100644
--- a/test/snapshots/fsearch.snap
+++ b/test/snapshots/fsearch.snap
@@ -15,7 +15,7 @@ stable 400 5000
key i
text g.txt
key esc
-settle 700
+settle 100
stable 400 5000
press right 6 8
release right 6 8
@@ -51,7 +51,7 @@ snap back
key /
text zz
key esc
-settle 700
+settle 100
stable 400 5000
snap cancel
# a no-match search: n has nowhere to go (nothing changes, no crash)
diff --git a/test/snapshots/hscroll.snap b/test/snapshots/hscroll.snap
index 97e8c99d..3f371147 100644
--- a/test/snapshots/hscroll.snap
+++ b/test/snapshots/hscroll.snap
@@ -14,7 +14,7 @@ stable 400 5000
key i
text wide.txt
key esc
-settle 700
+settle 100
stable 400 5000
press right 7 8
release right 7 8
diff --git a/test/snapshots/images.snap b/test/snapshots/images.snap
index e31d57ec..696aaec1 100644
--- a/test/snapshots/images.snap
+++ b/test/snapshots/images.snap
@@ -12,7 +12,7 @@ stable 400 5000
key i
text pic.ppm
key esc
-settle 700
+settle 100
press right 6 8
release right 6 8
wait 10000 img
diff --git a/test/snapshots/layout-close.snap b/test/snapshots/layout-close.snap
index d4160221..f353f39f 100644
--- a/test/snapshots/layout-close.snap
+++ b/test/snapshots/layout-close.snap
@@ -36,7 +36,7 @@ stable 400 5000
key i
text exit
key esc
-settle 700
+settle 100
key tab
stable 700 10000
snap exit
@@ -48,7 +48,7 @@ stable 400 5000
key i
text exit
key esc
-settle 700
+settle 100
key tab
stable 700 10000
snap colclose
diff --git a/test/snapshots/layout-open.snap b/test/snapshots/layout-open.snap
index ee909f2e..36d8bb1b 100644
--- a/test/snapshots/layout-open.snap
+++ b/test/snapshots/layout-open.snap
@@ -16,7 +16,7 @@ key i
text echo one; echo two
key enter
key esc
-settle 700
+settle 100
stable 400 5000
snap before
# Alt-n: new shell below pane 0 — the bottom pane's boundary must not move
@@ -38,7 +38,7 @@ stable 400 5000
key i
text sub/f.txt
key esc
-settle 700
+settle 100
stable 400 5000
press right 7 27
release right 7 27
diff --git a/test/snapshots/leader.snap b/test/snapshots/leader.snap
index 70b8a4d4..c8e13b8e 100644
--- a/test/snapshots/leader.snap
+++ b/test/snapshots/leader.snap
@@ -21,7 +21,7 @@ stable 400 5000
snap spc-h
# Esc abandons the sequence: indicator gone, nothing ran, cursor untouched
key esc
-settle 700
+settle 100
stable 400 5000
snap abandoned
# an UNMAPPED key abandons too, rather than waiting: a typo must not leave the
@@ -85,6 +85,6 @@ snap help-window
# nothing worth keeping, so the key that elsewhere only drops a selection
# dismisses the whole pane and hands its height back to the pane above.
key esc
-settle 700
+settle 100
stable 700 15000
snap esc-del
diff --git a/test/snapshots/load.snap b/test/snapshots/load.snap
index 4b6372c4..561d30ee 100644
--- a/test/snapshots/load.snap
+++ b/test/snapshots/load.snap
@@ -19,8 +19,8 @@ wheel up 10 8
stable 500 8000
press middle 35 1
release middle 35 1
-settle 800
-start 30 100 -l /tmp/pardes-dump.zon
+stable 600 8000
+start 30 100 -l $DUMP
wait 8000 LOADMARK
stable 700 20000
snap loaded
diff --git a/test/snapshots/look-center.snap b/test/snapshots/look-center.snap
index 8bd3ea72..1b11bfd8 100644
--- a/test/snapshots/look-center.snap
+++ b/test/snapshots/look-center.snap
@@ -12,7 +12,7 @@ stable 400 5000
key i
text big.txt:100 big.txt:150 big.txt:2
key esc
-settle 700
+settle 100
stable 400 5000
press right 7 8
release right 7 8
diff --git a/test/snapshots/look-dir.snap b/test/snapshots/look-dir.snap
index 38023fbd..918a5de5 100644
--- a/test/snapshots/look-dir.snap
+++ b/test/snapshots/look-dir.snap
@@ -11,7 +11,7 @@ stable 400 5000
key i
text sub
key esc
-settle 700
+settle 100
stable 400 5000
press right 6 8
release right 6 8
diff --git a/test/snapshots/look-file-1col.snap b/test/snapshots/look-file-1col.snap
index 3ef7e579..ce0ec78d 100644
--- a/test/snapshots/look-file-1col.snap
+++ b/test/snapshots/look-file-1col.snap
@@ -11,7 +11,7 @@ stable 400 5000
key i
text big.txt:40
key esc
-settle 700
+settle 100
stable 400 5000
press right 7 8
release right 7 8
diff --git a/test/snapshots/look-file.snap b/test/snapshots/look-file.snap
index 81cf14cb..4b8b39b7 100644
--- a/test/snapshots/look-file.snap
+++ b/test/snapshots/look-file.snap
@@ -10,7 +10,7 @@ stable 400 5000
key i
text big.txt:40
key esc
-settle 700
+settle 100
stable 400 5000
press right 7 8
release right 7 8
diff --git a/test/snapshots/lookloaded.snap b/test/snapshots/lookloaded.snap
index 89106d25..68674ba2 100644
--- a/test/snapshots/lookloaded.snap
+++ b/test/snapshots/lookloaded.snap
@@ -12,7 +12,7 @@ stable 400 5000
key i
text big.txt:40 big.txt:10
key esc
-settle 700
+settle 100
stable 400 5000
press right 7 8
release right 7 8
diff --git a/test/snapshots/lookpanes.snap b/test/snapshots/lookpanes.snap
index 98648226..64bb4a98 100644
--- a/test/snapshots/lookpanes.snap
+++ b/test/snapshots/lookpanes.snap
@@ -34,7 +34,7 @@ stable 400 5000
key i
text zqonly.txt pick.txt twin.txt
key esc
-settle 700
+settle 100
stable 400 5000
snap shells
# make the beta pane the most recently focused, so the walk has to pass it
diff --git a/test/snapshots/modal.snap b/test/snapshots/modal.snap
index 68809e18..b6769fdd 100644
--- a/test/snapshots/modal.snap
+++ b/test/snapshots/modal.snap
@@ -35,11 +35,11 @@ stable 400 5000
snap vsel
snapstyle vsel
key esc
-settle 700
+settle 100
key i
text TYPED
key esc
-settle 700
+settle 100
stable 400 5000
snap typed
key u
diff --git a/test/snapshots/reflow.snap b/test/snapshots/reflow.snap
index 0fa4a30d..931fe9d4 100644
--- a/test/snapshots/reflow.snap
+++ b/test/snapshots/reflow.snap
@@ -8,7 +8,7 @@ start 24 40
wait 8000 $
text printf 'y%.0s' $(seq 1 3000); echo do''ne
key enter
-settle 2500
+stable 600 15000
stable 700 15000
wheel up 10 10
wheel up 10 10
diff --git a/test/snapshots/restore.golden b/test/snapshots/restore.golden
index 611b9fc6..4ca6cedf 100644
--- a/test/snapshots/restore.golden
+++ b/test/snapshots/restore.golden
@@ -1,5 +1,5 @@
== snap dumped grid=100x30 cursor=4,5
-|Kill Newcol Tutor Debug NextColor Dump Restore /tmp/pardes-dump.zon
+|Kill Newcol Tutor Debug NextColor Dump Restore /tmp/pardes-snap/restore/dump.zon
| TTY /tmp/pardes-snap/restore/cwd Del
| $ ls
| $ echo KEEP''ME
diff --git a/test/snapshots/rsearch.snap b/test/snapshots/rsearch.snap
index 16f40137..669483e9 100644
--- a/test/snapshots/rsearch.snap
+++ b/test/snapshots/rsearch.snap
@@ -17,7 +17,7 @@ stable 400 5000
key i
text code.txt
key esc
-settle 700
+settle 100
stable 400 5000
press right 6 8
release right 6 8
diff --git a/test/snapshots/shrinktag.snap b/test/snapshots/shrinktag.snap
index d82f8192..94d36282 100644
--- a/test/snapshots/shrinktag.snap
+++ b/test/snapshots/shrinktag.snap
@@ -13,7 +13,7 @@ key i
text echo one; echo two
key enter
key esc
-settle 700
+settle 100
stable 400 5000
snap before
# drag pane 0's bottom handle (its last row) up to its tag row: tag-only pane
diff --git a/test/snapshots/syntax.snap b/test/snapshots/syntax.snap
index 0aa85b71..d660fdd9 100644
--- a/test/snapshots/syntax.snap
+++ b/test/snapshots/syntax.snap
@@ -10,7 +10,7 @@ stable 400 5000
key i
text demo.zig
key esc
-settle 700
+settle 100
press right 6 8
release right 6 8
wait 10000 @import
diff --git a/test/snapshots/tag.snap b/test/snapshots/tag.snap
index 4094e841..079244e1 100644
--- a/test/snapshots/tag.snap
+++ b/test/snapshots/tag.snap
@@ -12,7 +12,7 @@ release left 40 2
stable 400 5000
snap tag-focus
send \x20t.txt
-settle 700
+stable 600 8000
stable 400 5000
snap tag-typed
key bs
@@ -22,11 +22,11 @@ key bs
stable 400 5000
snap tag-del
send .txt
-settle 700
+stable 600 8000
stable 400 5000
# Esc: out of the tag, body back in normal mode, typed tail stays as text
key esc
-settle 700
+settle 100
stable 400 5000
snap tag-exit
# re-enter the tag and look the typed word: Enter opens t.txt as a file pane
diff --git a/test/snapshots/tagcmd.snap b/test/snapshots/tagcmd.snap
index d7d002ff..d5317b8d 100644
--- a/test/snapshots/tagcmd.snap
+++ b/test/snapshots/tagcmd.snap
@@ -14,7 +14,7 @@ stable 700 20000
key i
text ZZEDIT
key esc
-settle 700
+settle 100
stable 400 5000
snap edited
# `:` -> the tag, in normal mode, cursor on the tail's first cell
@@ -27,7 +27,7 @@ stable 400 5000
snap word
# Tab executes it: c.txt is written and focus is back in the body
key tab
-settle 800
+stable 600 8000
stable 400 5000
snap saved
snapfile written /tmp/pardes-snap/tagcmd/cwd/c.txt
@@ -35,6 +35,6 @@ snapfile written /tmp/pardes-snap/tagcmd/cwd/c.txt
key :
stable 400 5000
key esc
-settle 700
+settle 100
stable 400 5000
snap escaped
diff --git a/test/snapshots/tagnav.golden b/test/snapshots/tagnav.golden
index 77d5a846..6ea682d2 100644
--- a/test/snapshots/tagnav.golden
+++ b/test/snapshots/tagnav.golden
@@ -495,7 +495,7 @@
|
|
== snap topbar-exec grid=100x30 cursor=7,2
-|Kill Newcol Tutor Debug NextColor Dump Restore /tmp/pardes-dump.zon
+|Kill Newcol Tutor Debug NextColor Dump Restore /tmp/pardes-snap/tagnav/dump.zon
| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del TTY /tmp/pardes-snap/tagnav/cwd Del
| 1 alpha $ ls
| 2 beta w.txt
@@ -526,7 +526,7 @@
|
|
== snap topbar-restore-word grid=100x30 cursor=39,0
-|Kill Newcol Tutor Debug NextColor Dump Restore /tmp/pardes-dump.zon
+|Kill Newcol Tutor Debug NextColor Dump Restore /tmp/pardes-snap/tagnav/dump.zon
| NOR /tmp/pardes-snap/tagnav/cwd/w.txt Save Del TTY /tmp/pardes-snap/tagnav/cwd Del
| 1 alpha $ ls
| 2 beta w.txt
diff --git a/test/snapshots/tagnav.snap b/test/snapshots/tagnav.snap
index fbb9c917..3da71cd9 100644
--- a/test/snapshots/tagnav.snap
+++ b/test/snapshots/tagnav.snap
@@ -69,7 +69,7 @@ key right right
stable 400 5000
snap arrows-still-motion
key esc
-settle 700
+settle 100
stable 400 5000
snap back-to-body
# A TERMINAL MUST NOT LOSE ITS TTY MODE. Put the launch shell in raw tty mode
@@ -87,7 +87,7 @@ key l
stable 400 5000
snap tty-tag
key esc
-settle 700
+settle 100
stable 400 5000
snap tty-restored
# Enter/Tab on the BAR runs the word under the cursor through the very dispatch
diff --git a/test/snapshots/tagpath.snap b/test/snapshots/tagpath.snap
index 61181dd5..e4517ddc 100644
--- a/test/snapshots/tagpath.snap
+++ b/test/snapshots/tagpath.snap
@@ -37,7 +37,7 @@ snapstyle sel-path
# the file, which is the only way a snapshot can see what was yanked.
key y
key esc
-settle 700
+settle 100
stable 400 5000
key p
stable 400 5000
diff --git a/test/snapshots/tinywin.snap b/test/snapshots/tinywin.snap
index fab7f950..95050301 100644
--- a/test/snapshots/tinywin.snap
+++ b/test/snapshots/tinywin.snap
@@ -5,11 +5,11 @@
start 30 100
wait 8000 $
key a-n
-settle 400
+stable 400 8000
key a-n
-settle 400
+stable 400 8000
key a-n
-settle 400
+stable 400 8000
key a-n
stable 700 15000
snap stacked
@@ -20,9 +20,9 @@ snap shrunk
resize 30 100
stable 700 10000
key space c n
-settle 600
+stable 600 8000
key space c n
-settle 600
+stable 600 8000
key space c n
stable 700 15000
snap columns
diff --git a/test/snapshots/ttylook.snap b/test/snapshots/ttylook.snap
index f22cd0d1..cc746970 100644
--- a/test/snapshots/ttylook.snap
+++ b/test/snapshots/ttylook.snap
@@ -29,7 +29,7 @@ snap tokback
key i
text nn
key esc
-settle 700
+settle 100
stable 400 5000
snap insert
key u
diff --git a/test/snapshots/yankpaste.snap b/test/snapshots/yankpaste.snap
index 96082ad6..3445fe98 100644
--- a/test/snapshots/yankpaste.snap
+++ b/test/snapshots/yankpaste.snap
@@ -31,7 +31,7 @@ snap charwise-inline
# y then p. It pastes as one empty line.
key o
key esc
-settle 700
+settle 100
key y p
stable 400 5000
snap blankline