mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-31 20:59:02 +00:00
terminal/snapshot: clean up tests
This commit is contained in:
@@ -123,9 +123,12 @@ test "checkpoint BLAKE3-256 registry" {
|
||||
|
||||
var hasher = Blake3.init(.{});
|
||||
hasher.update("a");
|
||||
hasher.update("bc");
|
||||
var prefix: Digest = undefined;
|
||||
Blake3.hash("a", &prefix, .{});
|
||||
hasher.final(&actual);
|
||||
try std.testing.expectEqual(expected, actual);
|
||||
try std.testing.expectEqual(prefix, actual);
|
||||
|
||||
hasher.update("bc");
|
||||
hasher.final(&actual);
|
||||
try std.testing.expectEqual(expected, actual);
|
||||
}
|
||||
@@ -142,7 +145,6 @@ test "READY and FINISH checkpoint coverage" {
|
||||
var ready_digest: Digest = undefined;
|
||||
Blake3.hash(snapshot.written(), &ready_digest, .{});
|
||||
try encode(.ready, &snapshot);
|
||||
const ready_end = snapshot.written().len;
|
||||
|
||||
// History follows READY and is included by FINISH.
|
||||
try snapshot.writer.writeAll("history");
|
||||
@@ -160,19 +162,6 @@ test "READY and FINISH checkpoint coverage" {
|
||||
snapshot.written()[finish_offset..],
|
||||
);
|
||||
try decode(.finish, finish_digest, &finish_source);
|
||||
|
||||
// Including READY changes the FINISH digest even before history is added.
|
||||
var ready_record_digest: Digest = undefined;
|
||||
Blake3.hash(
|
||||
snapshot.written()[0..ready_end],
|
||||
&ready_record_digest,
|
||||
.{},
|
||||
);
|
||||
try testing.expect(!std.mem.eql(
|
||||
u8,
|
||||
&ready_digest,
|
||||
&ready_record_digest,
|
||||
));
|
||||
}
|
||||
|
||||
test "checkpoint rejects wrong tags, digests, and FINISH trailing data" {
|
||||
@@ -219,33 +208,3 @@ test "checkpoint rejects wrong tags, digests, and FINISH trailing data" {
|
||||
decode(.finish, digest, &trailing_source),
|
||||
);
|
||||
}
|
||||
|
||||
test "checkpoint rejects corruption and every truncation" {
|
||||
const testing = std.testing;
|
||||
|
||||
var snapshot: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer snapshot.deinit();
|
||||
try snapshot.writer.writeAll("prefix");
|
||||
const checkpoint_offset = snapshot.written().len;
|
||||
var digest: Digest = undefined;
|
||||
Blake3.hash(snapshot.written(), &digest, .{});
|
||||
try encode(.ready, &snapshot);
|
||||
const fixture = snapshot.written()[checkpoint_offset..];
|
||||
|
||||
const corrupt = try testing.allocator.dupe(u8, fixture);
|
||||
defer testing.allocator.free(corrupt);
|
||||
corrupt[record.Header.len] ^= 1;
|
||||
var corrupt_source: std.Io.Reader = .fixed(corrupt);
|
||||
try testing.expectError(
|
||||
error.InvalidChecksum,
|
||||
decode(.ready, digest, &corrupt_source),
|
||||
);
|
||||
|
||||
for (0..fixture.len) |fixture_len| {
|
||||
var source: std.Io.Reader = .fixed(fixture[0..fixture_len]);
|
||||
try testing.expectError(
|
||||
error.EndOfStream,
|
||||
decode(.ready, digest, &source),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,56 +668,60 @@ test "HISTORY rejects manifest mismatches" {
|
||||
|
||||
// Encode a manifest without PAGE records and vary only the fixed fields so
|
||||
// each structural validation boundary is exercised in isolation.
|
||||
const cases = .{
|
||||
const Case = struct {
|
||||
header: Header,
|
||||
expected: anyerror,
|
||||
};
|
||||
const cases = [_]Case{
|
||||
.{
|
||||
Header{
|
||||
.header = .{
|
||||
.key = .alternate,
|
||||
.page_count = 0,
|
||||
.total_rows = 0,
|
||||
.screen_overlap_rows = 0,
|
||||
},
|
||||
error.UnexpectedScreenKey,
|
||||
.expected = error.UnexpectedScreenKey,
|
||||
},
|
||||
.{
|
||||
Header{
|
||||
.header = .{
|
||||
.key = .primary,
|
||||
.page_count = 0,
|
||||
.total_rows = 1,
|
||||
.screen_overlap_rows = 1,
|
||||
},
|
||||
error.InvalidScreenOverlap,
|
||||
.expected = error.InvalidScreenOverlap,
|
||||
},
|
||||
.{
|
||||
Header{
|
||||
.header = .{
|
||||
.key = .primary,
|
||||
.page_count = 0,
|
||||
.total_rows = 1,
|
||||
.screen_overlap_rows = 0,
|
||||
},
|
||||
error.InvalidHistoryRows,
|
||||
.expected = error.InvalidHistoryRows,
|
||||
},
|
||||
.{
|
||||
Header{
|
||||
.header = .{
|
||||
.key = .primary,
|
||||
.page_count = 1,
|
||||
.total_rows = 1,
|
||||
.screen_overlap_rows = 0,
|
||||
},
|
||||
error.EndOfStream,
|
||||
.expected = error.EndOfStream,
|
||||
},
|
||||
};
|
||||
inline for (cases) |case| {
|
||||
for (cases) |case| {
|
||||
var destination: std.Io.Writer.Allocating = .init(
|
||||
std.testing.allocator,
|
||||
);
|
||||
defer destination.deinit();
|
||||
var record_writer = try record.Writer.init(&destination, .history);
|
||||
try case[0].encode(record_writer.payloadWriter());
|
||||
try case.header.encode(record_writer.payloadWriter());
|
||||
try record_writer.finish();
|
||||
|
||||
var source: std.Io.Reader = .fixed(destination.written());
|
||||
try std.testing.expectError(
|
||||
case[1],
|
||||
case.expected,
|
||||
decode(
|
||||
&source,
|
||||
std.testing.allocator,
|
||||
|
||||
@@ -357,7 +357,7 @@ test "decodePage ignores empty strings" {
|
||||
}
|
||||
|
||||
test "reject invalid kinds" {
|
||||
inline for (.{ 0, 3, std.math.maxInt(u8) }) |kind| {
|
||||
for ([_]u8{ 0, 3, std.math.maxInt(u8) }) |kind| {
|
||||
var fixture: [1]u8 = .{kind};
|
||||
var reader: std.Io.Reader = .fixed(&fixture);
|
||||
try std.testing.expectError(
|
||||
|
||||
@@ -94,7 +94,6 @@
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const envelope = @import("envelope.zig");
|
||||
const grid = @import("grid.zig");
|
||||
const hyperlink = @import("hyperlink.zig");
|
||||
const io = @import("io.zig");
|
||||
@@ -577,7 +576,7 @@ const test_empty_framed_page_fixture =
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00" ++
|
||||
"\x00";
|
||||
|
||||
test "golden encoding" {
|
||||
test "PAGE header golden encoding and decoding" {
|
||||
const header: Header = .{
|
||||
.columns = 0x0102,
|
||||
.rows = 0x0304,
|
||||
@@ -588,41 +587,20 @@ test "golden encoding" {
|
||||
.grapheme_capacity_bytes = 0x0d0e0f10,
|
||||
.string_capacity_bytes = 0x11121314,
|
||||
};
|
||||
|
||||
var buf: [Header.len]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try header.encode(&writer);
|
||||
|
||||
try std.testing.expectEqualStrings(
|
||||
"\x02\x01\x04\x03\x06\x05\x08\x07" ++
|
||||
"\x0a\x09\x0c\x0b\x10\x0f\x0e\x0d" ++
|
||||
"\x14\x13\x12\x11",
|
||||
writer.buffered(),
|
||||
);
|
||||
}
|
||||
|
||||
test "decode with a one-byte reader buffer" {
|
||||
const fixture =
|
||||
"\x02\x01\x04\x03\x06\x05\x08\x07" ++
|
||||
"\x0a\x09\x0c\x0b\x10\x0f\x0e\x0d" ++
|
||||
"\x14\x13\x12\x11";
|
||||
var source: std.Io.Reader = .fixed(fixture);
|
||||
var buf: [1]u8 = undefined;
|
||||
var limited = source.limited(.unlimited, &buf);
|
||||
|
||||
try std.testing.expectEqual(
|
||||
Header{
|
||||
.columns = 0x0102,
|
||||
.rows = 0x0304,
|
||||
.style_count = 0x0506,
|
||||
.hyperlink_count = 0x0708,
|
||||
.style_capacity = 0x090a,
|
||||
.hyperlink_capacity_bytes = 0x0b0c,
|
||||
.grapheme_capacity_bytes = 0x0d0e0f10,
|
||||
.string_capacity_bytes = 0x11121314,
|
||||
},
|
||||
try Header.decode(&limited.interface),
|
||||
);
|
||||
var buf: [Header.len]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try header.encode(&writer);
|
||||
try std.testing.expectEqualStrings(fixture, writer.buffered());
|
||||
|
||||
var source: std.Io.Reader = .fixed(fixture);
|
||||
var read_buf: [1]u8 = undefined;
|
||||
var limited = source.limited(.unlimited, &read_buf);
|
||||
try std.testing.expectEqual(header, try Header.decode(&limited.interface));
|
||||
}
|
||||
|
||||
test "reject every truncation" {
|
||||
@@ -870,41 +848,6 @@ test "framed PAGE encode and decode a sparse native page" {
|
||||
rewriter.buffered(),
|
||||
rewriter_again.buffered(),
|
||||
);
|
||||
|
||||
// The public codec appends one complete PAGE record after the envelope.
|
||||
var snapshot: std.Io.Writer.Allocating = .init(std.testing.allocator);
|
||||
defer snapshot.deinit();
|
||||
try envelope.encode(&snapshot.writer);
|
||||
try encode(&page, &snapshot);
|
||||
|
||||
const snapshot_bytes = snapshot.written();
|
||||
try std.testing.expectEqualStrings(
|
||||
test_page_fixture,
|
||||
snapshot_bytes[envelope.encoded_len + record.Header.len ..],
|
||||
);
|
||||
|
||||
var snapshot_reader: std.Io.Reader = .fixed(snapshot_bytes);
|
||||
try envelope.decode(&snapshot_reader);
|
||||
var framed_page = try decode(
|
||||
&snapshot_reader,
|
||||
std.testing.allocator,
|
||||
);
|
||||
defer framed_page.deinit();
|
||||
|
||||
try std.testing.expectEqual(header, Header.init(&framed_page));
|
||||
const framed_first = framed_page.getRowAndCell(0, 0);
|
||||
try std.testing.expectEqual(@as(u21, 'A'), framed_first.cell.codepoint());
|
||||
try std.testing.expectEqual(
|
||||
TerminalCell.SemanticContent.prompt,
|
||||
framed_first.cell.semantic_content,
|
||||
);
|
||||
try std.testing.expectEqualSlices(
|
||||
u21,
|
||||
&.{ 0x0301, 0x0302 },
|
||||
framed_page.lookupGrapheme(
|
||||
framed_page.getRowAndCell(0, 1).cell,
|
||||
).?,
|
||||
);
|
||||
}
|
||||
|
||||
test "framed PAGE golden empty record" {
|
||||
@@ -936,90 +879,22 @@ test "framed PAGE golden empty record" {
|
||||
);
|
||||
}
|
||||
|
||||
test "framed PAGE validates tag length checksum and exhaustion" {
|
||||
{
|
||||
// A valid non-PAGE tag is rejected before payload decoding.
|
||||
var wrong_tag = test_empty_framed_page_fixture.*;
|
||||
std.mem.writeInt(u16, wrong_tag[0..2], @intFromEnum(record.Tag.screen), .little);
|
||||
var reader: std.Io.Reader = .fixed(&wrong_tag);
|
||||
try std.testing.expectError(
|
||||
error.UnexpectedRecordTag,
|
||||
decode(&reader, std.testing.allocator),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// Corrupt only the stored checksum.
|
||||
var invalid_checksum = test_empty_framed_page_fixture.*;
|
||||
invalid_checksum[6] ^= 1;
|
||||
var reader: std.Io.Reader = .fixed(&invalid_checksum);
|
||||
try std.testing.expectError(
|
||||
error.InvalidChecksum,
|
||||
decode(&reader, std.testing.allocator),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// Mutate a cell without updating the checksum.
|
||||
var invalid_payload = test_empty_framed_page_fixture.*;
|
||||
const value_offset = record.Header.len +
|
||||
Header.len +
|
||||
1 +
|
||||
8;
|
||||
invalid_payload[value_offset] = 'A';
|
||||
var reader: std.Io.Reader = .fixed(&invalid_payload);
|
||||
try std.testing.expectError(
|
||||
error.InvalidChecksum,
|
||||
decode(&reader, std.testing.allocator),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// Advertise one byte less than the PAGE decoder requires.
|
||||
var short_payload = test_empty_framed_page_fixture.*;
|
||||
std.mem.writeInt(u32, short_payload[2..6], 36, .little);
|
||||
var reader: std.Io.Reader = .fixed(&short_payload);
|
||||
try std.testing.expectError(
|
||||
error.EndOfStream,
|
||||
decode(&reader, std.testing.allocator),
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
// Add a payload byte and recompute the checksum so exhaustion, rather
|
||||
// than checksum validation, is what fails.
|
||||
var trailing: [test_empty_framed_page_fixture.len + 1]u8 = undefined;
|
||||
@memcpy(
|
||||
trailing[0..test_empty_framed_page_fixture.len],
|
||||
test_empty_framed_page_fixture,
|
||||
);
|
||||
trailing[trailing.len - 1] = 0;
|
||||
std.mem.writeInt(u32, trailing[2..6], 38, .little);
|
||||
|
||||
var checksum: record.Checksum = .init(.page, 38);
|
||||
try checksum.writer().writeAll(trailing[record.Header.len..]);
|
||||
std.mem.writeInt(u32, trailing[6..10], checksum.final(), .little);
|
||||
|
||||
var reader: std.Io.Reader = .fixed(&trailing);
|
||||
try std.testing.expectError(
|
||||
error.PayloadNotExhausted,
|
||||
decode(&reader, std.testing.allocator),
|
||||
);
|
||||
}
|
||||
test "framed PAGE rejects a different record tag" {
|
||||
var wrong_tag = test_empty_framed_page_fixture.*;
|
||||
std.mem.writeInt(
|
||||
u16,
|
||||
wrong_tag[0..2],
|
||||
@intFromEnum(record.Tag.screen),
|
||||
.little,
|
||||
);
|
||||
var reader: std.Io.Reader = .fixed(&wrong_tag);
|
||||
try std.testing.expectError(
|
||||
error.UnexpectedRecordTag,
|
||||
decode(&reader, std.testing.allocator),
|
||||
);
|
||||
}
|
||||
|
||||
test "framed PAGE rejects every truncation and preserves following bytes" {
|
||||
for (0..test_empty_framed_page_fixture.len) |len| {
|
||||
var reader: std.Io.Reader = .fixed(
|
||||
test_empty_framed_page_fixture[0..len],
|
||||
);
|
||||
if (decode(&reader, std.testing.allocator)) |decoded_value| {
|
||||
var unexpected = decoded_value;
|
||||
unexpected.deinit();
|
||||
return error.ExpectedDecodeFailure;
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
test "framed PAGE preserves following bytes" {
|
||||
var source: std.Io.Reader = .fixed(
|
||||
test_empty_framed_page_fixture ++ "next",
|
||||
);
|
||||
@@ -1290,7 +1165,11 @@ test "decode rejects undefined row and cell values" {
|
||||
}
|
||||
|
||||
test "decode validates dimensions and native table capacities" {
|
||||
const cases = .{
|
||||
const Case = struct {
|
||||
expected: anyerror,
|
||||
header: Header,
|
||||
};
|
||||
const cases = [_]Case{
|
||||
.{
|
||||
.expected = error.InvalidDimensions,
|
||||
.header = Header{
|
||||
@@ -1345,7 +1224,7 @@ test "decode validates dimensions and native table capacities" {
|
||||
},
|
||||
};
|
||||
|
||||
inline for (cases) |case| {
|
||||
for (cases) |case| {
|
||||
var encoded: [Header.len]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&encoded);
|
||||
try case.header.encode(&writer);
|
||||
|
||||
@@ -1219,38 +1219,7 @@ fn testSavedCursor() SavedCursor {
|
||||
};
|
||||
}
|
||||
|
||||
fn expectHeaderRoundTrip(expected: Header) !void {
|
||||
var encoded: [Header.len]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&encoded);
|
||||
try expected.encode(&writer);
|
||||
|
||||
var reader: std.Io.Reader = .fixed(writer.buffered());
|
||||
const actual = try Header.decode(&reader);
|
||||
try std.testing.expectEqualDeep(expected, actual);
|
||||
}
|
||||
|
||||
fn expectHyperlinkEqual(
|
||||
expected: TerminalHyperlink,
|
||||
actual: TerminalHyperlink,
|
||||
) !void {
|
||||
try std.testing.expectEqualStrings(expected.uri, actual.uri);
|
||||
try std.testing.expectEqual(
|
||||
std.meta.activeTag(expected.id),
|
||||
std.meta.activeTag(actual.id),
|
||||
);
|
||||
switch (expected.id) {
|
||||
.implicit => |expected_id| try std.testing.expectEqual(
|
||||
expected_id,
|
||||
actual.id.implicit,
|
||||
),
|
||||
.explicit => |expected_id| try std.testing.expectEqualStrings(
|
||||
expected_id,
|
||||
actual.id.explicit,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
test "header golden encoding" {
|
||||
test "SCREEN header golden encoding and decoding" {
|
||||
try std.testing.expectEqual(Header.len, test_header_fixture.len);
|
||||
|
||||
var encoded: [Header.len]u8 = undefined;
|
||||
@@ -1261,9 +1230,7 @@ test "header golden encoding" {
|
||||
test_header_fixture,
|
||||
writer.buffered(),
|
||||
);
|
||||
}
|
||||
|
||||
test "header decoding with a one-byte reader buffer" {
|
||||
var source: std.Io.Reader = .fixed(test_header_fixture);
|
||||
var buffer: [1]u8 = undefined;
|
||||
var limited = source.limited(.unlimited, &buffer);
|
||||
@@ -1274,12 +1241,13 @@ test "header decoding with a one-byte reader buffer" {
|
||||
);
|
||||
}
|
||||
|
||||
test "header registry values round trip" {
|
||||
test "native enum values used by the SCREEN format" {
|
||||
const keys = [_]TerminalScreenKey{ .primary, .alternate };
|
||||
for (keys) |value| {
|
||||
var header = testHeader();
|
||||
header.key = value;
|
||||
try expectHeaderRoundTrip(header);
|
||||
for (keys, 0..) |value, expected| {
|
||||
try std.testing.expectEqual(
|
||||
expected,
|
||||
@as(usize, @intCast(@intFromEnum(value))),
|
||||
);
|
||||
}
|
||||
|
||||
const cursor_styles = [_]TerminalScreen.CursorStyle{
|
||||
@@ -1288,10 +1256,11 @@ test "header registry values round trip" {
|
||||
.underline,
|
||||
.block_hollow,
|
||||
};
|
||||
for (cursor_styles) |value| {
|
||||
var header = testHeader();
|
||||
header.cursor_style = value;
|
||||
try expectHeaderRoundTrip(header);
|
||||
for (cursor_styles, 0..) |value, expected| {
|
||||
try std.testing.expectEqual(
|
||||
expected,
|
||||
@as(usize, @intCast(@intFromEnum(value))),
|
||||
);
|
||||
}
|
||||
|
||||
const semantic_contents = [_]terminal_page.Cell.SemanticContent{
|
||||
@@ -1299,10 +1268,11 @@ test "header registry values round trip" {
|
||||
.input,
|
||||
.prompt,
|
||||
};
|
||||
for (semantic_contents) |value| {
|
||||
var header = testHeader();
|
||||
header.cursor_flags.semantic_content = value;
|
||||
try expectHeaderRoundTrip(header);
|
||||
for (semantic_contents, 0..) |value, expected| {
|
||||
try std.testing.expectEqual(
|
||||
expected,
|
||||
@as(usize, @intCast(@intFromEnum(value))),
|
||||
);
|
||||
}
|
||||
|
||||
const charsets = [_]terminal_charsets.Charset{
|
||||
@@ -1311,34 +1281,19 @@ test "header registry values round trip" {
|
||||
.british,
|
||||
.dec_special,
|
||||
};
|
||||
for (charsets) |value| {
|
||||
var header = testHeader();
|
||||
header.charset.charsets.set(.G0, value);
|
||||
header.charset.charsets.set(.G1, value);
|
||||
header.charset.charsets.set(.G2, value);
|
||||
header.charset.charsets.set(.G3, value);
|
||||
try expectHeaderRoundTrip(header);
|
||||
for (charsets, 0..) |value, expected| {
|
||||
try std.testing.expectEqual(
|
||||
expected,
|
||||
@as(usize, @intCast(@intFromEnum(value))),
|
||||
);
|
||||
}
|
||||
|
||||
const slots = [_]terminal_charsets.Slots{ .G0, .G1, .G2, .G3 };
|
||||
for (slots) |value| {
|
||||
var header = testHeader();
|
||||
header.charset.gl = value;
|
||||
header.charset.gr = value;
|
||||
try expectHeaderRoundTrip(header);
|
||||
}
|
||||
|
||||
const single_shifts = [_]?terminal_charsets.Slots{
|
||||
null,
|
||||
.G0,
|
||||
.G1,
|
||||
.G2,
|
||||
.G3,
|
||||
};
|
||||
for (single_shifts) |value| {
|
||||
var header = testHeader();
|
||||
header.charset.single_shift = value;
|
||||
try expectHeaderRoundTrip(header);
|
||||
for (slots, 0..) |value, expected| {
|
||||
try std.testing.expectEqual(
|
||||
expected,
|
||||
@as(usize, @intCast(@intFromEnum(value))),
|
||||
);
|
||||
}
|
||||
|
||||
const protected_modes = [_]terminal_ansi.ProtectedMode{
|
||||
@@ -1346,74 +1301,116 @@ test "header registry values round trip" {
|
||||
.iso,
|
||||
.dec,
|
||||
};
|
||||
for (protected_modes) |value| {
|
||||
var header = testHeader();
|
||||
header.protected_mode = value;
|
||||
try expectHeaderRoundTrip(header);
|
||||
for (protected_modes, 0..) |value, expected| {
|
||||
try std.testing.expectEqual(
|
||||
expected,
|
||||
@as(usize, @intCast(@intFromEnum(value))),
|
||||
);
|
||||
}
|
||||
|
||||
const semantic_clicks =
|
||||
[_]TerminalScreen.SemanticPrompt.SemanticClick{
|
||||
.none,
|
||||
.{ .click_events = .absolute },
|
||||
.{ .click_events = .relative },
|
||||
.{ .cl = .line },
|
||||
.{ .cl = .multiple },
|
||||
.{ .cl = .conservative_vertical },
|
||||
.{ .cl = .smart_vertical },
|
||||
};
|
||||
for (semantic_clicks) |value| {
|
||||
var header = testHeader();
|
||||
header.semantic_click = value;
|
||||
try expectHeaderRoundTrip(header);
|
||||
const click_kinds = [_]TerminalScreen.SemanticPrompt.SemanticClickKind{
|
||||
.none,
|
||||
.click_events,
|
||||
.cl,
|
||||
};
|
||||
for (click_kinds, 0..) |value, expected| {
|
||||
try std.testing.expectEqual(
|
||||
expected,
|
||||
@as(usize, @intCast(@intFromEnum(value))),
|
||||
);
|
||||
}
|
||||
|
||||
const click_events = [_]terminal_osc.semantic_prompt.ClickEvents{
|
||||
.absolute,
|
||||
.relative,
|
||||
};
|
||||
for (click_events, 0..) |value, expected| {
|
||||
try std.testing.expectEqual(
|
||||
expected,
|
||||
@as(usize, @intCast(@intFromEnum(value))),
|
||||
);
|
||||
}
|
||||
|
||||
const clicks = [_]terminal_osc.semantic_prompt.Click{
|
||||
.line,
|
||||
.multiple,
|
||||
.conservative_vertical,
|
||||
.smart_vertical,
|
||||
};
|
||||
for (clicks, 0..) |value, expected| {
|
||||
try std.testing.expectEqual(
|
||||
expected,
|
||||
@as(usize, @intCast(@intFromEnum(value))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "cursor, Kitty, and saved cursor flag bit layouts" {
|
||||
const cursor_cases = .{
|
||||
.{ CursorFlags{ .pending_wrap = true }, @as(u8, 1 << 0) },
|
||||
.{ CursorFlags{ .protected = true }, @as(u8, 1 << 1) },
|
||||
const CursorCase = struct {
|
||||
value: CursorFlags,
|
||||
expected: u8,
|
||||
};
|
||||
const cursor_cases = [_]CursorCase{
|
||||
.{ .value = .{ .pending_wrap = true }, .expected = 1 << 0 },
|
||||
.{ .value = .{ .protected = true }, .expected = 1 << 1 },
|
||||
.{
|
||||
CursorFlags{ .semantic_content = .input },
|
||||
@as(u8, 1 << 2),
|
||||
.value = .{ .semantic_content = .input },
|
||||
.expected = 1 << 2,
|
||||
},
|
||||
.{
|
||||
CursorFlags{ .semantic_content = .prompt },
|
||||
@as(u8, 2 << 2),
|
||||
.value = .{ .semantic_content = .prompt },
|
||||
.expected = 2 << 2,
|
||||
},
|
||||
.{
|
||||
CursorFlags{ .semantic_content_clear_eol = true },
|
||||
@as(u8, 1 << 4),
|
||||
.value = .{ .semantic_content_clear_eol = true },
|
||||
.expected = 1 << 4,
|
||||
},
|
||||
};
|
||||
inline for (cursor_cases) |case| {
|
||||
try std.testing.expectEqual(case[1], @as(u8, @bitCast(case[0])));
|
||||
for (cursor_cases) |case| {
|
||||
try std.testing.expectEqual(
|
||||
case.expected,
|
||||
@as(u8, @bitCast(case.value)),
|
||||
);
|
||||
}
|
||||
|
||||
const kitty_cases = .{
|
||||
.{ KittyKeyboard.Flags{ .disambiguate = true }, @as(u8, 1 << 0) },
|
||||
.{ KittyKeyboard.Flags{ .report_events = true }, @as(u8, 1 << 1) },
|
||||
const KittyCase = struct {
|
||||
value: KittyKeyboard.Flags,
|
||||
expected: u8,
|
||||
};
|
||||
const kitty_cases = [_]KittyCase{
|
||||
.{ .value = .{ .disambiguate = true }, .expected = 1 << 0 },
|
||||
.{ .value = .{ .report_events = true }, .expected = 1 << 1 },
|
||||
.{
|
||||
KittyKeyboard.Flags{ .report_alternates = true },
|
||||
@as(u8, 1 << 2),
|
||||
.value = .{ .report_alternates = true },
|
||||
.expected = 1 << 2,
|
||||
},
|
||||
.{ KittyKeyboard.Flags{ .report_all = true }, @as(u8, 1 << 3) },
|
||||
.{ .value = .{ .report_all = true }, .expected = 1 << 3 },
|
||||
.{
|
||||
KittyKeyboard.Flags{ .report_associated = true },
|
||||
@as(u8, 1 << 4),
|
||||
.value = .{ .report_associated = true },
|
||||
.expected = 1 << 4,
|
||||
},
|
||||
};
|
||||
inline for (kitty_cases) |case| {
|
||||
try std.testing.expectEqual(case[1], @as(u8, @bitCast(case[0])));
|
||||
for (kitty_cases) |case| {
|
||||
try std.testing.expectEqual(
|
||||
case.expected,
|
||||
@as(u8, @bitCast(case.value)),
|
||||
);
|
||||
}
|
||||
|
||||
const saved_cases = .{
|
||||
.{ SavedCursor.Flags{ .protected = true }, @as(u8, 1 << 0) },
|
||||
.{ SavedCursor.Flags{ .pending_wrap = true }, @as(u8, 1 << 1) },
|
||||
.{ SavedCursor.Flags{ .origin = true }, @as(u8, 1 << 2) },
|
||||
const SavedCase = struct {
|
||||
value: SavedCursor.Flags,
|
||||
expected: u8,
|
||||
};
|
||||
inline for (saved_cases) |case| {
|
||||
try std.testing.expectEqual(case[1], @as(u8, @bitCast(case[0])));
|
||||
const saved_cases = [_]SavedCase{
|
||||
.{ .value = .{ .protected = true }, .expected = 1 << 0 },
|
||||
.{ .value = .{ .pending_wrap = true }, .expected = 1 << 1 },
|
||||
.{ .value = .{ .origin = true }, .expected = 1 << 2 },
|
||||
};
|
||||
for (saved_cases) |case| {
|
||||
try std.testing.expectEqual(
|
||||
case.expected,
|
||||
@as(u8, @bitCast(case.value)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1543,12 +1540,12 @@ test "header decoding rejects invalid values" {
|
||||
Header.decode(&semantic_click_kind_reader),
|
||||
);
|
||||
|
||||
const invalid_semantic_clicks = .{
|
||||
.{ @as(u8, 0), @as(u8, 1) },
|
||||
.{ @as(u8, 1), @as(u8, 2) },
|
||||
.{ @as(u8, 2), @as(u8, 4) },
|
||||
const invalid_semantic_clicks = [_][2]u8{
|
||||
.{ 0, 1 },
|
||||
.{ 1, 2 },
|
||||
.{ 2, 4 },
|
||||
};
|
||||
inline for (invalid_semantic_clicks) |invalid| {
|
||||
for (invalid_semantic_clicks) |invalid| {
|
||||
var fixture = valid;
|
||||
fixture[42] = invalid[0];
|
||||
fixture[43] = invalid[1];
|
||||
@@ -1678,7 +1675,7 @@ test "saved cursor decoding rejects every truncation" {
|
||||
}
|
||||
}
|
||||
|
||||
test "cursor hyperlink encoding and decoding" {
|
||||
test "cursor hyperlink null encoding and decoding" {
|
||||
var none_encoded: [1]u8 = undefined;
|
||||
var none_writer: std.Io.Writer = .fixed(&none_encoded);
|
||||
try encodeCursorHyperlink(null, &none_writer);
|
||||
@@ -1689,31 +1686,6 @@ test "cursor hyperlink encoding and decoding" {
|
||||
null,
|
||||
try decodeCursorHyperlink(&none_reader, std.testing.allocator),
|
||||
);
|
||||
|
||||
const values = [_]TerminalHyperlink{
|
||||
.{
|
||||
.id = .{ .implicit = 0x01020304 },
|
||||
.uri = "implicit",
|
||||
},
|
||||
.{
|
||||
.id = .{ .explicit = "id" },
|
||||
.uri = "explicit",
|
||||
},
|
||||
};
|
||||
|
||||
for (values) |expected| {
|
||||
var encoded: [128]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&encoded);
|
||||
try encodeCursorHyperlink(expected, &writer);
|
||||
|
||||
var reader: std.Io.Reader = .fixed(writer.buffered());
|
||||
var actual = (try decodeCursorHyperlink(
|
||||
&reader,
|
||||
std.testing.allocator,
|
||||
)).?;
|
||||
defer actual.deinit(std.testing.allocator);
|
||||
try expectHyperlinkEqual(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
test "framed native SCREEN and PAGE sequence" {
|
||||
@@ -1797,44 +1769,6 @@ test "framed native SCREEN and PAGE sequence" {
|
||||
destination.written()[0..6],
|
||||
);
|
||||
|
||||
var source: std.Io.Reader = .fixed(destination.written()[6..]);
|
||||
var record_reader: record.Reader = undefined;
|
||||
try record_reader.init(&source);
|
||||
try std.testing.expectEqual(record.Tag.screen, record_reader.header.tag);
|
||||
|
||||
const payload_reader = record_reader.payloadReader();
|
||||
var expected_header = testHeader();
|
||||
expected_header.page_count = 1;
|
||||
expected_header.cursor_x = 7;
|
||||
expected_header.cursor_y = 6;
|
||||
expected_header.hyperlink_implicit_id = 0x0a0b0c0e;
|
||||
try std.testing.expectEqualDeep(
|
||||
expected_header,
|
||||
try Header.decode(payload_reader),
|
||||
);
|
||||
try std.testing.expectEqualDeep(
|
||||
testSavedCursor(),
|
||||
try SavedCursor.decode(payload_reader),
|
||||
);
|
||||
|
||||
const expected_hyperlink: TerminalHyperlink = .{
|
||||
.id = .{ .implicit = 0x0a0b0c0d },
|
||||
.uri = "cursor-uri",
|
||||
};
|
||||
var actual_hyperlink = (try decodeCursorHyperlink(
|
||||
payload_reader,
|
||||
std.testing.allocator,
|
||||
)).?;
|
||||
defer actual_hyperlink.deinit(std.testing.allocator);
|
||||
try expectHyperlinkEqual(expected_hyperlink, actual_hyperlink);
|
||||
try record_reader.finish();
|
||||
|
||||
var decoded_page = try page.decode(&source, std.testing.allocator);
|
||||
defer decoded_page.deinit();
|
||||
try std.testing.expectEqual(@as(u16, 8), decoded_page.size.cols);
|
||||
try std.testing.expectEqual(@as(u16, 8), decoded_page.size.rows);
|
||||
try std.testing.expectError(error.EndOfStream, source.takeByte());
|
||||
|
||||
// The complete sequence restores directly into native Screen/PageList
|
||||
// state, including page-local cursor references.
|
||||
var restore_source: std.Io.Reader = .fixed(destination.written()[6..]);
|
||||
@@ -1868,10 +1802,18 @@ test "framed native SCREEN and PAGE sequence" {
|
||||
@as(u32, 0x0a0b0c0e),
|
||||
restored.cursor.hyperlink_implicit_id,
|
||||
);
|
||||
try expectHyperlinkEqual(
|
||||
expected_hyperlink,
|
||||
restored.cursor.hyperlink.?.*,
|
||||
const restored_hyperlink = restored.cursor.hyperlink.?.*;
|
||||
try std.testing.expectEqualStrings(
|
||||
"cursor-uri",
|
||||
restored_hyperlink.uri,
|
||||
);
|
||||
switch (restored_hyperlink.id) {
|
||||
.implicit => |id| try std.testing.expectEqual(
|
||||
@as(u32, 0x0a0b0c0d),
|
||||
id,
|
||||
),
|
||||
.explicit => try std.testing.expect(false),
|
||||
}
|
||||
|
||||
const restored_saved = restored.saved_cursor.?;
|
||||
try std.testing.expectEqual(@as(u16, 0x0102), restored_saved.x);
|
||||
@@ -2138,62 +2080,6 @@ test "SCREEN sequence failure preserves preceding bytes" {
|
||||
try std.testing.expectEqualStrings("prefix", destination.written());
|
||||
}
|
||||
|
||||
test "cursor hyperlink rejects invalid kind and every truncation" {
|
||||
var invalid_kind_reader: std.Io.Reader = .fixed("\x03");
|
||||
try std.testing.expectError(
|
||||
error.InvalidKind,
|
||||
decodeCursorHyperlink(
|
||||
&invalid_kind_reader,
|
||||
std.testing.allocator,
|
||||
),
|
||||
);
|
||||
|
||||
const empty_cases = [_]struct {
|
||||
fixture: []const u8,
|
||||
expected: anyerror,
|
||||
}{
|
||||
.{
|
||||
.fixture = "\x01\x04\x03\x02\x01\x00\x00\x00\x00",
|
||||
.expected = error.InvalidUri,
|
||||
},
|
||||
.{
|
||||
.fixture = "\x02\x00\x00\x00\x00",
|
||||
.expected = error.InvalidExplicitId,
|
||||
},
|
||||
.{
|
||||
.fixture = "\x02\x02\x00\x00\x00id\x00\x00\x00\x00",
|
||||
.expected = error.InvalidUri,
|
||||
},
|
||||
};
|
||||
for (empty_cases) |case| {
|
||||
var reader: std.Io.Reader = .fixed(case.fixture);
|
||||
try std.testing.expectError(
|
||||
case.expected,
|
||||
decodeCursorHyperlink(
|
||||
&reader,
|
||||
std.testing.allocator,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const fixtures = .{
|
||||
"\x01\x04\x03\x02\x01\x03\x00\x00\x00uri",
|
||||
"\x02\x02\x00\x00\x00id\x03\x00\x00\x00uri",
|
||||
};
|
||||
inline for (fixtures) |fixture| {
|
||||
for (0..fixture.len) |fixture_len| {
|
||||
var reader: std.Io.Reader = .fixed(fixture[0..fixture_len]);
|
||||
try std.testing.expectError(
|
||||
error.EndOfStream,
|
||||
decodeCursorHyperlink(
|
||||
&reader,
|
||||
std.testing.allocator,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "SCREEN decode rejects an empty cursor hyperlink URI" {
|
||||
var destination: std.Io.Writer.Allocating = .init(
|
||||
std.testing.allocator,
|
||||
|
||||
@@ -84,6 +84,9 @@ pub const DecodeError = std.Io.Reader.Error || error{
|
||||
/// A color kind is not defined by snapshot version 1.
|
||||
InvalidColorKind,
|
||||
|
||||
/// Bytes unused by the selected color kind are not zero.
|
||||
InvalidColor,
|
||||
|
||||
/// The encoded underline kind is not defined by snapshot version 1.
|
||||
InvalidUnderline,
|
||||
|
||||
@@ -187,8 +190,14 @@ fn decodeColor(
|
||||
};
|
||||
|
||||
return switch (kind) {
|
||||
.none => .none,
|
||||
.palette => .{ .palette = encoded[1] },
|
||||
.none => if (std.mem.eql(u8, encoded[1..], &.{ 0, 0, 0 }))
|
||||
.none
|
||||
else
|
||||
error.InvalidColor,
|
||||
.palette => if (encoded[2] == 0 and encoded[3] == 0)
|
||||
.{ .palette = encoded[1] }
|
||||
else
|
||||
error.InvalidColor,
|
||||
.rgb => .{ .rgb = .{
|
||||
.r = encoded[1],
|
||||
.g = encoded[2],
|
||||
@@ -206,7 +215,7 @@ fn computeLen() usize {
|
||||
}
|
||||
}
|
||||
|
||||
test "golden encoding" {
|
||||
test "golden encoding and decoding" {
|
||||
const value: terminal_style.Style = .{
|
||||
.fg_color = .none,
|
||||
.bg_color = .{ .palette = 0x7f },
|
||||
@@ -227,103 +236,100 @@ test "golden encoding" {
|
||||
.underline = .curly,
|
||||
},
|
||||
};
|
||||
|
||||
var buf: [len]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try encode(value, &writer);
|
||||
|
||||
try std.testing.expectEqualStrings(
|
||||
"\x00\x00\x00\x00" ++
|
||||
"\x01\x7f\x00\x00" ++
|
||||
"\x02\x12\x34\x56" ++
|
||||
"\xff\x03\x00\x00",
|
||||
writer.buffered(),
|
||||
);
|
||||
}
|
||||
|
||||
test "flag bit layout" {
|
||||
const cases = .{
|
||||
.{ Flags{ .bold = true }, @as(u16, 1 << 0) },
|
||||
.{ Flags{ .italic = true }, @as(u16, 1 << 1) },
|
||||
.{ Flags{ .faint = true }, @as(u16, 1 << 2) },
|
||||
.{ Flags{ .blink = true }, @as(u16, 1 << 3) },
|
||||
.{ Flags{ .inverse = true }, @as(u16, 1 << 4) },
|
||||
.{ Flags{ .invisible = true }, @as(u16, 1 << 5) },
|
||||
.{ Flags{ .strikethrough = true }, @as(u16, 1 << 6) },
|
||||
.{ Flags{ .overline = true }, @as(u16, 1 << 7) },
|
||||
.{
|
||||
Flags{ .underline = @intFromEnum(sgr.Attribute.Underline.single) },
|
||||
@as(u16, 1 << 8),
|
||||
},
|
||||
.{
|
||||
Flags{ .underline = @intFromEnum(sgr.Attribute.Underline.double) },
|
||||
@as(u16, 2 << 8),
|
||||
},
|
||||
.{
|
||||
Flags{ .underline = @intFromEnum(sgr.Attribute.Underline.curly) },
|
||||
@as(u16, 3 << 8),
|
||||
},
|
||||
.{
|
||||
Flags{ .underline = @intFromEnum(sgr.Attribute.Underline.dotted) },
|
||||
@as(u16, 4 << 8),
|
||||
},
|
||||
.{
|
||||
Flags{ .underline = @intFromEnum(sgr.Attribute.Underline.dashed) },
|
||||
@as(u16, 5 << 8),
|
||||
},
|
||||
.{ Flags{ .reserved = 1 }, @as(u16, 1 << 11) },
|
||||
};
|
||||
|
||||
inline for (cases) |case| {
|
||||
try std.testing.expectEqual(case[1], @as(u16, @bitCast(case[0])));
|
||||
}
|
||||
}
|
||||
|
||||
test "decoding" {
|
||||
const fixture =
|
||||
"\x00\x00\x00\x00" ++
|
||||
"\x01\x7f\x00\x00" ++
|
||||
"\x02\x12\x34\x56" ++
|
||||
"\xff\x03\x00\x00";
|
||||
var source: std.Io.Reader = .fixed(fixture);
|
||||
var buf: [1]u8 = undefined;
|
||||
var limited = source.limited(.unlimited, &buf);
|
||||
|
||||
const expected: terminal_style.Style = .{
|
||||
.fg_color = .none,
|
||||
.bg_color = .{ .palette = 0x7f },
|
||||
.underline_color = .{ .rgb = .{
|
||||
.r = 0x12,
|
||||
.g = 0x34,
|
||||
.b = 0x56,
|
||||
} },
|
||||
.flags = .{
|
||||
.bold = true,
|
||||
.italic = true,
|
||||
.faint = true,
|
||||
.blink = true,
|
||||
.inverse = true,
|
||||
.invisible = true,
|
||||
.strikethrough = true,
|
||||
.overline = true,
|
||||
.underline = .curly,
|
||||
},
|
||||
};
|
||||
const actual = try decode(&limited.interface);
|
||||
try std.testing.expect(expected.eql(actual));
|
||||
var buf: [len]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try encode(value, &writer);
|
||||
try std.testing.expectEqualStrings(fixture, writer.buffered());
|
||||
|
||||
var source: std.Io.Reader = .fixed(fixture);
|
||||
var read_buf: [1]u8 = undefined;
|
||||
var limited = source.limited(.unlimited, &read_buf);
|
||||
try std.testing.expect(value.eql(try decode(&limited.interface)));
|
||||
}
|
||||
|
||||
test "reject invalid color kinds" {
|
||||
inline for (.{ 0, 4, 8 }) |offset| {
|
||||
test "flag bit layout" {
|
||||
const Case = struct {
|
||||
value: Flags,
|
||||
expected: u16,
|
||||
};
|
||||
const cases = [_]Case{
|
||||
.{ .value = .{ .bold = true }, .expected = 1 << 0 },
|
||||
.{ .value = .{ .italic = true }, .expected = 1 << 1 },
|
||||
.{ .value = .{ .faint = true }, .expected = 1 << 2 },
|
||||
.{ .value = .{ .blink = true }, .expected = 1 << 3 },
|
||||
.{ .value = .{ .inverse = true }, .expected = 1 << 4 },
|
||||
.{ .value = .{ .invisible = true }, .expected = 1 << 5 },
|
||||
.{ .value = .{ .strikethrough = true }, .expected = 1 << 6 },
|
||||
.{ .value = .{ .overline = true }, .expected = 1 << 7 },
|
||||
.{
|
||||
.value = .{
|
||||
.underline = @intFromEnum(sgr.Attribute.Underline.single),
|
||||
},
|
||||
.expected = 1 << 8,
|
||||
},
|
||||
.{
|
||||
.value = .{
|
||||
.underline = @intFromEnum(sgr.Attribute.Underline.double),
|
||||
},
|
||||
.expected = 2 << 8,
|
||||
},
|
||||
.{
|
||||
.value = .{
|
||||
.underline = @intFromEnum(sgr.Attribute.Underline.curly),
|
||||
},
|
||||
.expected = 3 << 8,
|
||||
},
|
||||
.{
|
||||
.value = .{
|
||||
.underline = @intFromEnum(sgr.Attribute.Underline.dotted),
|
||||
},
|
||||
.expected = 4 << 8,
|
||||
},
|
||||
.{
|
||||
.value = .{
|
||||
.underline = @intFromEnum(sgr.Attribute.Underline.dashed),
|
||||
},
|
||||
.expected = 5 << 8,
|
||||
},
|
||||
.{ .value = .{ .reserved = 1 }, .expected = 1 << 11 },
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
try std.testing.expectEqual(
|
||||
case.expected,
|
||||
@as(u16, @bitCast(case.value)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "reject invalid colors" {
|
||||
for ([_]usize{ 0, 4, 8 }) |offset| {
|
||||
var invalid_kind: [len]u8 = @splat(0);
|
||||
invalid_kind[offset] = 3;
|
||||
var reader: std.Io.Reader = .fixed(&invalid_kind);
|
||||
try std.testing.expectError(error.InvalidColorKind, decode(&reader));
|
||||
}
|
||||
|
||||
var invalid_none: [len]u8 = @splat(0);
|
||||
invalid_none[1] = 1;
|
||||
var none_reader: std.Io.Reader = .fixed(&invalid_none);
|
||||
try std.testing.expectError(error.InvalidColor, decode(&none_reader));
|
||||
|
||||
var invalid_palette: [len]u8 = @splat(0);
|
||||
invalid_palette[0] = @intFromEnum(ColorKind.palette);
|
||||
invalid_palette[2] = 1;
|
||||
var palette_reader: std.Io.Reader = .fixed(&invalid_palette);
|
||||
try std.testing.expectError(error.InvalidColor, decode(&palette_reader));
|
||||
}
|
||||
|
||||
test "reject invalid flags and reserved field" {
|
||||
inline for (.{ 6, 7 }) |underline| {
|
||||
for ([_]u16{ 6, 7 }) |underline| {
|
||||
var invalid_underline: [len]u8 = @splat(0);
|
||||
std.mem.writeInt(
|
||||
u16,
|
||||
|
||||
@@ -1195,16 +1195,11 @@ const test_header_fixture =
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff" ++
|
||||
"\x08\x07\x06\x05\x04\x03\x02\x01";
|
||||
|
||||
test "TERMINAL payload type registries" {
|
||||
try std.testing.expectEqual(@as(usize, 103), Header.len);
|
||||
test "TERMINAL mode bit layout" {
|
||||
try std.testing.expectEqual(
|
||||
@as(usize, 42),
|
||||
@bitSizeOf(terminal_modes.ModePacked),
|
||||
);
|
||||
try std.testing.expectEqual(
|
||||
@as(usize, 8),
|
||||
@sizeOf(terminal_modes.ModePacked),
|
||||
);
|
||||
|
||||
var first: terminal_modes.ModePacked = std.mem.zeroes(
|
||||
terminal_modes.ModePacked,
|
||||
@@ -1232,15 +1227,6 @@ test "TERMINAL payload type registries" {
|
||||
@as(u42, 1) << 41,
|
||||
@as(u42, @bitCast(last)),
|
||||
);
|
||||
|
||||
try std.testing.expectEqual(
|
||||
@as(u1, 0),
|
||||
@intFromEnum(terminal_ansi.StatusDisplay.main),
|
||||
);
|
||||
try std.testing.expectEqual(
|
||||
@as(c_int, 33),
|
||||
@intFromEnum(terminal_mouse.Shape.zoom_out),
|
||||
);
|
||||
}
|
||||
|
||||
test "TERMINAL header golden encoding and decoding" {
|
||||
@@ -1297,28 +1283,33 @@ test "TERMINAL header rejects invalid values" {
|
||||
|
||||
// Single-byte registries and boolean encodings reject every value just
|
||||
// beyond their current snapshot range.
|
||||
const byte_cases = .{
|
||||
.{ @as(usize, 20), @as(u8, 2), error.InvalidStatusDisplay },
|
||||
.{ @as(usize, 21), @as(u8, 2), error.InvalidActiveScreenKey },
|
||||
.{ @as(usize, 29), @as(u8, 2), error.InvalidCursorIsDefault },
|
||||
.{ @as(usize, 30), @as(u8, 4), error.InvalidCursorStyle },
|
||||
.{ @as(usize, 31), @as(u8, 3), error.InvalidCursorBlink },
|
||||
.{ @as(usize, 32), @as(u8, 3), error.InvalidShellRedraw },
|
||||
.{ @as(usize, 33), @as(u8, 2), error.InvalidModifyOtherKeys },
|
||||
.{ @as(usize, 34), @as(u8, 5), error.InvalidMouseEvent },
|
||||
.{ @as(usize, 35), @as(u8, 5), error.InvalidMouseFormat },
|
||||
.{ @as(usize, 36), @as(u8, 3), error.InvalidMouseShiftCapture },
|
||||
.{ @as(usize, 37), @as(u8, 34), error.InvalidMouseShape },
|
||||
.{ @as(usize, 38), @as(u8, 2), error.InvalidPasswordInput },
|
||||
.{ @as(usize, 44), @as(u8, 4), error.InvalidModes },
|
||||
.{ @as(usize, 63), @as(u8, 2), error.InvalidDynamicRGB },
|
||||
.{ @as(usize, 68), @as(u8, 1), error.InvalidDynamicRGB },
|
||||
const ByteCase = struct {
|
||||
offset: usize,
|
||||
value: u8,
|
||||
expected: anyerror,
|
||||
};
|
||||
inline for (byte_cases) |case| {
|
||||
const byte_cases = [_]ByteCase{
|
||||
.{ .offset = 20, .value = 2, .expected = error.InvalidStatusDisplay },
|
||||
.{ .offset = 21, .value = 2, .expected = error.InvalidActiveScreenKey },
|
||||
.{ .offset = 29, .value = 2, .expected = error.InvalidCursorIsDefault },
|
||||
.{ .offset = 30, .value = 4, .expected = error.InvalidCursorStyle },
|
||||
.{ .offset = 31, .value = 3, .expected = error.InvalidCursorBlink },
|
||||
.{ .offset = 32, .value = 3, .expected = error.InvalidShellRedraw },
|
||||
.{ .offset = 33, .value = 2, .expected = error.InvalidModifyOtherKeys },
|
||||
.{ .offset = 34, .value = 5, .expected = error.InvalidMouseEvent },
|
||||
.{ .offset = 35, .value = 5, .expected = error.InvalidMouseFormat },
|
||||
.{ .offset = 36, .value = 3, .expected = error.InvalidMouseShiftCapture },
|
||||
.{ .offset = 37, .value = 34, .expected = error.InvalidMouseShape },
|
||||
.{ .offset = 38, .value = 2, .expected = error.InvalidPasswordInput },
|
||||
.{ .offset = 44, .value = 4, .expected = error.InvalidModes },
|
||||
.{ .offset = 63, .value = 2, .expected = error.InvalidDynamicRGB },
|
||||
.{ .offset = 68, .value = 1, .expected = error.InvalidDynamicRGB },
|
||||
};
|
||||
for (byte_cases) |case| {
|
||||
var fixture = test_header_fixture.*;
|
||||
fixture[case[0]] = case[1];
|
||||
fixture[case.offset] = case.value;
|
||||
var reader: std.Io.Reader = .fixed(&fixture);
|
||||
try testing.expectError(case[2], Header.decode(&reader));
|
||||
try testing.expectError(case.expected, Header.decode(&reader));
|
||||
}
|
||||
|
||||
// Cross-field and multi-byte invariants are validated after decoding.
|
||||
@@ -1480,7 +1471,7 @@ test "TERMINAL payload rejects noncanonical state" {
|
||||
try testing.expectEqual(@as(usize, 0), writer.end);
|
||||
}
|
||||
|
||||
test "TERMINAL payload rejects padding and every truncation" {
|
||||
test "TERMINAL payload rejects tab-stop padding" {
|
||||
const testing = std.testing;
|
||||
|
||||
var tabstops = try TerminalTabstops.init(
|
||||
@@ -1519,16 +1510,6 @@ test "TERMINAL payload rejects padding and every truncation" {
|
||||
error.InvalidTabStops,
|
||||
decodePayload(&padding_reader, testing.allocator),
|
||||
);
|
||||
|
||||
for (0..destination.written().len) |fixture_len| {
|
||||
var reader: std.Io.Reader = .fixed(
|
||||
destination.written()[0..fixture_len],
|
||||
);
|
||||
try testing.expectError(
|
||||
error.EndOfStream,
|
||||
decodePayload(&reader, testing.allocator),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "TERMINAL record encodes native terminal state" {
|
||||
@@ -1579,87 +1560,14 @@ test "TERMINAL record encodes native terminal state" {
|
||||
try terminal.setPwd("file:///tmp/native");
|
||||
try terminal.setTitle("native title");
|
||||
|
||||
// Decode the generated framing and payload independently.
|
||||
// Encode and restore through the public record codec.
|
||||
var destination: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer destination.deinit();
|
||||
try encode(&terminal, &destination);
|
||||
|
||||
var source: std.Io.Reader = .fixed(destination.written());
|
||||
var record_reader: record.Reader = undefined;
|
||||
try record_reader.init(&source);
|
||||
try testing.expectEqual(record.Tag.terminal, record_reader.header.tag);
|
||||
var decoded = try decodePayload(
|
||||
record_reader.payloadReader(),
|
||||
testing.allocator,
|
||||
);
|
||||
defer decoded.deinit(testing.allocator);
|
||||
try record_reader.finish();
|
||||
|
||||
// Native screen routing and policy are derived rather than caller-supplied.
|
||||
try testing.expectEqual(@as(u16, 10), decoded.header.columns);
|
||||
try testing.expectEqual(@as(u16, 3), decoded.header.rows);
|
||||
try testing.expectEqual(@as(u32, 800), decoded.header.width_px);
|
||||
try testing.expectEqual(@as(u32, 600), decoded.header.height_px);
|
||||
try testing.expectEqual(
|
||||
terminal.scrolling_region,
|
||||
Terminal.ScrollingRegion{
|
||||
.top = decoded.header.scrolling_region_top,
|
||||
.bottom = decoded.header.scrolling_region_bottom,
|
||||
.left = decoded.header.scrolling_region_left,
|
||||
.right = decoded.header.scrolling_region_right,
|
||||
},
|
||||
);
|
||||
try testing.expectEqual(terminal.status_display, decoded.header.status_display);
|
||||
try testing.expectEqual(.primary, decoded.header.active_screen_key);
|
||||
try testing.expectEqual(@as(u16, 1), decoded.header.screen_count);
|
||||
try testing.expectEqual(@as(?u21, 'X'), decoded.header.previous_codepoint);
|
||||
try testing.expectEqual(terminal.cursor.is_default, decoded.header.cursor_is_default);
|
||||
try testing.expectEqual(
|
||||
terminal.cursor.default_style,
|
||||
decoded.header.cursor_default_style,
|
||||
);
|
||||
try testing.expectEqual(
|
||||
terminal.cursor.default_blink,
|
||||
decoded.header.cursor_default_blink,
|
||||
);
|
||||
try testing.expectEqual(
|
||||
terminal.flags.shell_redraws_prompt,
|
||||
decoded.header.shell_redraw,
|
||||
);
|
||||
try testing.expectEqual(
|
||||
terminal.flags.modify_other_keys_2,
|
||||
decoded.header.modify_other_keys_2,
|
||||
);
|
||||
try testing.expectEqual(terminal.flags.mouse_event, decoded.header.mouse_event);
|
||||
try testing.expectEqual(
|
||||
terminal.flags.mouse_format,
|
||||
decoded.header.mouse_format,
|
||||
);
|
||||
try testing.expectEqual(@as(?bool, true), decoded.header.mouse_shift_capture);
|
||||
try testing.expectEqual(terminal.mouse_shape, decoded.header.mouse_shape);
|
||||
try testing.expectEqual(
|
||||
terminal.flags.password_input,
|
||||
decoded.header.password_input,
|
||||
);
|
||||
try testing.expectEqualDeep(terminal.modes.values, decoded.header.current_modes);
|
||||
try testing.expectEqualDeep(terminal.modes.saved, decoded.header.saved_modes);
|
||||
try testing.expectEqualDeep(terminal.modes.default, decoded.header.default_modes);
|
||||
try testing.expectEqualDeep(
|
||||
terminal.colors.background,
|
||||
decoded.header.background,
|
||||
);
|
||||
try testing.expectEqualDeep(terminal.colors.palette, decoded.palette);
|
||||
try testing.expectEqual(@as(?u64, 12_345), decoded.header.max_scrollback_bytes);
|
||||
try testing.expectEqual(@as(?u64, 67), decoded.header.max_scrollback_rows);
|
||||
try testing.expect(decoded.tabstops.get(1));
|
||||
try testing.expect(decoded.tabstops.get(9));
|
||||
try testing.expectEqualStrings("file:///tmp/native", decoded.pwd);
|
||||
try testing.expectEqualStrings("native title", decoded.title);
|
||||
|
||||
// The public decoder installs the same payload into native ownership.
|
||||
var native_source: std.Io.Reader = .fixed(destination.written());
|
||||
var restored = try decode(
|
||||
&native_source,
|
||||
&source,
|
||||
testing.io,
|
||||
testing.allocator,
|
||||
);
|
||||
@@ -1675,6 +1583,31 @@ test "TERMINAL record encodes native terminal state" {
|
||||
try testing.expect(restored.screens.get(.alternate) == null);
|
||||
try testing.expectEqual(@as(?u21, 'X'), restored.previous_char);
|
||||
try testing.expectEqualDeep(terminal.cursor, restored.cursor);
|
||||
try testing.expectEqual(
|
||||
terminal.flags.shell_redraws_prompt,
|
||||
restored.flags.shell_redraws_prompt,
|
||||
);
|
||||
try testing.expectEqual(
|
||||
terminal.flags.modify_other_keys_2,
|
||||
restored.flags.modify_other_keys_2,
|
||||
);
|
||||
try testing.expectEqual(
|
||||
terminal.flags.mouse_event,
|
||||
restored.flags.mouse_event,
|
||||
);
|
||||
try testing.expectEqual(
|
||||
terminal.flags.mouse_format,
|
||||
restored.flags.mouse_format,
|
||||
);
|
||||
try testing.expectEqual(
|
||||
terminal.flags.mouse_shift_capture,
|
||||
restored.flags.mouse_shift_capture,
|
||||
);
|
||||
try testing.expectEqual(
|
||||
terminal.flags.password_input,
|
||||
restored.flags.password_input,
|
||||
);
|
||||
try testing.expectEqual(terminal.mouse_shape, restored.mouse_shape);
|
||||
try testing.expectEqualDeep(terminal.modes, restored.modes);
|
||||
try testing.expectEqualDeep(terminal.colors, restored.colors);
|
||||
try testing.expectEqual(
|
||||
|
||||
Reference in New Issue
Block a user