diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/e2e_harness.zig | 28 | ||||
| -rw-r--r-- | test/fs.py | 46 | ||||
| -rw-r--r-- | test/monkey9p.py | 16 |
3 files changed, 81 insertions, 9 deletions
diff --git a/test/e2e_harness.zig b/test/e2e_harness.zig index eaa19b43..9e67c49f 100644 --- a/test/e2e_harness.zig +++ b/test/e2e_harness.zig @@ -64,6 +64,8 @@ pub const Harness = struct { /// when true, print the captured screen state after each pump/waitFor and on /// every assertion, so live test runs can be inspected (zig build test -Dtrace). trace: bool = false, + /// How much of a `CSI 5 n` the output has ended on (`feed`). + status_request: u8 = 0, /// every raw byte the app has emitted (accumulated in pump). Lets tests assert /// on control sequences the emulator consumes and never renders (e.g. OSC 52 /// clipboard writes). Capture is bounded explicitly so a runaway child cannot @@ -147,6 +149,24 @@ pub const Harness = struct { /// Read pty output and feed it to our ghostty terminal for `ms` ms. After /// this, the grid reflects everything the app rendered so far. + /// The app's output into the emulator. The one query answered is the + /// device status request (`CSI 5 n` → `CSI 0 n`), as every terminal + /// does: the tty asks it after each frame and draws no other until the + /// answer (FrameAck in src/tty/tty.zig). + fn feed(self: *Harness, bytes: []const u8) void { + self.stream.nextSlice(bytes); + const request = "\x1b[5n"; + for (bytes) |b| { + if (b == request[self.status_request]) { + self.status_request += 1; + } else self.status_request = if (b == 0x1b) 1 else 0; + if (self.status_request == request.len) { + self.status_request = 0; + _ = libc.write(self.master, "\x1b[0n", 4); + } + } + } + pub fn pump(self: *Harness, ms: i64) !void { const deadline = nowMs() + ms; var buf: [4096]u8 = undefined; @@ -157,7 +177,7 @@ pub const Harness = struct { const n = posix.read(self.master, &buf) catch break; if (n == 0) break; self.recordRaw(buf[0..n]); - self.stream.nextSlice(buf[0..n]); + self.feed(buf[0..n]); } } if (self.trace) self.traceScreen("pump"); @@ -176,7 +196,7 @@ pub const Harness = struct { const n = posix.read(self.master, &buf) catch return false; if (n == 0) return false; self.recordRaw(buf[0..n]); - self.stream.nextSlice(buf[0..n]); + self.feed(buf[0..n]); return true; } @@ -243,7 +263,7 @@ pub const Harness = struct { const n = posix.read(self.master, &buf) catch break; if (n == 0) break; self.recordRaw(buf[0..n]); - self.stream.nextSlice(buf[0..n]); + self.feed(buf[0..n]); } const text = try self.screenText(); defer self.gpa.free(text); @@ -270,7 +290,7 @@ pub const Harness = struct { const n = posix.read(self.master, &buf) catch break; if (n == 0) break; self.recordRaw(buf[0..n]); - self.stream.nextSlice(buf[0..n]); + self.feed(buf[0..n]); if (std.mem.indexOf(u8, self.raw.items, needle) != null) return true; } return false; @@ -953,6 +953,20 @@ def test(binary, quic=False): with Client(address, msize=256) as small: assert set(small.list('/os' + str(listing))) == { f'entry-{number:02}' for number in range(24)} + # 64 KiB frames: a ctl line with no newline, whole in its one + # Twrite, runs with it however long -- the cutoff for a write + # that may go on is the frame the client asked for, less 24. + with Client(address, msize=65536) as big: + assert big.msize == 65536 + ctl = big.open('/pane/1/ctl', 1) + line = b'bogus' + b'x' * 20000 + try: + big.rpc(118, struct.pack('<IQI', ctl, 0, len(line)) + line) + except OSError: + pass # refused with its write (a line over 1024 bytes), not held + else: + raise AssertionError('a whole 20000-byte line waited for a newline') + big.close(ctl) frozen = client.open('/screen') before = bytearray(client.read_fid(frozen, count=31)) @@ -1261,10 +1275,42 @@ def test(binary, quic=False): assert time.monotonic() - deleted < .05, time.monotonic() - deleted assert second.wait(timeout=5) == 0 assert edited.read_bytes() == b'edited\n' + # A name not there yet opens a pane named it, as acme's B does, + # and Save creates it: git's and fish's temporary files are. + fresh = root / 'wait new.txt' + before = serials() + made = subprocess.Popen([binary, '--wait', fresh.name], cwd=root, env=env, + stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) + deadline = time.monotonic() + 3 + while not serials() - before: + assert made.poll() is None, made.stderr.read() + assert time.monotonic() < deadline, 'a new name opened no pane' + time.sleep(.01) + named, = serials() - before + assert str(fresh).encode() in client.read('/index') + client.write(f'/pane/{named}/body', b'new\n') + execute(client, named, 'Save') + client.remove(f'/pane/{named}') + assert made.wait(timeout=5) == 0 + assert fresh.read_bytes() == b'new\n' orphan = launch('--wait') assert orphan.poll() is None assert orphan.wait(timeout=5) == 1 + # A launch with no terminal to draw on (a pty that is no one's + # controlling terminal) says so and exits 1, no error trace. + master, slave = os.openpty() + try: + env = {k: v for k, v in os.environ.items() if not k.startswith('PARDES_')} + env.update(HOME=str(root), XDG_RUNTIME_DIR=str(root), TERM='xterm-256color') + result = subprocess.run([binary, '--tty', 'x.txt'], cwd=root, env=env, stdin=slave, stdout=slave, + stderr=subprocess.PIPE, start_new_session=True, timeout=10) + finally: + os.close(master) + os.close(slave) + assert result.returncode == 1, result + assert b'pardes: no terminal to draw on' in result.stderr and b'.zig:' not in result.stderr, result.stderr + for tty in [False, True]: name = 'lsp-tty' if tty else 'lsp-detached' with session(binary, root, name, tty=tty) as (client, _): diff --git a/test/monkey9p.py b/test/monkey9p.py index c0e99fb8..ee0b0ebc 100644 --- a/test/monkey9p.py +++ b/test/monkey9p.py @@ -641,11 +641,11 @@ def op_write(sess, op): ok, body = w.call(TOPEN, struct.pack('<IB', fid, mode), timeout) res.add(open_kind(path, mode), path, ok, None if ok else body) sizes = chunking(op.get('chunks', 'whole'), len(data), w.msize - 23) - allowances(res, path, data, sizes, w.msize - 24) opened = ok sent = data if ok: offset = 0 + took = [] for size in sizes: piece = data[offset:offset + size] ok, body = w.call(TWRITE, struct.pack('<IQI', fid, op.get('offset', offset), len(piece)) + piece, timeout) @@ -654,8 +654,13 @@ def op_write(sess, op): res.add('write' if mode & 3 else 'write-badfid', path, ok, None if ok else body) offset += size sent = data[:offset] + if ok: + took.append(size) if not ok and op.get('stop_on_error', True): break + # Misses are allowed the Twrites that succeeded: a line written a + # byte a Twrite is a look a byte, each its own write. + allowances(res, path, data[:sum(took)], took, w.msize - 24) ok, body = w.call(TCLUNK, struct.pack('<I', fid), timeout) res.add('clunk-write' if mode & 3 else 'clunk', path, ok, None if ok else body) # What the open holds when it closes: the last line written, if unended. @@ -850,11 +855,12 @@ def op_hwrite(sess, op): fid, path, mode, off, _ = h data = dec(op['data'])[:sess.wire.msize - 23] # one Twrite h[4] = data - allowances(res, path, data, [len(data)], sess.wire.msize - 24) ok, body = sess.wire.call(TWRITE, struct.pack('<IQI', fid, off[0], len(data)) + data, SLOW_TIMEOUT if slow(data) else TIMEOUT) res.add('write' if mode & 3 else 'write-badfid', path, ok, None if ok else body) if ok: + allowances(res, path, data, [len(data)], sess.wire.msize - 24) + if ok: off[0] += len(data) return res @@ -1498,9 +1504,9 @@ def one_failure_rule(ctx, res): and w[1].rstrip('/') != '/pane/new'] errs = occurrences(res.window, 'err') msgs = occurrences(res.window, 'msg') - # Only for a write that succeeded: a failed one logs its one err. - failed_paths = {w[1] for w in failed} - allowed = sum(r[3] for r in res.requests if r[0] == 'allow' and r[1] not in failed_paths) + # Allowed only the Twrites that succeeded (allowances): a failed one + # logs its one err. + allowed = sum(r[3] for r in res.requests if r[0] == 'allow') # fs.md 'A write of command lines ... what is left when it closes runs at # the close ... and its failure is in the log alone'. closes = [r for r in res.requests if r[0] == 'close-runs'] |
