From 32f11a4663c897366f91876b5dda7fdc715e6ac0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 30 Jul 2026 15:14:14 -0700 Subject: [PATCH] terminal/snapshot: test fixtures --- src/terminal/snapshot/envelope.zig | 18 +- src/terminal/snapshot/fixture.zig | 416 ++++++++++++++++++ src/terminal/snapshot/history.zig | 25 +- src/terminal/snapshot/hyperlink.zig | 45 +- src/terminal/snapshot/page.zig | 88 ++-- src/terminal/snapshot/record.zig | 20 +- src/terminal/snapshot/screen.zig | 51 ++- src/terminal/snapshot/snapshot.zig | 81 +++- src/terminal/snapshot/style.zig | 21 +- src/terminal/snapshot/terminal.zig | 40 +- .../snapshot/testdata/complete-v1.hex | 138 ++++++ .../snapshot/testdata/envelope-v1.hex | 7 + .../snapshot/testdata/history-header-v1.hex | 7 + .../testdata/hyperlink-explicit-v1.hex | 7 + .../testdata/hyperlink-implicit-v1.hex | 7 + .../testdata/page-empty-record-v1.hex | 9 + .../snapshot/testdata/page-header-v1.hex | 8 + src/terminal/snapshot/testdata/page-v1.hex | 21 + .../testdata/record-page-header-v1.hex | 7 + .../snapshot/testdata/screen-header-v1.hex | 9 + .../testdata/screen-saved-cursor-v1.hex | 8 + src/terminal/snapshot/testdata/style-v1.hex | 7 + .../snapshot/testdata/terminal-header-v1.hex | 13 + 23 files changed, 913 insertions(+), 140 deletions(-) create mode 100644 src/terminal/snapshot/fixture.zig create mode 100644 src/terminal/snapshot/testdata/complete-v1.hex create mode 100644 src/terminal/snapshot/testdata/envelope-v1.hex create mode 100644 src/terminal/snapshot/testdata/history-header-v1.hex create mode 100644 src/terminal/snapshot/testdata/hyperlink-explicit-v1.hex create mode 100644 src/terminal/snapshot/testdata/hyperlink-implicit-v1.hex create mode 100644 src/terminal/snapshot/testdata/page-empty-record-v1.hex create mode 100644 src/terminal/snapshot/testdata/page-header-v1.hex create mode 100644 src/terminal/snapshot/testdata/page-v1.hex create mode 100644 src/terminal/snapshot/testdata/record-page-header-v1.hex create mode 100644 src/terminal/snapshot/testdata/screen-header-v1.hex create mode 100644 src/terminal/snapshot/testdata/screen-saved-cursor-v1.hex create mode 100644 src/terminal/snapshot/testdata/style-v1.hex create mode 100644 src/terminal/snapshot/testdata/terminal-header-v1.hex diff --git a/src/terminal/snapshot/envelope.zig b/src/terminal/snapshot/envelope.zig index 726167391..9963ced2a 100644 --- a/src/terminal/snapshot/envelope.zig +++ b/src/terminal/snapshot/envelope.zig @@ -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)); } } diff --git a/src/terminal/snapshot/fixture.zig b/src/terminal/snapshot/fixture.zig new file mode 100644 index 000000000..6d256ac3d --- /dev/null +++ b/src/terminal/snapshot/fixture.zig @@ -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; + } +} diff --git a/src/terminal/snapshot/history.zig b/src/terminal/snapshot/history.zig index c9f63510c..8422c4f73 100644 --- a/src/terminal/snapshot/history.zig +++ b/src/terminal/snapshot/history.zig @@ -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), diff --git a/src/terminal/snapshot/hyperlink.zig b/src/terminal/snapshot/hyperlink.zig index 804288249..6baade906 100644 --- a/src/terminal/snapshot/hyperlink.zig +++ b/src/terminal/snapshot/hyperlink.zig @@ -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| { diff --git a/src/terminal/snapshot/page.zig b/src/terminal/snapshot/page.zig index 9001ac8c9..4466f0663 100644 --- a/src/terminal/snapshot/page.zig +++ b/src/terminal/snapshot/page.zig @@ -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], diff --git a/src/terminal/snapshot/record.zig b/src/terminal/snapshot/record.zig index 6a5a20e70..19c9285a5 100644 --- a/src/terminal/snapshot/record.zig +++ b/src/terminal/snapshot/record.zig @@ -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)); } } diff --git a/src/terminal/snapshot/screen.zig b/src/terminal/snapshot/screen.zig index ea282db56..e9de03c66 100644 --- a/src/terminal/snapshot/screen.zig +++ b/src/terminal/snapshot/screen.zig @@ -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( diff --git a/src/terminal/snapshot/snapshot.zig b/src/terminal/snapshot/snapshot.zig index 48494a5fe..cd7215ab4 100644 --- a/src/terminal/snapshot/snapshot.zig +++ b/src/terminal/snapshot/snapshot.zig @@ -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. diff --git a/src/terminal/snapshot/style.zig b/src/terminal/snapshot/style.zig index 481d7da99..268c29289 100644 --- a/src/terminal/snapshot/style.zig +++ b/src/terminal/snapshot/style.zig @@ -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))); diff --git a/src/terminal/snapshot/terminal.zig b/src/terminal/snapshot/terminal.zig index 40a79c29d..d0fd0e49f 100644 --- a/src/terminal/snapshot/terminal.zig +++ b/src/terminal/snapshot/terminal.zig @@ -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; diff --git a/src/terminal/snapshot/testdata/complete-v1.hex b/src/terminal/snapshot/testdata/complete-v1.hex new file mode 100644 index 000000000..8982c2add --- /dev/null +++ b/src/terminal/snapshot/testdata/complete-v1.hex @@ -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 diff --git a/src/terminal/snapshot/testdata/envelope-v1.hex b/src/terminal/snapshot/testdata/envelope-v1.hex new file mode 100644 index 000000000..3e4ddadfa --- /dev/null +++ b/src/terminal/snapshot/testdata/envelope-v1.hex @@ -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 diff --git a/src/terminal/snapshot/testdata/history-header-v1.hex b/src/terminal/snapshot/testdata/history-header-v1.hex new file mode 100644 index 000000000..7949ca620 --- /dev/null +++ b/src/terminal/snapshot/testdata/history-header-v1.hex @@ -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 diff --git a/src/terminal/snapshot/testdata/hyperlink-explicit-v1.hex b/src/terminal/snapshot/testdata/hyperlink-explicit-v1.hex new file mode 100644 index 000000000..6f925099a --- /dev/null +++ b/src/terminal/snapshot/testdata/hyperlink-explicit-v1.hex @@ -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 diff --git a/src/terminal/snapshot/testdata/hyperlink-implicit-v1.hex b/src/terminal/snapshot/testdata/hyperlink-implicit-v1.hex new file mode 100644 index 000000000..8ae474676 --- /dev/null +++ b/src/terminal/snapshot/testdata/hyperlink-implicit-v1.hex @@ -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 diff --git a/src/terminal/snapshot/testdata/page-empty-record-v1.hex b/src/terminal/snapshot/testdata/page-empty-record-v1.hex new file mode 100644 index 000000000..06db7cc54 --- /dev/null +++ b/src/terminal/snapshot/testdata/page-empty-record-v1.hex @@ -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 diff --git a/src/terminal/snapshot/testdata/page-header-v1.hex b/src/terminal/snapshot/testdata/page-header-v1.hex new file mode 100644 index 000000000..811ad80fc --- /dev/null +++ b/src/terminal/snapshot/testdata/page-header-v1.hex @@ -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 diff --git a/src/terminal/snapshot/testdata/page-v1.hex b/src/terminal/snapshot/testdata/page-v1.hex new file mode 100644 index 000000000..d3c04d0c4 --- /dev/null +++ b/src/terminal/snapshot/testdata/page-v1.hex @@ -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 diff --git a/src/terminal/snapshot/testdata/record-page-header-v1.hex b/src/terminal/snapshot/testdata/record-page-header-v1.hex new file mode 100644 index 000000000..339246dab --- /dev/null +++ b/src/terminal/snapshot/testdata/record-page-header-v1.hex @@ -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 diff --git a/src/terminal/snapshot/testdata/screen-header-v1.hex b/src/terminal/snapshot/testdata/screen-header-v1.hex new file mode 100644 index 000000000..3b8f9ef92 --- /dev/null +++ b/src/terminal/snapshot/testdata/screen-header-v1.hex @@ -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 diff --git a/src/terminal/snapshot/testdata/screen-saved-cursor-v1.hex b/src/terminal/snapshot/testdata/screen-saved-cursor-v1.hex new file mode 100644 index 000000000..62470761c --- /dev/null +++ b/src/terminal/snapshot/testdata/screen-saved-cursor-v1.hex @@ -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 diff --git a/src/terminal/snapshot/testdata/style-v1.hex b/src/terminal/snapshot/testdata/style-v1.hex new file mode 100644 index 000000000..94e381cda --- /dev/null +++ b/src/terminal/snapshot/testdata/style-v1.hex @@ -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 diff --git a/src/terminal/snapshot/testdata/terminal-header-v1.hex b/src/terminal/snapshot/testdata/terminal-header-v1.hex new file mode 100644 index 000000000..690891c65 --- /dev/null +++ b/src/terminal/snapshot/testdata/terminal-header-v1.hex @@ -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