diff options
Diffstat (limited to 'src/ninep/events.zig')
| -rw-r--r-- | src/ninep/events.zig | 322 |
1 files changed, 279 insertions, 43 deletions
diff --git a/src/ninep/events.zig b/src/ninep/events.zig index ccc61352..98799774 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -110,7 +110,9 @@ pub fn readQueue(p: *Pardes, req: Req, q: *Queue) Reply { // ---- the editor-wide log ---- -pub const LogKind = enum { new, del, rename, save }; +/// `unsaved`: a pane a refusal (Exit, Restore, Del, Delcol) is about, +/// logged before the write's err so a script reads which, whole. +pub const LogKind = enum { new, del, rename, save, unsaved }; /// A pane leaving, before its state goes with it; one never announced /// leaves silently. @@ -122,6 +124,12 @@ pub fn noteRetire(p: *Pardes, pane: *Pane) void { pane.fs.unannounced = false; return; } + // A question still standing ends with it, so a follower of its `ask` + // hears the end. + if (pane.prompt == .repl_choice or pane.prompt == .del_side or pane.prompt == .save) { + var rec: [32]u8 = undefined; + notePath(p, "answer", std.fmt.bufPrint(&rec, "{d} -", .{pane.serial}) catch ""); + } noteLog(p, .del, pane); } @@ -143,12 +151,20 @@ pub fn noteLog(p: *Pardes, kind: LogKind, pane: *Pane) void { } /// Records `msg <serial> <text>` for what the editor said, `-` for no pane. -pub fn noteMessage(p: *Pardes, serial: u32, text: []const u8) void { +/// Text past LoggedMessage.cap bytes is cut between words with an ellipsis, +/// as an err record's reason is. +pub fn noteMessage(p: *Pardes, serial: u32, said: []const u8) void { var buf: [pardes.Messages.LoggedMessage.cap + 32]u8 = undefined; - pushLog(p, (if (serial == 0) - std.fmt.bufPrint(&buf, "msg - {s}\n", .{text}) + const kept = pardes.Messages.clip(said, pardes.Messages.LoggedMessage.cap); + const cut = if (kept.len < said.len) "…" else ""; + // A builtin's own word for what it did stands in for its Verbose + // announcement just before (`Undo` then `Undo: nothing to undo`), so + // that 1000 Undos with nothing left are one line, counted, as err's are. + dropAnnouncement(p, serial, kept); + pushCounting(p, (if (serial == 0) + std.fmt.bufPrint(&buf, "msg - {s}{s}\n", .{ kept, cut }) else - std.fmt.bufPrint(&buf, "msg {d} {s}\n", .{ serial, text })) catch return); + std.fmt.bufPrint(&buf, "msg {d} {s}{s}\n", .{ serial, kept, cut })) catch return); } /// Records `err <serial|-> <file>: <why>` for a write or truncation that was @@ -165,16 +181,25 @@ pub fn notePath(p: *Pardes, what: []const u8, path: []const u8) void { pub fn noteError(p: *Pardes, req: Req, reply: Reply) void { const why = if (reply.ename.len > 0) reply.ename else cloud9.fs.errString(reply.errno); - var name: [16]u8 = undefined; + var name: [32]u8 = undefined; var serial: []const u8 = "-"; var file: []const u8 = "os"; if (req.node & tree.sources.archive_node != 0) file = "src"; if (req.node != pardes.filesystem.os_root and req.node & pardes.filesystem.os_node == 0 and req.node & tree.sources.archive_node == 0) { if (tree.Node.target(req.node)) |t| switch (t) { - .top => |f| file = f.fileName(), + .top => |f| { + file = f.fileName(); + // The root's exec or look ran at the keyboard's pane: its + // failure is that pane's, as a click there would be. + if ((f == .exec or f == .look) and p.fs.ran_at != 0) + serial = std.fmt.bufPrint(&name, "{d}", .{p.fs.ran_at}) catch "-"; + }, .col => |c| { serial = "-"; - file = std.fmt.bufPrint(&name, "col/{d}/tag", .{c.serial}) catch "col"; + file = (if (c.file == .dir) + std.fmt.bufPrint(&name, "col/{d}", .{c.serial}) + else + std.fmt.bufPrint(&name, "col/{d}/{s}", .{ c.serial, @tagName(c.file) })) catch "col"; }, .pane => |pt| { serial = std.fmt.bufPrint(&name, "{d}", .{pt.serial}) catch "-"; @@ -189,26 +214,91 @@ pub fn noteError(p: *Pardes, req: Req, reply: Reply) void { }; } var buf: [256]u8 = undefined; - const record = std.fmt.bufPrint(&buf, "err {s} {s}: {s}\n", .{ serial, file, why[0..@min(why.len, 200)] }) catch return; + // A long reason is cut between words, with an ellipsis, never mid-word. + const kept = pardes.Messages.clip(why, 200); + const record = std.fmt.bufPrint(&buf, "err {s} {s}: {s}{s}\n", .{ serial, file, kept, if (kept.len < why.len) "…" else "" }) catch return; for (record[0 .. record.len - 1]) |*c| if (c.* < ' ') { c.* = ' '; }; + pushCounting(p, record); +} + +/// Drops the newest record when it is the Verbose announcement (`msg +/// <serial> <Word>`) of the builtin whose words `text` are (`Word: ...`): +/// what the builtin said, or the err its failure logs, takes its place. +/// Not one a follower has read. +pub fn dropAnnouncement(p: *Pardes, serial: u32, text: []const u8) void { + const last = newest(p) orelse return; + if (followerRead(p, last.seq) or !std.mem.startsWith(u8, last.text, "msg ")) return; + const rest = last.text["msg ".len..]; + const sp = std.mem.indexOfScalar(u8, rest, ' ') orelse rest.len; + const word = rest[@min(sp + 1, rest.len)..]; + var who: [16]u8 = undefined; + const mine = if (serial == 0) "-" else std.fmt.bufPrint(&who, "{d}", .{serial}) catch ""; + if (std.mem.eql(u8, rest[0..sp], mine) and word.len > 0 and std.mem.indexOfScalar(u8, word, ' ') == null and + text.len > word.len and std.mem.startsWith(u8, text, word) and text[word.len] == ':') + p.fs.log.buf.shrinkRetainingCapacity(last.at); +} + +/// The sequence number of the newest log record when it is a msg that +/// ends in `word` alone: not one counted (`(x2)`) over an earlier say. +pub fn newestSaying(p: *Pardes, word: []const u8) ?u64 { + const last = newest(p) orelse return null; + if (!std.mem.startsWith(u8, last.text, "msg ") or !std.mem.endsWith(u8, last.text, word)) return null; + if (last.text[last.text.len - word.len - 1] != ' ') return null; + return last.seq; +} + +/// Takes record `seq` back out of the log, unless a follower has read it. +pub fn dropRecord(p: *Pardes, seq: u64) void { + if (followerRead(p, seq)) return; + const q = &p.fs.log; + var at = q.head; + var s = q.dropped; + while (at + 4 <= q.buf.items.len) : (s += 1) { + const size = 4 + std.mem.readInt(u32, q.buf.items[at..][0..4], .little); + if (s == seq) { + const items = q.buf.items; + std.mem.copyForwards(u8, items[at..], items[at + size ..]); + q.buf.shrinkRetainingCapacity(items.len - size); + return; + } + at += size; + } +} + +/// A builtin's failure: its Verbose announcement goes from the log, when +/// `text` is that builtin's (it starts with its word) or `any`, as for a +/// failure said while the builtin itself runs. +pub fn dropFailedAnnouncement(p: *Pardes, text: []const u8, any: bool) void { + const seq = p.fs.announced_seq orelse return; + const word = p.fs.announced_word[0..p.fs.announced_word_len]; + if (!any and !(std.mem.startsWith(u8, text, word) and text.len > word.len and (text[word.len] == ' ' or text[word.len] == ':'))) return; + p.fs.announced_seq = null; + dropRecord(p, seq); +} + +/// Drops the newest record when it is a msg saying `text` (of any pane) +/// that no follower has read: a failure its write's err record will say. +pub fn dropMessage(p: *Pardes, text: []const u8) void { + const last = newest(p) orelse return; + if (followerRead(p, last.seq) or !std.mem.startsWith(u8, last.text, "msg ")) return; + const rest = last.text["msg ".len..]; + const sp = std.mem.indexOfScalar(u8, rest, ' ') orelse return; + if (std.mem.eql(u8, rest[sp + 1 ..], text)) p.fs.log.buf.shrinkRetainingCapacity(last.at); +} + +/// Pushes `record`, or, when it repeats the newest record word for word, +/// counts that one instead: `<record> (x2)`, the count being every time it +/// was said. One a follower has already read is not rewritten: the repeat +/// is a new line carrying the running count, `(x3)`. +fn pushCounting(p: *Pardes, record: []u8) void { // A client retrying a write that fails the same way would fill the ring // with one line, so a repeat of the newest record is that record counted, // `(x3)`, as +Messages counts its repeats; unless a follower has read it // already and so waits on the repeat as a line of its own. - const q = &p.fs.log; - var at = q.head; - var last: ?usize = null; - var seq = q.dropped; - while (at + 4 <= q.buf.items.len) : (seq += 1) { - last = at; - at += 4 + std.mem.readInt(u32, q.buf.items[at..][0..4], .little); - } - const last_at = last orelse return pushLog(p, record); - const last_seq = seq - 1; - var said = q.buf.items[last_at + 4 ..]; - said = said[0 .. said.len - 1]; + const last = newest(p) orelse return pushLog(p, record); + var said = last.text; var times: u32 = 1; if (std.mem.endsWith(u8, said, ")")) if (std.mem.lastIndexOf(u8, said, " (x")) |x| { if (std.fmt.parseInt(u32, said[x + 3 .. said.len - 1], 10)) |n| { @@ -217,14 +307,35 @@ pub fn noteError(p: *Pardes, req: Req, reply: Reply) void { } else |_| {} }; if (!std.mem.eql(u8, said, record[0 .. record.len - 1])) return pushLog(p, record); + var counted: [pardes.Messages.LoggedMessage.cap + 64]u8 = undefined; + const again = std.fmt.bufPrint(&counted, "{s} (x{d})\n", .{ said, times +| 1 }) catch return; + if (!followerRead(p, last.seq)) p.fs.log.buf.shrinkRetainingCapacity(last.at); + pushLog(p, again); +} + +/// The newest record in the ring: where it starts, its sequence number and +/// its text without the newline. +fn newest(p: *Pardes) ?struct { at: usize, seq: u64, text: []const u8 } { + const q = &p.fs.log; + var at = q.head; + var last: ?usize = null; + var seq = q.dropped; + while (at + 4 <= q.buf.items.len) : (seq += 1) { + last = at; + at += 4 + std.mem.readInt(u32, q.buf.items[at..][0..4], .little); + } + const last_at = last orelse return null; + const text = q.buf.items[last_at + 4 ..]; + return .{ .at = last_at, .seq = seq - 1, .text = text[0 .. text.len - 1] }; +} + +/// Whether a follower has read the record `seq`, or is part way through it. +fn followerRead(p: *Pardes, seq: u64) bool { for (p.fs.opens) |o| if (o.node != 0 and o.what == .log) { const l = o.what.log; - if (l.follow and (l.next > last_seq or (l.next == last_seq and l.part > 0))) return pushLog(p, record); + if (l.follow and (l.next > seq or (l.next == seq and l.part > 0))) return true; }; - var counted: [280]u8 = undefined; - const again = std.fmt.bufPrint(&counted, "{s} (x{d})\n", .{ said, times +| 1 }) catch return; - q.buf.shrinkRetainingCapacity(last_at); - pushLog(p, again); + return false; } /// The log is one ring that records whether or not anyone reads it. A record @@ -322,7 +433,12 @@ pub fn readLog(p: *Pardes, req: Req) Reply { /// records instead of ending; closing it is the way back. pub fn writeLog(p: *Pardes, req: Req) Reply { const slot = &(tree.openOf(p, req) orelse return Reply.fail(req.tag, E.INVAL)).what.log; - if (!std.mem.eql(u8, std.mem.trim(u8, req.data, " \t\r\n"), "follow")) return Reply.fail(req.tag, E.INVAL); + const word = std.mem.trim(u8, req.data, " \t\r\n"); + // `follow` reads what the open froze, then waits; `follow new` skips + // what was there and waits for what comes after, as tail -n0 -f does. + if (std.mem.eql(u8, word, "follow new")) { + slot.pos = slot.bytes.len; + } else if (!std.mem.eql(u8, word, "follow")) return Reply.fail(req.tag, E.INVAL); slot.follow = true; return .{ .tag = req.tag, .written = @intCast(req.data.len) }; } @@ -459,6 +575,16 @@ pub fn noteAction( return true; } +/// A record that follows another with no place of its own, `<c>0 0 0 <n> +/// <text>`: a chorded exec's argument and its location (acme exec.c:182). +pub fn noteFollow(p: *Pardes, id: usize, action: Action, text: []const u8) void { + const pane = p.panes[id] orelse return; + if (pane.fs.readers == 0) return; + var buf: [max_record_text + 64]u8 = undefined; + pane.fs.events.push(p.gpa, formatRecord(&buf, p.fs.origin, action, 0, 0, 0, text)); + p.fs.news = true; +} + pub fn notePtyOutput(p: *Pardes, id: usize, bytes: []const u8) void { if (id >= MAX_PANES or bytes.len == 0) return; const pf = &(p.panes[id] orelse return).fs; @@ -472,7 +598,7 @@ pub fn notePtyOutput(p: *Pardes, id: usize, bytes: []const u8) void { p.fs.news = true; } -const EventRecord = struct { origin: u8, action: Action, q0: u32, q1: u32, text: ?[]const u8 = null }; +const EventRecord = struct { origin: u8, action: Action, q0: u32, q1: u32, flag: u32 = 0, text: ?[]const u8 = null }; const EventReader = struct { data: []const u8, @@ -493,8 +619,9 @@ const EventReader = struct { // is taken too, so a click with no place in the text (one written // to a pane's own look or exec) can be written back and done. var text: ?[]const u8 = null; + var flag: u32 = 0; if (i < er.data.len and er.data[i] != '\n') { - _ = scanNumber(er.data, &i) orelse return null; + flag = scanNumber(er.data, &i) orelse return null; const n = scanNumber(er.data, &i) orelse return null; if (i >= er.data.len or er.data[i] != ' ' or er.data.len - (i + 1) < n) return null; text = er.data[i + 1 ..][0..n]; @@ -502,7 +629,7 @@ const EventReader = struct { } if (i >= er.data.len or er.data[i] != '\n') return null; er.i = i + 1; - return .{ .origin = origin, .action = action, .q0 = q0, .q1 = q1, .text = text }; + return .{ .origin = origin, .action = action, .q0 = q0, .q1 = q1, .flag = flag, .text = text }; } }; @@ -530,7 +657,9 @@ pub fn writeEvent(p: *Pardes, req: Req, id: usize) Reply { else => return tree.failText(req.tag, E.INVAL, tree.e_bad_event), } const n = if (r.action.onTag()) tag.len else body.len; - if (r.q0 > r.q1 or r.q1 > n) return tree.failText(req.tag, E.INVAL, tree.e_bad_event); + if (r.q0 > r.q1) return tree.failText(req.tag, E.INVAL, tree.e_bad_event); + // Well formed, but not a range of this text: say which. + if (r.q1 > n) return tree.failText(req.tag, E.INVAL, if (r.action.onTag()) "range past end of tag" else "range past end of body"); } if (check.i != req.data.len) return tree.failText(req.tag, E.INVAL, tree.e_bad_event); } @@ -548,9 +677,65 @@ pub fn writeEvent(p: *Pardes, req: Req, id: usize) Reply { // been (acme's xfideventwrite, xfid.c:842-844): a REPL bound for // it takes it. One that came from a write to exec (`F`) was // never a click, and runs as the command it was. - .body_exec => _ = exec.executeFrom(p, live, text, r.origin != 'F'), - .tag_exec => _ = exec.execute(p, live, text), - .body_look, .tag_look => look.lookAt(p, live, text), + // The chorded exec a reader was told of runs with its argument + // (mouse.reportGesture), when it is that range written back. + .body_exec, .tag_exec => { + const pf = &pane.fs; + // The two records that follow a chorded exec (its argument + // and where it came from), written back after it, are its: + // consumed, never run as commands of their own. + if (pf.chord_follow > 0 and r.q0 == 0 and r.q1 == 0 and r.flag == 0) { + pf.chord_follow -= 1; + // The argument a command written back alone waited for. + if (pf.chord_pending) |command| { + pf.chord_pending = null; + defer p.gpa.free(command); + const line = exec.withArg(p, command, r.text orelse ""); + if (pf.chord_pending_tag) _ = exec.execute(p, live, line) else _ = exec.executeFrom(p, live, line, true); + } + continue; + } + var arg = if (pf.chord_arg) |a| (if (pf.chord_at.q0 == r.q0 and pf.chord_at.q1 == r.q1) a else null) else null; + if (r.flag & flag_chorded != 0) { + // Its argument is the next record when it came in the + // same write; the rest that follow are consumed. + pf.chord_follow = 2; + const save = run.i; + if (run.next()) |following| if (following.q0 == 0 and following.q1 == 0 and following.flag == 0) { + if (following.text) |t| arg = p.scratch.allocator().dupe(u8, t) catch arg; + pf.chord_follow = 1; + } else { + run.i = save; + }; + // Alone, with no argument kept: it waits for its own. + if (arg == null) { + if (pf.chord_pending) |old| p.gpa.free(old); + pf.chord_pending = p.gpa.dupe(u8, text) catch null; + pf.chord_pending_tag = r.action == .tag_exec; + continue; + } + } + if (r.action == .tag_exec) + _ = exec.execute(p, live, exec.withArg(p, text, arg)) + else + _ = exec.executeFrom(p, live, exec.withArg(p, text, arg), r.origin != 'F'); + if (arg != null) if (p.paneBySerial(serial)) |still| { + const spf = &p.panes[still].?.fs; + if (spf.chord_arg) |a| p.gpa.free(a); + spf.chord_arg = null; + }; + }, + // What it found is what the look file reads back next, as a + // look written there would leave (ctl.run). + .body_look, .tag_look => { + p.look_missed = false; + look.lookAt(p, live, text); + p.fs.results_len = 0; + if (!p.look_missed) if (p.panes[p.active]) |found| { + p.fs.results[0] = found.serial; + p.fs.results_len = 1; + }; + }, else => unreachable, } } @@ -726,7 +911,7 @@ test "a pane deleted while its event file is open leaves no suppression behind" try testing.expectEqual(@as(u16, 0), p.fs.listeners); // The reader's next read hears what acme says of a window gone under it. const shut = call(p, .{ .tag = 19, .op = .read, .node = event, .handle = a.reply.handle, .size = 64 }); - try testing.expectEqual(E.IO, shut.errno()); + try testing.expectEqual(E.NOENT, shut.errno()); try testing.expectEqualStrings(tree.e_shut_down, shut.reply.ename); _ = call(p, .{ .tag = 20, .op = .release, .node = event, .handle = a.reply.handle }); @@ -767,6 +952,12 @@ test "writing an event record back performs the action it names" { try testing.expectEqual(E.INVAL, wr(p, event, bad).errno()); try testing.expectEqual(@as(usize, 0), pane.msg_len); } + // A look written back sets what look reads back. + _ = wr(p, event, "FL0 3\n"); + var want_serial: [16]u8 = undefined; + try testing.expectEqualStrings(try std.fmt.bufPrint(&want_serial, "{d}\n", .{serial}), rd(p, @intFromEnum(tree.TopFile.look), 0, 64).bytes); + // Well formed but past the text: said so, not "bad event syntax". + try testing.expectEqualStrings("range past end of body", wr(p, event, "MX0 999\n").reply.ename); _ = call(p, .{ .tag = 23, .op = .open, .node = event }); p.fs.origin = 'K'; @@ -848,10 +1039,10 @@ test "the log records whether or not anyone reads, and an open that follows wait try testing.expectEqual(Status.again, rdf.next(p, log, fh, frozen).reply.status); // A shell's `read` asks for a few bytes at a time: the record comes in - // pieces rather than failing. Every repeat is its own line. + // pieces rather than failing. A repeat the follower waits on, having + // read the first, is its own line. const said_by = p.panes[0].?.serial; p.setMessage(0, "same failure"); - p.setMessage(0, "same failure"); var pieced: [64]u8 = undefined; var got: usize = 0; while (got == 0 or pieced[got - 1] != '\n') { @@ -860,7 +1051,8 @@ test "the log records whether or not anyone reads, and an open that follows wait got += piece.len; } try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "msg {d} same failure\n", .{said_by}), pieced[0..got]); - try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "msg {d} same failure\n", .{said_by}), rdf.next(p, log, fh, frozen).bytes); + p.setMessage(0, "same failure"); + try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "msg {d} same failure (x2)\n", .{said_by}), rdf.next(p, log, fh, frozen).bytes); // A follower the ring outran hears how much it missed, then carries on. var filler: [200]u8 = @splat('x'); @@ -911,19 +1103,20 @@ test "a refused or failed write is an err record in the log, saying which file a // A write that succeeds says nothing. _ = wr(p, Node.of(serial, .addr), "#0"); try testing.expectEqual(Status.again, call(p, .{ .tag = 5, .op = .read, .node = log, .handle = f, .off = frozen, .size = 4096 }).reply.status); - // A follower that has read an err waits on its repeat: a line of its own. + // A follower that has read an err waits on its repeat: a line of its + // own, with the running count. _ = wr(p, Node.of(serial, .addr), "/nowhere/"); try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "err {d} addr: no match for regexp\n", .{serial}), next.record(p, f, frozen)); _ = wr(p, Node.of(serial, .addr), "/nowhere/"); - try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "err {d} addr: no match for regexp\n", .{serial}), next.record(p, f, frozen)); + try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "err {d} addr: no match for regexp (x2)\n", .{serial}), next.record(p, f, frozen)); _ = call(p, .{ .tag = 6, .op = .release, .node = log, .handle = f }); - // With the follower gone, repeats are the newest record counted: the - // one it read, and three more. + // With the follower gone, repeats are the newest record counted on: + // its two, and three more. for (0..3) |_| _ = wr(p, Node.of(serial, .addr), "/elsewhere/"); const g = call(p, .{ .tag = 7, .op = .open, .node = log }).reply.handle; const text = call(p, .{ .tag = 8, .op = .read, .node = log, .handle = g, .size = 1 << 16 }).bytes; - try testing.expect(std.mem.endsWith(u8, text, try std.fmt.bufPrint(&want, "regexp\nerr {d} addr: no match for regexp (x4)\n", .{serial}))); + try testing.expect(std.mem.endsWith(u8, text, try std.fmt.bufPrint(&want, "regexp\nerr {d} addr: no match for regexp (x5)\n", .{serial}))); _ = call(p, .{ .tag = 9, .op = .release, .node = log, .handle = g }); } @@ -1020,3 +1213,46 @@ test "a click in the body reports the offsets of the word it takes, so the recor _ = call(p, .{ .tag = 3, .op = .release, .node = event, .handle = h }); } } + +test "follow new skips what the log held and waits for what comes after" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + p.update(.tick); // the pane's `new` is logged first + while (p.nextEffect()) |_| {} + p.setMessage(0, "before the open"); + const log = @intFromEnum(tree.TopFile.log); + const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle; + try testing.expectEqual(Status.ok, call(p, .{ .tag = 2, .op = .write, .node = log, .handle = h, .data = "follow new\n" }).reply.status); + try testing.expectEqual(Status.again, call(p, .{ .tag = 3, .op = .read, .node = log, .handle = h, .size = 4096 }).reply.status); + p.setMessage(0, "after it"); + try testing.expect(std.mem.endsWith(u8, call(p, .{ .tag = 4, .op = .read, .node = log, .handle = h, .size = 4096 }).bytes, " after it\n")); + _ = call(p, .{ .tag = 5, .op = .release, .node = log, .handle = h }); +} + +test "a long msg record is cut between words at its cap, with an ellipsis" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const long = "word " ** 60; // 300 bytes + _ = wr(p, Node.of(serialOf(p), .ctl), "Msg " ++ long ++ "\n"); + const log = @intFromEnum(tree.TopFile.log); + const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle; + const text = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = h, .size = 1 << 16 }).bytes; + const at = std.mem.lastIndexOf(u8, text, "msg ").?; + const record = text[at .. std.mem.indexOfScalarPos(u8, text, at, '\n').? + 1]; + try testing.expect(std.mem.endsWith(u8, record, " word…\n")); + try testing.expect(record.len <= "msg 1 ".len + pardes.Messages.LoggedMessage.cap + "…\n".len); + _ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h }); +} + +test "a long err record is cut between words, with an ellipsis" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + _ = wr(p, @intFromEnum(tree.TopFile.ctl), "Bogus " ++ "abcdefgh " ** 40 ++ "\n"); + const log = @intFromEnum(tree.TopFile.log); + const h = call(p, .{ .tag = 1, .op = .open, .node = log }).reply.handle; + const text = call(p, .{ .tag = 2, .op = .read, .node = log, .handle = h, .size = 1 << 16 }).bytes; + const at = std.mem.lastIndexOf(u8, text, "err - ctl: ").?; + const record = text[at .. std.mem.indexOfScalarPos(u8, text, at, '\n').? + 1]; + try testing.expect(std.mem.endsWith(u8, record, "abcdefgh…\n")); + _ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h }); +} |
