terminal/snapshot: test fixtures

This commit is contained in:
Mitchell Hashimoto
2026-07-30 15:14:14 -07:00
parent 92c8dfd508
commit 32f11a4663
23 changed files with 913 additions and 140 deletions

View File

@@ -17,6 +17,7 @@
//! | 8 | 2 | Version (`u16`) |
const std = @import("std");
const test_fixture = @import("fixture.zig");
const io = @import("io.zig");
/// Identifies a Ghostty terminal snapshot and rejects unrelated input before
@@ -65,15 +66,25 @@ fn computeLen() usize {
}
}
const test_golden_fixture = test_fixture.parse(
@embedFile("testdata/envelope-v1.hex"),
);
test "golden encoding" {
var buf: [encoded_len]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try encode(&writer);
try std.testing.expectEqualStrings(
"GHOSTSNP\x01\x00",
try test_fixture.expectEqual(
.bytes,
"src/terminal/snapshot/testdata/envelope-v1.hex",
"snapshot_fixture-envelope-v1.hex",
&test_golden_fixture,
writer.buffered(),
);
var reader: std.Io.Reader = .fixed(&test_golden_fixture);
try decode(&reader);
}
test "reject invalid magic and version" {
@@ -88,9 +99,8 @@ test "reject invalid magic and version" {
}
test "reject every truncation" {
const fixture = "GHOSTSNP\x01\x00";
for (0..encoded_len) |len| {
var reader: std.Io.Reader = .fixed(fixture[0..len]);
var reader: std.Io.Reader = .fixed(test_golden_fixture[0..len]);
try std.testing.expectError(error.EndOfStream, decode(&reader));
}
}

View File

@@ -0,0 +1,416 @@
//! Test-only support for reviewable snapshot golden fixtures.
//!
//! Snapshot compatibility references are stored as annotated hexadecimal text
//! under `testdata/`. Each byte is written as exactly two hexadecimal digits.
//! Whitespace is insignificant, and `#` begins a comment through the end of the
//! line. This permits field, record, and absolute-offset annotations without
//! changing the bytes embedded by a test.
//!
//! `parse` validates and decodes an `@embedFile` at comptime. Tests should
//! encode their native value, compare the generated bytes with that embedded
//! reference using `expectEqual`, and then decode the reference itself. This
//! keeps encoding regression coverage separate from backwards-decoding
//! coverage.
//!
//! `expectEqual` formats a fresh candidate on every run. Matching candidates
//! remain in the test's temporary directory. A mismatch copies the candidate
//! to the repository root, reports its path and first differing byte, and
//! fails without modifying the checked-in reference. After reviewing an
//! intentional wire change, a maintainer manually replaces the reference.
//! Fixture filenames include the snapshot version so a future format adds new
//! references instead of overwriting compatibility data for an older version.
//!
//! A typical fixed-value test looks like this:
//!
//! ```zig
//! const fixture = @import("fixture.zig");
//!
//! const reference = fixture.parse(
//! @embedFile("testdata/example-v1.hex"),
//! );
//!
//! test "example golden encoding and decoding" {
//! var encoded: [Example.len]u8 = undefined;
//! var writer: std.Io.Writer = .fixed(&encoded);
//! try Example.encode(value, &writer);
//!
//! try fixture.expectEqual(
//! .bytes,
//! "src/terminal/snapshot/testdata/example-v1.hex",
//! "snapshot_fixture-example-v1.hex",
//! &reference,
//! writer.buffered(),
//! );
//!
//! var reader: std.Io.Reader = .fixed(&reference);
//! try std.testing.expectEqualDeep(
//! value,
//! try Example.decode(&reader),
//! );
//! }
//! ```
const std = @import("std");
const envelope = @import("envelope.zig");
const record = @import("record.zig");
const log = std.log.scoped(.snapshot_fixture);
pub const Kind = enum {
bytes,
page,
snapshot,
};
/// Decode an annotated hexadecimal fixture at comptime.
///
/// Whitespace is ignored. `#` begins a comment that continues through the end
/// of the line. Every other token must be exactly two hexadecimal digits.
pub fn parse(comptime source: []const u8) [decodedLen(source)]u8 {
comptime {
// Large complete-snapshot fixtures exceed Zig's default comptime
// branch budget while scanning their comments and byte tokens.
@setEvalBranchQuota(100_000_000);
// The first pass in decodedLen gives this result its exact array type.
// Keeping it an array lets callers use the fixture in comptime slices.
var result: [decodedLen(source)]u8 = undefined;
var source_index: usize = 0;
var result_index: usize = 0;
while (source_index < source.len) {
// Comments and layout are purely for reviewers and contribute no
// bytes to the embedded reference.
skipIgnored(source, &source_index);
if (source_index == source.len) break;
// Every data token is one complete byte. Parse it directly rather
// than accepting variable-width integers or other hex syntax.
if (source_index + 1 >= source.len) {
@compileError("snapshot fixture ends with one hex digit");
}
result[result_index] =
hexNibble(source[source_index]) << 4 |
hexNibble(source[source_index + 1]);
result_index += 1;
source_index += 2;
// Requiring a separator catches accidental third digits and makes
// the accepted grammar match the formatter below.
if (source_index < source.len and
!std.ascii.isWhitespace(source[source_index]) and
source[source_index] != '#')
{
@compileError(
"snapshot fixture hex bytes must be separated by whitespace",
);
}
}
return result;
}
}
/// Compare generated bytes with a checked-in reference.
///
/// A formatted candidate is always created in a temporary directory. On a
/// mismatch it is copied into the repository root before the test fails.
pub fn expectEqual(
kind: Kind,
reference_path: []const u8,
candidate_name: []const u8,
expected: []const u8,
actual: []const u8,
) !void {
const testing = std.testing;
const alloc = testing.allocator;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
// Always materialize a candidate, even when it will match. This keeps the
// generation path exercised on every test run while avoiding repository
// artifacts for the normal successful case.
{
var candidate_file = try tmp.dir.createFile(
testing.io,
candidate_name,
.{},
);
defer candidate_file.close(testing.io);
var write_buffer: [4096]u8 = undefined;
var candidate_writer = candidate_file.writer(
testing.io,
&write_buffer,
);
try format(kind, actual, &candidate_writer.interface);
try candidate_writer.interface.flush();
}
// Matching candidates remain in the temporary directory and disappear
// with its cleanup.
if (std.mem.eql(u8, expected, actual)) return;
// The test temp directory is not necessarily below the working directory.
// Resolve both paths before copying the candidate to the stable,
// user-visible repository-root name.
const tmp_path = try tmp.dir.realPathFileAlloc(
testing.io,
candidate_name,
alloc,
);
defer alloc.free(tmp_path);
const cwd = try std.Io.Dir.cwd().realPathFileAlloc(
testing.io,
".",
alloc,
);
defer alloc.free(cwd);
const candidate_path = try std.fs.path.join(
alloc,
&.{ cwd, candidate_name },
);
defer alloc.free(candidate_path);
try std.Io.Dir.copyFileAbsolute(
tmp_path,
candidate_path,
testing.io,
.{},
);
// Report the first useful comparison point. When one byte sequence is a
// prefix of the other, this is the shared length where they diverge.
const difference = difference: {
const shared_len = @min(expected.len, actual.len);
for (
expected[0..shared_len],
actual[0..shared_len],
0..,
) |expected_byte, actual_byte, index| {
if (expected_byte != actual_byte) break :difference index;
}
break :difference shared_len;
};
log.err(
"snapshot fixture differs reference={s} candidate={s} " ++
"offset=0x{x} expected_len={} actual_len={}",
.{
reference_path,
candidate_path,
difference,
expected.len,
actual.len,
},
);
return error.TestExpectedEqual;
}
fn decodedLen(comptime source: []const u8) usize {
comptime {
// This is a complete validation pass, not just a count. Invalid syntax
// therefore fails before parse allocates and fills its result array.
@setEvalBranchQuota(100_000_000);
var index: usize = 0;
var result: usize = 0;
while (index < source.len) {
skipIgnored(source, &index);
if (index == source.len) break;
if (index + 1 >= source.len) {
@compileError("snapshot fixture ends with one hex digit");
}
_ = hexNibble(source[index]);
_ = hexNibble(source[index + 1]);
result += 1;
index += 2;
if (index < source.len and
!std.ascii.isWhitespace(source[index]) and
source[index] != '#')
{
@compileError(
"snapshot fixture hex bytes must be separated by whitespace",
);
}
}
return result;
}
}
fn skipIgnored(comptime source: []const u8, index: *usize) void {
while (index.* < source.len) {
// Layout whitespace is unrestricted so fixtures can group fields and
// keep deterministic sixteen-byte output lines.
if (std.ascii.isWhitespace(source[index.*])) {
index.* += 1;
continue;
}
// A comment consumes the rest of its line. The newline itself is
// consumed by the whitespace case on the next iteration.
if (source[index.*] != '#') return;
while (index.* < source.len and source[index.*] != '\n') {
index.* += 1;
}
}
}
fn hexNibble(comptime value: u8) u8 {
return switch (value) {
'0'...'9' => value - '0',
'a'...'f' => value - 'a' + 10,
'A'...'F' => value - 'A' + 10,
else => @compileError("snapshot fixture contains a non-hex digit"),
};
}
fn format(
kind: Kind,
bytes: []const u8,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
// Every generated candidate starts with enough maintenance context to be
// understandable when copied out of a failing test's temp directory.
try writer.print(
"# Ghostty snapshot fixture\n" ++
"# Wire version: {}\n" ++
"# Generated by its snapshot test; review before replacing.\n" ++
"# On mismatch, the candidate is copied to the repository root.\n",
.{envelope.version},
);
// Small values only need a hex dump. Larger compound formats get
// structure-aware section labels without changing their decoded bytes.
switch (kind) {
.bytes => try writeSection(
"encoded bytes",
bytes,
0,
bytes.len,
writer,
),
.page => try formatPage(bytes, writer),
.snapshot => try formatSnapshot(bytes, writer),
}
}
fn formatPage(
bytes: []const u8,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
// PAGE version 1 has a twenty-byte fixed header. The remainder is
// self-describing through the counts and dimensions in that header.
const header_end = @min(bytes.len, 20);
try writeSection("PAGE header", bytes, 0, header_end, writer);
if (header_end < bytes.len) {
try writeSection(
"style and hyperlink tables, rows, and cells",
bytes,
header_end,
bytes.len,
writer,
);
}
}
fn formatSnapshot(
bytes: []const u8,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
// A complete snapshot begins with its unframed envelope. Clamp this first
// section so a partially generated candidate can still be inspected.
const envelope_end = @min(bytes.len, envelope.encoded_len);
try writeSection("snapshot envelope", bytes, 0, envelope_end, writer);
// Walk record framing only far enough to annotate boundaries. The emitted
// hex remains authoritative; this formatter never repairs candidate data.
var offset: usize = envelope_end;
while (offset < bytes.len) {
// A partial header cannot provide a safe payload boundary, so preserve
// the remaining bytes as one explicitly incomplete section.
if (bytes.len - offset < record.Header.len) {
try writeSection(
"trailing partial record",
bytes,
offset,
bytes.len,
writer,
);
return;
}
// Tag and payload length are the first six bytes of every record
// header. CRC validation belongs to the codec test, not this formatter.
const tag_raw = std.mem.readInt(
u16,
bytes[offset..][0..2],
.little,
);
const payload_len: u32 = std.mem.readInt(
u32,
bytes[offset + 2 ..][0..4],
.little,
);
// Clamp the section to the available candidate bytes. This makes a
// truncated payload reviewable instead of indexing beyond the slice.
const record_end: usize = @min(
bytes.len,
offset + record.Header.len + @as(usize, payload_len),
);
// Known tags make the annotated diff readable. Preserve unknown tags
// numerically so formatter output is still useful for encoder bugs.
if (std.enums.fromInt(record.Tag, tag_raw)) |tag| {
try writer.print(
"\n# offset 0x{x:0>8}: {s} record, payload {} bytes\n",
.{ offset, @tagName(tag), payload_len },
);
} else {
try writer.print(
"\n# offset 0x{x:0>8}: unknown record {}, payload {} bytes\n",
.{ offset, tag_raw, payload_len },
);
}
try writeHex(bytes, offset, record_end, writer);
offset = record_end;
}
}
fn writeSection(
name: []const u8,
bytes: []const u8,
start: usize,
end: usize,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
// Sections begin on a fresh line and carry their absolute wire offset.
try writer.print(
"\n# offset 0x{x:0>8}: {s}\n",
.{ start, name },
);
try writeHex(bytes, start, end, writer);
}
fn writeHex(
bytes: []const u8,
start: usize,
end: usize,
writer: *std.Io.Writer,
) std.Io.Writer.Error!void {
const digits = "0123456789abcdef";
var offset = start;
while (offset < end) {
// Lowercase, sixteen-byte lines provide stable textual diffs. The
// trailing offset is a comment and is ignored by parse.
const line_end = @min(end, offset + 16);
for (bytes[offset..line_end], 0..) |byte, index| {
if (index != 0) try writer.writeByte(' ');
try writer.writeByte(digits[byte >> 4]);
try writer.writeByte(digits[byte & 0x0f]);
}
try writer.print(" # 0x{x:0>8}\n", .{offset});
offset = line_end;
}
}

View File

@@ -65,6 +65,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const test_fixture = @import("fixture.zig");
const io = @import("io.zig");
const page = @import("page.zig");
const record = @import("record.zig");
@@ -334,6 +335,10 @@ fn hasSemanticPrompt(terminal_page: *const TerminalPage) bool {
return false;
}
const test_header_fixture = test_fixture.parse(
@embedFile("testdata/history-header-v1.hex"),
);
test "HISTORY header golden encoding and decoding" {
const expected: Header = .{
.key = .alternate,
@@ -341,20 +346,24 @@ test "HISTORY header golden encoding and decoding" {
.total_rows = 0x0102030405060708,
.screen_overlap_rows = 0x090a,
};
const fixture =
"\x01\x00\x04\x03\x02\x01" ++
"\x08\x07\x06\x05\x04\x03\x02\x01\x0a\x09";
try std.testing.expectEqual(Header.len, fixture.len);
var encoded: [Header.len]u8 = undefined;
var writer: std.Io.Writer = .fixed(&encoded);
try expected.encode(&writer);
try std.testing.expectEqualStrings(fixture, writer.buffered());
try test_fixture.expectEqual(
.bytes,
"src/terminal/snapshot/testdata/history-header-v1.hex",
"snapshot_fixture-history-header-v1.hex",
&test_header_fixture,
writer.buffered(),
);
try std.testing.expectEqual(Header.len, test_header_fixture.len);
var reader: std.Io.Reader = .fixed(fixture);
var reader: std.Io.Reader = .fixed(&test_header_fixture);
try std.testing.expectEqualDeep(expected, try Header.decode(&reader));
for (0..Header.len) |fixture_len| {
var truncated: std.Io.Reader = .fixed(fixture[0..fixture_len]);
var truncated: std.Io.Reader = .fixed(
test_header_fixture[0..fixture_len],
);
try std.testing.expectError(
error.EndOfStream,
Header.decode(&truncated),

View File

@@ -42,6 +42,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const test_fixture = @import("fixture.zig");
const io = @import("io.zig");
const terminal_hyperlink = @import("../hyperlink.zig");
const terminal_page = @import("../page.zig");
@@ -270,6 +271,14 @@ fn decodePageString(
};
}
const test_implicit_fixture = test_fixture.parse(
@embedFile("testdata/hyperlink-implicit-v1.hex"),
);
const test_explicit_fixture = test_fixture.parse(
@embedFile("testdata/hyperlink-explicit-v1.hex"),
);
test "golden implicit encoding" {
const value: terminal_hyperlink.Hyperlink = .{
.id = .{ .implicit = 0x01020304 },
@@ -280,10 +289,18 @@ test "golden implicit encoding" {
var writer: std.Io.Writer = .fixed(&buf);
try encode(value, &writer);
try std.testing.expectEqualStrings(
"\x01\x04\x03\x02\x01\x03\x00\x00\x00uri",
try test_fixture.expectEqual(
.bytes,
"src/terminal/snapshot/testdata/hyperlink-implicit-v1.hex",
"snapshot_fixture-hyperlink-implicit-v1.hex",
&test_implicit_fixture,
writer.buffered(),
);
var reader: std.Io.Reader = .fixed(&test_implicit_fixture);
var decoded = try decode(&reader, std.testing.allocator);
defer decoded.deinit(std.testing.allocator);
try std.testing.expectEqualDeep(value, decoded);
}
test "golden explicit encoding" {
@@ -296,10 +313,18 @@ test "golden explicit encoding" {
var writer: std.Io.Writer = .fixed(&buf);
try encode(value, &writer);
try std.testing.expectEqualStrings(
"\x02\x02\x00\x00\x00id\x03\x00\x00\x00uri",
try test_fixture.expectEqual(
.bytes,
"src/terminal/snapshot/testdata/hyperlink-explicit-v1.hex",
"snapshot_fixture-hyperlink-explicit-v1.hex",
&test_explicit_fixture,
writer.buffered(),
);
var reader: std.Io.Reader = .fixed(&test_explicit_fixture);
var decoded = try decode(&reader, std.testing.allocator);
defer decoded.deinit(std.testing.allocator);
try std.testing.expectEqualDeep(value, decoded);
}
test "decode rejects empty strings" {
@@ -369,9 +394,7 @@ test "reject invalid kinds" {
test "decode allocation failure" {
{
var reader: std.Io.Reader = .fixed(
"\x01\x04\x03\x02\x01\x03\x00\x00\x00uri",
);
var reader: std.Io.Reader = .fixed(&test_implicit_fixture);
var failing = std.testing.FailingAllocator.init(
std.testing.allocator,
.{ .fail_index = 0 },
@@ -387,9 +410,7 @@ test "decode allocation failure" {
std.testing.allocator,
.{ .fail_index = fail_index },
);
var reader: std.Io.Reader = .fixed(
"\x02\x02\x00\x00\x00id\x03\x00\x00\x00uri",
);
var reader: std.Io.Reader = .fixed(&test_explicit_fixture);
try std.testing.expectError(
error.OutOfMemory,
decode(&reader, failing.allocator()),
@@ -399,8 +420,8 @@ test "decode allocation failure" {
test "reject every truncation" {
const fixtures: [2][]const u8 = .{
"\x01\x04\x03\x02\x01\x03\x00\x00\x00uri",
"\x02\x02\x00\x00\x00id\x03\x00\x00\x00uri",
&test_implicit_fixture,
&test_explicit_fixture,
};
for (fixtures) |fixture| {

View File

@@ -94,6 +94,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const test_fixture = @import("fixture.zig");
const grid = @import("grid.zig");
const hyperlink = @import("hyperlink.zig");
const io = @import("io.zig");
@@ -546,35 +547,17 @@ fn pageHyperlink(
};
}
// Regenerate these after a wire-format change from `writer.buffered()` in the
// sparse-page test and `destination.written()` in the empty-record test below.
// Format those byte slices as Zig-escaped strings before replacing the literals.
const test_page_fixture =
"\x03\x00\x02\x00\x02\x00\x02\x00\x08\x00\x00\x02\x80\x00\x00\x00" ++
"\x00\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" ++
"\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x00\x00\x01\x2a\x00\x00" ++
"\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x01\x00\x00\x00\x61" ++
"\x05\x00\x00\x00\x61\x6c\x70\x68\x61\x03\x00\x01\x04\x03\x02\x01" ++
"\x04\x00\x00\x00\x62\x65\x74\x61\x04\x00\x01\x05\x00\x01\x00\x01" ++
"\x00\x41\x00\x00\x00\x00\x00\x00\x00\x00\x02\x02\x00\x03\x00\x03" ++
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x01" ++
"\x00\x07\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00" ++
"\x00\x00\x78\x00\x00\x00\x02\x00\x00\x00\x01\x03\x00\x00\x02\x03" ++
"\x00\x00\x02\x00\x01\x00\x00\x00\x00\x00\xaa\xbb\xcc\x00\x00\x00" ++
"\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" ++
"\x00\x00";
const test_page_fixture = test_fixture.parse(
@embedFile("testdata/page-v1.hex"),
);
const test_empty_framed_page_fixture =
// Record header: PAGE, 37-byte payload, CRC32C.
"\x03\x00\x25\x00\x00\x00\x8c\x05\xd6\xd3" ++
// PAGE header: one column, one row, and zero counts/capacities.
"\x01\x00\x01\x00\x00\x00\x00\x00" ++
"\x00\x00\x00\x00\x00\x00\x00\x00" ++
"\x00\x00\x00\x00" ++
// One default row and one empty cell.
"\x00\x00\x00\x00\x00\x00\x00\x00" ++
"\x00\x00\x00\x00\x00\x00\x00\x00" ++
"\x00";
const test_header_fixture = test_fixture.parse(
@embedFile("testdata/page-header-v1.hex"),
);
const test_empty_framed_page_fixture = test_fixture.parse(
@embedFile("testdata/page-empty-record-v1.hex"),
);
test "PAGE header golden encoding and decoding" {
const header: Header = .{
@@ -587,29 +570,26 @@ test "PAGE header golden encoding and decoding" {
.grapheme_capacity_bytes = 0x0d0e0f10,
.string_capacity_bytes = 0x11121314,
};
const fixture =
"\x02\x01\x04\x03\x06\x05\x08\x07" ++
"\x0a\x09\x0c\x0b\x10\x0f\x0e\x0d" ++
"\x14\x13\x12\x11";
var buf: [Header.len]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try header.encode(&writer);
try std.testing.expectEqualStrings(fixture, writer.buffered());
try test_fixture.expectEqual(
.bytes,
"src/terminal/snapshot/testdata/page-header-v1.hex",
"snapshot_fixture-page-header-v1.hex",
&test_header_fixture,
writer.buffered(),
);
var source: std.Io.Reader = .fixed(fixture);
var source: std.Io.Reader = .fixed(&test_header_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" {
const fixture =
"\x02\x01\x04\x03\x06\x05\x08\x07" ++
"\x0a\x09\x0c\x0b\x10\x0f\x0e\x0d" ++
"\x14\x13\x12\x11";
for (0..Header.len) |len| {
var reader: std.Io.Reader = .fixed(fixture[0..len]);
var reader: std.Io.Reader = .fixed(test_header_fixture[0..len]);
try std.testing.expectError(error.EndOfStream, Header.decode(&reader));
}
}
@@ -729,12 +709,21 @@ test "framed PAGE encode and decode a sparse native page" {
var writer: std.Io.Writer = .fixed(&encoded);
try encodePayload(&page, &writer);
const fixture = test_page_fixture;
try std.testing.expectEqualStrings(fixture, writer.buffered());
try std.testing.expectEqual(@as(u64, fixture.len), counter.count);
try test_fixture.expectEqual(
.page,
"src/terminal/snapshot/testdata/page-v1.hex",
"snapshot_fixture-page-v1.hex",
&test_page_fixture,
writer.buffered(),
);
try std.testing.expectEqual(
@as(u64, test_page_fixture.len),
counter.count,
);
// Decode the checked-in reference rather than the just-generated bytes.
// A one-byte backing buffer exercises streaming reads across every field.
var source: std.Io.Reader = .fixed(writer.buffered());
var source: std.Io.Reader = .fixed(&test_page_fixture);
var read_buf: [1]u8 = undefined;
var limited = source.limited(.unlimited, &read_buf);
var decoded = try decodePayload(
@@ -865,12 +854,15 @@ test "framed PAGE golden empty record" {
var destination: std.Io.Writer.Allocating = .init(std.testing.allocator);
defer destination.deinit();
try encode(&page, &destination);
try std.testing.expectEqualStrings(
test_empty_framed_page_fixture,
try test_fixture.expectEqual(
.bytes,
"src/terminal/snapshot/testdata/page-empty-record-v1.hex",
"snapshot_fixture-page-empty-record-v1.hex",
&test_empty_framed_page_fixture,
destination.written(),
);
var source: std.Io.Reader = .fixed(destination.written());
var source: std.Io.Reader = .fixed(&test_empty_framed_page_fixture);
var decoded = try decode(&source, std.testing.allocator);
defer decoded.deinit();
try std.testing.expectEqual(
@@ -880,7 +872,7 @@ test "framed PAGE golden empty record" {
}
test "framed PAGE rejects a different record tag" {
var wrong_tag = test_empty_framed_page_fixture.*;
var wrong_tag = test_empty_framed_page_fixture;
std.mem.writeInt(
u16,
wrong_tag[0..2],

View File

@@ -19,6 +19,7 @@
//! Supported tags are in `Tag`.
const std = @import("std");
const test_fixture = @import("fixture.zig");
const io = @import("io.zig");
/// CRC32C as specified by the snapshot format. Zig names this standard
@@ -295,6 +296,10 @@ fn encodeChecksumPrefix(
try io.writeInt(writer, u32, payload_len);
}
const test_page_header_fixture = test_fixture.parse(
@embedFile("testdata/record-page-header-v1.hex"),
);
test "golden PAGE record header and checksum" {
const page_header =
"\x50\x00\x18\x00\x00\x00\x00\x00" ++
@@ -314,10 +319,16 @@ test "golden PAGE record header and checksum" {
var writer: std.Io.Writer = .fixed(&buf);
try header.encode(&writer);
try std.testing.expectEqualStrings(
"\x03\x00\x18\x00\x00\x00\x1b\x44\x78\x71",
try test_fixture.expectEqual(
.bytes,
"src/terminal/snapshot/testdata/record-page-header-v1.hex",
"snapshot_fixture-record-page-header-v1.hex",
&test_page_header_fixture,
writer.buffered(),
);
var reader: std.Io.Reader = .fixed(&test_page_header_fixture);
try std.testing.expectEqual(header, try Header.decode(&reader));
}
test "reject invalid tags" {
@@ -330,9 +341,10 @@ test "reject invalid tags" {
}
test "reject every header truncation" {
const fixture = "\x03\x00\x18\x00\x00\x00\x1b\x44\x78\x71";
for (0..Header.len) |len| {
var reader: std.Io.Reader = .fixed(fixture[0..len]);
var reader: std.Io.Reader = .fixed(
test_page_header_fixture[0..len],
);
try std.testing.expectError(error.EndOfStream, Header.decode(&reader));
}
}

View File

@@ -201,6 +201,7 @@ const std = @import("std");
const build_options = @import("terminal_options");
const Allocator = std.mem.Allocator;
const hyperlink = @import("hyperlink.zig");
const test_fixture = @import("fixture.zig");
const io = @import("io.zig");
const page = @import("page.zig");
const record = @import("record.zig");
@@ -1116,19 +1117,13 @@ pub fn decodeCursorHyperlink(
return try hyperlink.decode(reader, alloc);
}
const test_header_fixture =
"\x01\x00\x03\x02\x05\x04\x07\x06\x03\x19" ++
"\x00\x00\x00\x00\x01\x7f\x00\x00" ++
"\x02\x12\x34\x56\xff\x03\x00\x00" ++
"\x0d\x0c\x0b\x0a\xe4\x3d\x02\x07" ++
"\x01\x02\x04\x08\x10\x1f\x00\x11" ++
"\x02\x03\x01";
const test_header_fixture = test_fixture.parse(
@embedFile("testdata/screen-header-v1.hex"),
);
const test_saved_cursor_fixture =
"\x02\x01\x04\x03" ++
"\x00\x00\x00\x00\x00\x00\x00\x00" ++
"\x00\x00\x00\x00\x00\x00\x00\x00" ++
"\x07\xe4\x3d";
const test_saved_cursor_fixture = test_fixture.parse(
@embedFile("testdata/screen-saved-cursor-v1.hex"),
);
fn testCharsetState() TerminalScreen.CharsetState {
var result: TerminalScreen.CharsetState = .{
@@ -1220,18 +1215,20 @@ fn testSavedCursor() SavedCursor {
}
test "SCREEN header golden encoding and decoding" {
try std.testing.expectEqual(Header.len, test_header_fixture.len);
var encoded: [Header.len]u8 = undefined;
var writer: std.Io.Writer = .fixed(&encoded);
try testHeader().encode(&writer);
try std.testing.expectEqualStrings(
test_header_fixture,
try test_fixture.expectEqual(
.bytes,
"src/terminal/snapshot/testdata/screen-header-v1.hex",
"snapshot_fixture-screen-header-v1.hex",
&test_header_fixture,
writer.buffered(),
);
try std.testing.expectEqual(Header.len, test_header_fixture.len);
var source: std.Io.Reader = .fixed(test_header_fixture);
var source: std.Io.Reader = .fixed(&test_header_fixture);
var buffer: [1]u8 = undefined;
var limited = source.limited(.unlimited, &buffer);
@@ -1601,20 +1598,22 @@ test "native SCREEN payload omits absent optional state" {
}
test "saved cursor golden encoding and decoding" {
var encoded: [SavedCursor.len]u8 = undefined;
var writer: std.Io.Writer = .fixed(&encoded);
try testSavedCursor().encode(&writer);
try test_fixture.expectEqual(
.bytes,
"src/terminal/snapshot/testdata/screen-saved-cursor-v1.hex",
"snapshot_fixture-screen-saved-cursor-v1.hex",
&test_saved_cursor_fixture,
writer.buffered(),
);
try std.testing.expectEqual(
SavedCursor.len,
test_saved_cursor_fixture.len,
);
var encoded: [SavedCursor.len]u8 = undefined;
var writer: std.Io.Writer = .fixed(&encoded);
try testSavedCursor().encode(&writer);
try std.testing.expectEqualStrings(
test_saved_cursor_fixture,
writer.buffered(),
);
var source: std.Io.Reader = .fixed(test_saved_cursor_fixture);
var source: std.Io.Reader = .fixed(&test_saved_cursor_fixture);
var buffer: [1]u8 = undefined;
var limited = source.limited(.unlimited, &buffer);
try std.testing.expectEqualDeep(

View File

@@ -5,14 +5,20 @@ const build_options = @import("terminal_options");
const Allocator = std.mem.Allocator;
const checkpoint = @import("checkpoint.zig");
const envelope = @import("envelope.zig");
const test_fixture = @import("fixture.zig");
const history = @import("history.zig");
const record = @import("record.zig");
const screen = @import("screen.zig");
const terminal = @import("terminal.zig");
const Terminal = @import("../Terminal.zig");
const TerminalPageList = @import("../PageList.zig");
const TerminalScreen = @import("../Screen.zig");
const TerminalScreenKey = @import("../ScreenSet.zig").Key;
const test_complete_fixture = test_fixture.parse(
@embedFile("testdata/complete-v1.hex"),
);
/// Errors possible while encoding one complete terminal snapshot.
pub const EncodeError = terminal.EncodeError ||
screen.EncodeError ||
@@ -196,15 +202,14 @@ test "complete snapshot round trip with history and alternate screen" {
const testing = std.testing;
var t = try Terminal.init(testing.io, testing.allocator, .{
.cols = 80,
.cols = 2,
.rows = 3,
.max_scrollback_bytes = null,
.max_scrollback_lines = null,
});
defer t.deinit(testing.allocator);
// Exercise terminal-wide state and grow the primary screen until its
// active area is preceded by multiple complete history pages.
// Exercise terminal-wide state.
t.width_px = 800;
t.height_px = 600;
t.colors.palette.set(7, .{ .r = 1, .g = 2, .b = 3 });
@@ -213,9 +218,58 @@ test "complete snapshot round trip with history and alternate screen" {
try t.setTitle("complete snapshot");
const primary = t.screens.get(.primary).?;
while (primary.pages.totalPages() < 4) {
try t.printString("primary history\n");
}
// Use small exact capacities so this compound golden remains practical to
// review while still containing two complete history pages and one active
// page. Replacing the Screen in place preserves ScreenSet routing.
var replacement: TerminalScreen = replacement: {
var builder = try TerminalPageList.Builder.init(
testing.allocator,
.{
.cols = t.cols,
.rows = t.rows,
.max_size = null,
.max_lines = null,
},
);
defer builder.deinit();
const oldest = try builder.allocatePage(.{ .cols = 2, .rows = 2 });
oldest.size.rows = 2;
oldest.getRowAndCell(0, 0).cell.* = .init('A');
const recent = try builder.allocatePage(.{ .cols = 2, .rows = 2 });
recent.size.rows = 2;
recent.getRowAndCell(0, 0).cell.* = .init('B');
const active = try builder.allocatePage(.{ .cols = 2, .rows = 3 });
active.size.rows = 3;
active.getRowAndCell(0, 0).cell.* = .init('C');
active.getRowAndCell(0, 1).cell.* = .init('D');
active.getRowAndCell(0, 2).cell.* = .init('E');
var pages = try builder.finish();
errdefer pages.deinit();
const cursor_pin = try pages.trackPin(
pages.pin(.{ .active = .{} }).?,
);
const cursor_rac = cursor_pin.rowAndCell();
break :replacement .{
.io = testing.io,
.alloc = testing.allocator,
.pages = pages,
.cursor = .{
.page_pin = cursor_pin,
.page_row = cursor_rac.row,
.page_cell = cursor_rac.cell,
},
};
};
primary.deinit();
primary.* = replacement;
replacement = undefined;
try testing.expect(primary.pages.scrollbar().total > t.rows);
// Compression is an internal source representation and must remain
@@ -232,8 +286,16 @@ test "complete snapshot round trip with history and alternate screen" {
defer encoded.deinit();
try encode(&t, &encoded);
try testing.expectEqualDeep(source_memory, primary.pages.memoryStats());
try test_fixture.expectEqual(
.snapshot,
"src/terminal/snapshot/testdata/complete-v1.hex",
"snapshot_fixture-complete-v1.hex",
&test_complete_fixture,
encoded.written(),
);
var encoded_source: std.Io.Reader = .fixed(encoded.written());
// Restore the checked-in reference rather than the just-generated bytes.
var encoded_source: std.Io.Reader = .fixed(&test_complete_fixture);
var source_buffer: [1]u8 = undefined;
var limited = encoded_source.limited(.unlimited, &source_buffer);
var restored = try decode(
@@ -266,7 +328,10 @@ test "complete snapshot round trip with history and alternate screen" {
var reencoded: std.Io.Writer.Allocating = .init(testing.allocator);
defer reencoded.deinit();
try encode(&restored, &reencoded);
try testing.expectEqualStrings(encoded.written(), reencoded.written());
try testing.expectEqualStrings(
&test_complete_fixture,
reencoded.written(),
);
// SCREEN keys make their order independent. Keep HISTORY canonical here;
// only the active-state screen sequences are intentionally reversed.

View File

@@ -46,6 +46,7 @@
//! Underline values 6 and 7 are invalid in snapshot version 1.
const std = @import("std");
const test_fixture = @import("fixture.zig");
const io = @import("io.zig");
const sgr = @import("../sgr.zig");
const terminal_style = @import("../style.zig");
@@ -215,6 +216,10 @@ fn computeLen() usize {
}
}
const test_golden_fixture = test_fixture.parse(
@embedFile("testdata/style-v1.hex"),
);
test "golden encoding and decoding" {
const value: terminal_style.Style = .{
.fg_color = .none,
@@ -236,18 +241,18 @@ test "golden encoding and decoding" {
.underline = .curly,
},
};
const fixture =
"\x00\x00\x00\x00" ++
"\x01\x7f\x00\x00" ++
"\x02\x12\x34\x56" ++
"\xff\x03\x00\x00";
var buf: [len]u8 = undefined;
var writer: std.Io.Writer = .fixed(&buf);
try encode(value, &writer);
try std.testing.expectEqualStrings(fixture, writer.buffered());
try test_fixture.expectEqual(
.bytes,
"src/terminal/snapshot/testdata/style-v1.hex",
"snapshot_fixture-style-v1.hex",
&test_golden_fixture,
writer.buffered(),
);
var source: std.Io.Reader = .fixed(fixture);
var source: std.Io.Reader = .fixed(&test_golden_fixture);
var read_buf: [1]u8 = undefined;
var limited = source.limited(.unlimited, &read_buf);
try std.testing.expect(value.eql(try decode(&limited.interface)));

View File

@@ -239,6 +239,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const test_fixture = @import("fixture.zig");
const io = @import("io.zig");
const record = @import("record.zig");
const terminal_ansi = @import("../ansi.zig");
@@ -1180,20 +1181,9 @@ const test_header: Header = header: {
};
};
const test_header_fixture =
"\x02\x01\x04\x03\x08\x07\x06\x05\x0c\x0b\x0a\x09" ++
"\x01\x00\x02\x00\x03\x00\x04\x00" ++
"\x01\x01\x00\x02\x00\x41\x00\x00\x00" ++
"\x01\x03\x02" ++
"\x02\x01\x04\x04\x01\x21\x01" ++
"\x01\x00\x00\x00\x00\x00\x00\x00" ++
"\x00\x00\x00\x00\x00\x02\x00\x00" ++
"\x01\x00\x00\x00\x00\x02\x00\x00" ++
"\x01\x01\x02\x03\x00\x00\x00\x00" ++
"\x00\x00\x00\x00\x01\x04\x05\x06" ++
"\x01\x07\x08\x09\x01\x0a\x0b\x0c" ++
"\xff\xff\xff\xff\xff\xff\xff\xff" ++
"\x08\x07\x06\x05\x04\x03\x02\x01";
const test_header_fixture = test_fixture.parse(
@embedFile("testdata/terminal-header-v1.hex"),
);
test "TERMINAL mode bit layout" {
try std.testing.expectEqual(
@@ -1231,15 +1221,21 @@ test "TERMINAL mode bit layout" {
test "TERMINAL header golden encoding and decoding" {
const testing = std.testing;
try testing.expectEqual(Header.len, test_header_fixture.len);
var encoded: [Header.len]u8 = undefined;
var writer: std.Io.Writer = .fixed(&encoded);
try test_header.encode(&writer);
try testing.expectEqualStrings(test_header_fixture, writer.buffered());
try test_fixture.expectEqual(
.bytes,
"src/terminal/snapshot/testdata/terminal-header-v1.hex",
"snapshot_fixture-terminal-header-v1.hex",
&test_header_fixture,
writer.buffered(),
);
try testing.expectEqual(Header.len, test_header_fixture.len);
// Exercise the streaming path with less buffered data than every integer.
var source: std.Io.Reader = .fixed(test_header_fixture);
var source: std.Io.Reader = .fixed(&test_header_fixture);
var buffer: [1]u8 = undefined;
var limited = source.limited(.unlimited, &buffer);
try testing.expectEqualDeep(
@@ -1306,14 +1302,14 @@ test "TERMINAL header rejects invalid values" {
.{ .offset = 68, .value = 1, .expected = error.InvalidDynamicRGB },
};
for (byte_cases) |case| {
var fixture = test_header_fixture.*;
var fixture = test_header_fixture;
fixture[case.offset] = case.value;
var reader: std.Io.Reader = .fixed(&fixture);
try testing.expectError(case.expected, Header.decode(&reader));
}
// Cross-field and multi-byte invariants are validated after decoding.
var invalid_screen_count = test_header_fixture.*;
var invalid_screen_count = test_header_fixture;
invalid_screen_count[23] = 3;
var screen_count_reader: std.Io.Reader = .fixed(&invalid_screen_count);
try testing.expectError(
@@ -1321,7 +1317,7 @@ test "TERMINAL header rejects invalid values" {
Header.decode(&screen_count_reader),
);
var missing_active_screen = test_header_fixture.*;
var missing_active_screen = test_header_fixture;
missing_active_screen[23] = 1;
var active_screen_reader: std.Io.Reader = .fixed(&missing_active_screen);
try testing.expectError(
@@ -1329,7 +1325,7 @@ test "TERMINAL header rejects invalid values" {
Header.decode(&active_screen_reader),
);
var invalid_scrolling_region = test_header_fixture.*;
var invalid_scrolling_region = test_header_fixture;
invalid_scrolling_region[14] = 0x04;
invalid_scrolling_region[15] = 0x03;
var scrolling_region_reader: std.Io.Reader = .fixed(
@@ -1340,7 +1336,7 @@ test "TERMINAL header rejects invalid values" {
Header.decode(&scrolling_region_reader),
);
var invalid_codepoint = test_header_fixture.*;
var invalid_codepoint = test_header_fixture;
invalid_codepoint[25] = 0x00;
invalid_codepoint[26] = 0xd8;
invalid_codepoint[27] = 0x00;

View File

@@ -0,0 +1,138 @@
# Ghostty snapshot fixture
# Wire version: 1
# Generated by its snapshot test; review before replacing.
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: snapshot envelope
47 48 4f 53 54 53 4e 50 01 00 # 0x00000000
# offset 0x0000000a: terminal record, payload 952 bytes
01 00 b8 03 00 00 e3 5f 8b cc 02 00 03 00 20 03 # 0x0000000a
00 00 58 02 00 00 00 00 02 00 00 00 01 00 00 01 # 0x0000001a
00 02 00 65 00 00 00 01 01 01 00 00 00 00 00 08 # 0x0000002a
00 04 22 00 64 10 00 00 00 04 22 00 64 00 00 00 # 0x0000003a
00 04 22 00 64 00 00 00 00 00 00 00 00 00 00 00 # 0x0000004a
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000005a
00 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff # 0x0000006a
ff 00 1d 1f 21 cc 66 66 b5 bd 68 f0 c6 74 81 a2 # 0x0000007a
be b2 94 bb 8a be b7 c5 c8 c6 66 66 66 d5 4e 53 # 0x0000008a
b9 ca 4a e7 c5 47 7a a6 da c3 97 d8 70 c0 b1 ea # 0x0000009a
ea ea 00 00 00 00 00 5f 00 00 87 00 00 af 00 00 # 0x000000aa
d7 00 00 ff 00 5f 00 00 5f 5f 00 5f 87 00 5f af # 0x000000ba
00 5f d7 00 5f ff 00 87 00 00 87 5f 00 87 87 00 # 0x000000ca
87 af 00 87 d7 00 87 ff 00 af 00 00 af 5f 00 af # 0x000000da
87 00 af af 00 af d7 00 af ff 00 d7 00 00 d7 5f # 0x000000ea
00 d7 87 00 d7 af 00 d7 d7 00 d7 ff 00 ff 00 00 # 0x000000fa
ff 5f 00 ff 87 00 ff af 00 ff d7 00 ff ff 5f 00 # 0x0000010a
00 5f 00 5f 5f 00 87 5f 00 af 5f 00 d7 5f 00 ff # 0x0000011a
5f 5f 00 5f 5f 5f 5f 5f 87 5f 5f af 5f 5f d7 5f # 0x0000012a
5f ff 5f 87 00 5f 87 5f 5f 87 87 5f 87 af 5f 87 # 0x0000013a
d7 5f 87 ff 5f af 00 5f af 5f 5f af 87 5f af af # 0x0000014a
5f af d7 5f af ff 5f d7 00 5f d7 5f 5f d7 87 5f # 0x0000015a
d7 af 5f d7 d7 5f d7 ff 5f ff 00 5f ff 5f 5f ff # 0x0000016a
87 5f ff af 5f ff d7 5f ff ff 87 00 00 87 00 5f # 0x0000017a
87 00 87 87 00 af 87 00 d7 87 00 ff 87 5f 00 87 # 0x0000018a
5f 5f 87 5f 87 87 5f af 87 5f d7 87 5f ff 87 87 # 0x0000019a
00 87 87 5f 87 87 87 87 87 af 87 87 d7 87 87 ff # 0x000001aa
87 af 00 87 af 5f 87 af 87 87 af af 87 af d7 87 # 0x000001ba
af ff 87 d7 00 87 d7 5f 87 d7 87 87 d7 af 87 d7 # 0x000001ca
d7 87 d7 ff 87 ff 00 87 ff 5f 87 ff 87 87 ff af # 0x000001da
87 ff d7 87 ff ff af 00 00 af 00 5f af 00 87 af # 0x000001ea
00 af af 00 d7 af 00 ff af 5f 00 af 5f 5f af 5f # 0x000001fa
87 af 5f af af 5f d7 af 5f ff af 87 00 af 87 5f # 0x0000020a
af 87 87 af 87 af af 87 d7 af 87 ff af af 00 af # 0x0000021a
af 5f af af 87 af af af af af d7 af af ff af d7 # 0x0000022a
00 af d7 5f af d7 87 af d7 af af d7 d7 af d7 ff # 0x0000023a
af ff 00 af ff 5f af ff 87 af ff af af ff d7 af # 0x0000024a
ff ff d7 00 00 d7 00 5f d7 00 87 d7 00 af d7 00 # 0x0000025a
d7 d7 00 ff d7 5f 00 d7 5f 5f d7 5f 87 d7 5f af # 0x0000026a
d7 5f d7 d7 5f ff d7 87 00 d7 87 5f d7 87 87 d7 # 0x0000027a
87 af d7 87 d7 d7 87 ff d7 af 00 d7 af 5f d7 af # 0x0000028a
87 d7 af af d7 af d7 d7 af ff d7 d7 00 d7 d7 5f # 0x0000029a
d7 d7 87 d7 d7 af d7 d7 d7 d7 d7 ff d7 ff 00 d7 # 0x000002aa
ff 5f d7 ff 87 d7 ff af d7 ff d7 d7 ff ff ff 00 # 0x000002ba
00 ff 00 5f ff 00 87 ff 00 af ff 00 d7 ff 00 ff # 0x000002ca
ff 5f 00 ff 5f 5f ff 5f 87 ff 5f af ff 5f d7 ff # 0x000002da
5f ff ff 87 00 ff 87 5f ff 87 87 ff 87 af ff 87 # 0x000002ea
d7 ff 87 ff ff af 00 ff af 5f ff af 87 ff af af # 0x000002fa
ff af d7 ff af ff ff d7 00 ff d7 5f ff d7 87 ff # 0x0000030a
d7 af ff d7 d7 ff d7 ff ff ff 00 ff ff 5f ff ff # 0x0000031a
87 ff ff af ff ff d7 ff ff ff 08 08 08 12 12 12 # 0x0000032a
1c 1c 1c 26 26 26 30 30 30 3a 3a 3a 44 44 44 4e # 0x0000033a
4e 4e 58 58 58 62 62 62 6c 6c 6c 76 76 76 80 80 # 0x0000034a
80 8a 8a 8a 94 94 94 9e 9e 9e a8 a8 a8 b2 b2 b2 # 0x0000035a
bc bc bc c6 c6 c6 d0 d0 d0 da da da e4 e4 e4 ee # 0x0000036a
ee ee 80 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000037a
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000038a
00 00 01 02 03 14 00 00 00 66 69 6c 65 3a 2f 2f # 0x0000039a
2f 74 6d 70 2f 73 6e 61 70 73 68 6f 74 11 00 00 # 0x000003aa
00 63 6f 6d 70 6c 65 74 65 20 73 6e 61 70 73 68 # 0x000003ba
6f 74 # 0x000003ca
# offset 0x000003cc: screen record, payload 46 bytes
02 00 2e 00 00 00 dc 02 f1 37 00 00 01 00 00 00 # 0x000003cc
00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000003dc
00 00 00 00 00 00 00 00 00 08 00 00 00 00 00 00 # 0x000003ec
00 00 00 00 00 00 00 00 # 0x000003fc
# offset 0x00000404: page record, payload 119 bytes
03 00 77 00 00 00 48 b7 91 cb 02 00 03 00 00 00 # 0x00000404
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x00000414
00 00 00 00 00 00 00 43 00 00 00 00 00 00 00 00 # 0x00000424
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000434
00 00 00 00 00 00 00 00 44 00 00 00 00 00 00 00 # 0x00000444
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000454
00 00 00 00 00 00 00 00 00 45 00 00 00 00 00 00 # 0x00000464
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000474
00 # 0x00000484
# offset 0x00000485: screen record, payload 46 bytes
02 00 2e 00 00 00 05 b9 5a 3a 01 00 01 00 01 00 # 0x00000485
02 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000495
00 00 00 00 00 00 00 00 00 08 00 00 00 00 00 00 # 0x000004a5
00 00 00 00 00 00 00 00 # 0x000004b5
# offset 0x000004bd: page record, payload 119 bytes
03 00 77 00 00 00 84 a6 e9 08 02 00 03 00 00 00 # 0x000004bd
00 00 80 00 c0 00 00 02 00 00 00 08 00 00 03 00 # 0x000004cd
00 00 00 00 00 00 00 72 00 00 00 00 00 00 00 00 # 0x000004dd
00 00 00 00 00 00 00 6e 00 00 00 00 00 00 00 03 # 0x000004ed
00 00 00 00 00 00 00 00 61 00 00 00 00 00 00 00 # 0x000004fd
00 00 00 00 00 00 00 00 74 00 00 00 00 00 00 00 # 0x0000050d
03 00 00 00 00 00 00 00 00 65 00 00 00 00 00 00 # 0x0000051d
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x0000052d
00 # 0x0000053d
# offset 0x0000053e: ready record, payload 32 bytes
05 00 20 00 00 00 29 ce 51 e1 28 ab 80 18 19 00 # 0x0000053e
ef b3 15 fa 9d 03 c3 42 7f 77 2d f3 0c 00 87 cc # 0x0000054e
a1 dc ac ce 6c fc fb 27 66 e1 # 0x0000055e
# offset 0x00000568: history record, payload 16 bytes
04 00 10 00 00 00 cc 85 a3 b6 00 00 02 00 00 00 # 0x00000568
04 00 00 00 00 00 00 00 00 00 # 0x00000578
# offset 0x00000582: page record, payload 86 bytes
03 00 56 00 00 00 52 3b 6e 8d 02 00 02 00 00 00 # 0x00000582
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x00000592
00 00 00 00 00 00 00 42 00 00 00 00 00 00 00 00 # 0x000005a2
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005b2
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005c2
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000005d2
# offset 0x000005e2: page record, payload 86 bytes
03 00 56 00 00 00 fb bc 15 06 02 00 02 00 00 00 # 0x000005e2
00 00 10 00 c0 00 00 04 00 00 00 08 00 00 00 00 # 0x000005f2
00 00 00 00 00 00 00 41 00 00 00 00 00 00 00 00 # 0x00000602
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000612
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000622
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000632
# offset 0x00000642: history record, payload 16 bytes
04 00 10 00 00 00 39 57 cc cf 01 00 00 00 00 00 # 0x00000642
00 00 00 00 00 00 00 00 00 00 # 0x00000652
# offset 0x0000065c: finish record, payload 32 bytes
06 00 20 00 00 00 05 71 15 22 3e 4d e9 7a c1 02 # 0x0000065c
0b a3 6a 10 a0 88 1c 07 03 2c 68 52 15 be 76 d0 # 0x0000066c
4c a9 a6 2b fd f4 ac 49 62 2b # 0x0000067c

View File

@@ -0,0 +1,7 @@
# Ghostty snapshot fixture
# Wire version: 1
# Generated by its snapshot test; review before replacing.
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: encoded bytes
47 48 4f 53 54 53 4e 50 01 00 # 0x00000000

View File

@@ -0,0 +1,7 @@
# Ghostty snapshot fixture
# Wire version: 1
# Generated by its snapshot test; review before replacing.
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: encoded bytes
01 00 04 03 02 01 08 07 06 05 04 03 02 01 0a 09 # 0x00000000

View File

@@ -0,0 +1,7 @@
# Ghostty snapshot fixture
# Wire version: 1
# Generated by its snapshot test; review before replacing.
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: encoded bytes
02 02 00 00 00 69 64 03 00 00 00 75 72 69 # 0x00000000

View File

@@ -0,0 +1,7 @@
# Ghostty snapshot fixture
# Wire version: 1
# Generated by its snapshot test; review before replacing.
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: encoded bytes
01 04 03 02 01 03 00 00 00 75 72 69 # 0x00000000

View File

@@ -0,0 +1,9 @@
# Ghostty snapshot fixture
# Wire version: 1
# Generated by its snapshot test; review before replacing.
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: encoded bytes
03 00 25 00 00 00 8c 05 d6 d3 01 00 01 00 00 00 # 0x00000000
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000010
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000020

View File

@@ -0,0 +1,8 @@
# Ghostty snapshot fixture
# Wire version: 1
# Generated by its snapshot test; review before replacing.
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: encoded bytes
02 01 04 03 06 05 08 07 0a 09 0c 0b 10 0f 0e 0d # 0x00000000
14 13 12 11 # 0x00000010

View File

@@ -0,0 +1,21 @@
# Ghostty snapshot fixture
# Wire version: 1
# Generated by its snapshot test; review before replacing.
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: PAGE header
03 00 02 00 02 00 02 00 08 00 00 02 80 00 00 00 # 0x00000000
00 01 00 00 # 0x00000010
# offset 0x00000014: style and hyperlink tables, rows, and cells
01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 # 0x00000014
00 00 03 00 00 00 00 00 01 2a 00 00 00 00 00 00 # 0x00000024
00 00 00 00 01 00 02 01 00 00 00 61 05 00 00 00 # 0x00000034
61 6c 70 68 61 03 00 01 04 03 02 01 04 00 00 00 # 0x00000044
62 65 74 61 04 00 01 05 00 01 00 01 00 41 00 00 # 0x00000054
00 00 00 00 00 00 02 02 00 03 00 03 00 00 00 00 # 0x00000064
00 00 00 00 00 01 00 00 00 01 00 01 00 07 00 00 # 0x00000074
00 00 00 00 00 0b 00 00 00 00 00 00 00 00 78 00 # 0x00000084
00 00 02 00 00 00 01 03 00 00 02 03 00 00 02 00 # 0x00000094
01 00 00 00 00 00 aa bb cc 00 00 00 00 00 00 03 # 0x000000a4
00 00 00 00 00 00 00 00 00 00 00 00 00 00 # 0x000000b4

View File

@@ -0,0 +1,7 @@
# Ghostty snapshot fixture
# Wire version: 1
# Generated by its snapshot test; review before replacing.
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: encoded bytes
03 00 18 00 00 00 1b 44 78 71 # 0x00000000

View File

@@ -0,0 +1,9 @@
# Ghostty snapshot fixture
# Wire version: 1
# Generated by its snapshot test; review before replacing.
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: encoded bytes
01 00 03 02 05 04 07 06 03 19 00 00 00 00 01 7f # 0x00000000
00 00 02 12 34 56 ff 03 00 00 0d 0c 0b 0a e4 3d # 0x00000010
02 07 01 02 04 08 10 1f 00 11 02 03 01 # 0x00000020

View File

@@ -0,0 +1,8 @@
# Ghostty snapshot fixture
# Wire version: 1
# Generated by its snapshot test; review before replacing.
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: encoded bytes
02 01 04 03 00 00 00 00 00 00 00 00 00 00 00 00 # 0x00000000
00 00 00 00 07 e4 3d # 0x00000010

View File

@@ -0,0 +1,7 @@
# Ghostty snapshot fixture
# Wire version: 1
# Generated by its snapshot test; review before replacing.
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: encoded bytes
00 00 00 00 01 7f 00 00 02 12 34 56 ff 03 00 00 # 0x00000000

View File

@@ -0,0 +1,13 @@
# Ghostty snapshot fixture
# Wire version: 1
# Generated by its snapshot test; review before replacing.
# On mismatch, the candidate is copied to the repository root.
# offset 0x00000000: encoded bytes
02 01 04 03 08 07 06 05 0c 0b 0a 09 01 00 02 00 # 0x00000000
03 00 04 00 01 01 00 02 00 41 00 00 00 01 03 02 # 0x00000010
02 01 04 04 01 21 01 01 00 00 00 00 00 00 00 00 # 0x00000020
00 00 00 00 02 00 00 01 00 00 00 00 02 00 00 01 # 0x00000030
01 02 03 00 00 00 00 00 00 00 00 01 04 05 06 01 # 0x00000040
07 08 09 01 0a 0b 0c ff ff ff ff ff ff ff ff 08 # 0x00000050
07 06 05 04 03 02 01 # 0x00000060