test/script.zig
Ref: Size: 3.6 KiB History
//! What the two scripted e2e fixtures share. ptyclient (a real client on a
//! real pty) and wsclient (the browser stand-in on a WebSocket) drive
//! different transports, but both read the SAME line-oriented dialect off
//! stdin and both report failures through the same exit codes — so the
//! escape table and the codes live here, in one copy. Two copies of an
//! escape decoder is two fixtures that can disagree about what a scenario
//! sent, with the scenario's heredoc reading identically either way.
const std = @import("std");
// Exit codes, distinct so a scenario failure names its layer:
// 2 usage / setup failure (bad flag, bad script line, cannot open a file)
// 3 an expect deadline passed
// 4 the far side died before the script finished — ptyclient's client on
// the pty, wsclient's hub on the socket
// Anything else a fixture exits with is the child's own status, which only
// ptyclient's `waitexit` propagates.
pub const EXIT_USAGE: u8 = 2;
pub const EXIT_TIMEOUT: u8 = 3;
pub const EXIT_DIED: u8 = 4;
/// C-style escapes: \xNN, \n, \r, \t, \\. Anything else after a backslash
/// is an error — a typo'd escape must fail loudly, not send mystery bytes.
pub fn decodeEscapes(alloc: std.mem.Allocator, s: []const u8) ![]u8 {
var out: std.ArrayList(u8) = .empty;
errdefer out.deinit(alloc);
var i: usize = 0;
while (i < s.len) : (i += 1) {
if (s[i] != '\\') {
try out.append(alloc, s[i]);
continue;
}
i += 1;
if (i >= s.len) return error.BadEscape;
switch (s[i]) {
'n' => try out.append(alloc, '\n'),
'r' => try out.append(alloc, '\r'),
't' => try out.append(alloc, '\t'),
'\\' => try out.append(alloc, '\\'),
'x' => {
if (i + 2 >= s.len) return error.BadEscape;
// Digit by digit rather than parseInt: parseInt accepts a
// sign, so `\x+1` would quietly decode as 0x01.
const hi = std.fmt.charToDigit(s[i + 1], 16) catch return error.BadEscape;
const lo = std.fmt.charToDigit(s[i + 2], 16) catch return error.BadEscape;
try out.append(alloc, hi * 16 + lo);
i += 2;
},
else => return error.BadEscape,
}
}
return out.toOwnedSlice(alloc);
}
test "decodeEscapes: named, hex, literal backslash" {
const alloc = std.testing.allocator;
const cases = [_]struct { in: []const u8, want: []const u8 }{
.{ .in = "hello\\n", .want = "hello\n" },
.{ .in = "\\x1b[5;2~", .want = "\x1b[5;2~" },
.{ .in = "a\\\\b", .want = "a\\b" },
.{ .in = "\\x04", .want = "\x04" },
.{ .in = "cr\\r", .want = "cr\r" },
.{ .in = "tab\\there", .want = "tab\there" },
};
for (cases) |cs| {
const got = try decodeEscapes(alloc, cs.in);
defer alloc.free(got);
try std.testing.expectEqualSlices(u8, cs.want, got);
}
try std.testing.expectError(error.BadEscape, decodeEscapes(alloc, "bad\\q"));
try std.testing.expectError(error.BadEscape, decodeEscapes(alloc, "trunc\\x1"));
try std.testing.expectError(error.BadEscape, decodeEscapes(alloc, "trailing\\"));
// parseInt would take the sign and decode this as 0x01.
try std.testing.expectError(error.BadEscape, decodeEscapes(alloc, "\\x+1"));
}
// Forces semantic analysis of every pub decl under `zig build test`, so an
// unreferenced decl must at least compile (the silent-module-loss hazard,
// decisions.md). Pub decls only: std.meta.declarations sees nothing private.
test {
std.testing.refAllDeclsRecursive(@This());
}