From 13bc78b7f33536351fef16b2e37992b845215679 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 30 Jul 2026 20:16:46 -0700 Subject: [PATCH] terminal/snapshot: grid tests --- src/terminal/snapshot/checkpoint.zig | 57 ++++-- src/terminal/snapshot/grid.zig | 162 ++++++++++++++++++ src/terminal/snapshot/page.zig | 34 ---- .../snapshot/testdata/checkpoint-ready-v1.hex | 14 ++ src/terminal/snapshot/testdata/grid-v1.hex | 29 ++++ 5 files changed, 248 insertions(+), 48 deletions(-) create mode 100644 src/terminal/snapshot/testdata/checkpoint-ready-v1.hex create mode 100644 src/terminal/snapshot/testdata/grid-v1.hex diff --git a/src/terminal/snapshot/checkpoint.zig b/src/terminal/snapshot/checkpoint.zig index 3eca93f82..7d8f0f097 100644 --- a/src/terminal/snapshot/checkpoint.zig +++ b/src/terminal/snapshot/checkpoint.zig @@ -24,6 +24,7 @@ //! ``` const std = @import("std"); +const test_fixture = @import("fixture.zig"); const record = @import("record.zig"); const Blake3 = std.crypto.hash.Blake3; @@ -110,27 +111,55 @@ pub fn decode( } } -test "checkpoint BLAKE3-256 registry" { - const expected = [_]u8{ - 0x64, 0x37, 0xb3, 0xac, 0x38, 0x46, 0x51, 0x33, - 0xff, 0xb6, 0x3b, 0x75, 0x27, 0x3a, 0x8d, 0xb5, - 0x48, 0xc5, 0x58, 0x46, 0x5d, 0x79, 0xdb, 0x03, - 0xfd, 0x35, 0x9c, 0x6c, 0xd5, 0xbd, 0x9d, 0x85, - }; - var actual: Digest = undefined; - Blake3.hash("abc", &actual, .{}); - try std.testing.expectEqual(expected, actual); +const test_ready_fixture = test_fixture.parse( + @embedFile("testdata/checkpoint-ready-v1.hex"), +); +test "READY golden encoding and BLAKE3-256 registry" { + const prefix = "abc"; + + var snapshot: std.Io.Writer.Allocating = .init(std.testing.allocator); + defer snapshot.deinit(); + try snapshot.writer.writeAll(prefix); + try encode(.ready, &snapshot); + + try test_fixture.expectEqual( + .bytes, + "src/terminal/snapshot/testdata/checkpoint-ready-v1.hex", + "snapshot_fixture-checkpoint-ready-v1.hex", + &test_ready_fixture, + snapshot.written(), + ); + + // The checked-in payload independently locks the BLAKE3-256 registry. + var actual: Digest = undefined; + Blake3.hash(prefix, &actual, .{}); + const payload_offset = prefix.len + record.Header.len; + try std.testing.expectEqualSlices( + u8, + test_ready_fixture[payload_offset..][0..actual.len], + &actual, + ); + + // Finalizing a streaming hasher neither consumes nor resets it. var hasher = Blake3.init(.{}); hasher.update("a"); - var prefix: Digest = undefined; - Blake3.hash("a", &prefix, .{}); + var prefix_digest: Digest = undefined; + Blake3.hash("a", &prefix_digest, .{}); hasher.final(&actual); - try std.testing.expectEqual(prefix, actual); + try std.testing.expectEqual(prefix_digest, actual); hasher.update("bc"); hasher.final(&actual); - try std.testing.expectEqual(expected, actual); + try std.testing.expectEqualSlices( + u8, + test_ready_fixture[payload_offset..][0..actual.len], + &actual, + ); + + // Decode the checked-in record rather than the generated candidate. + var source: std.Io.Reader = .fixed(test_ready_fixture[prefix.len..]); + try decode(.ready, actual, &source); } test "READY and FINISH checkpoint coverage" { diff --git a/src/terminal/snapshot/grid.zig b/src/terminal/snapshot/grid.zig index f9aa1d2b1..4d598aa46 100644 --- a/src/terminal/snapshot/grid.zig +++ b/src/terminal/snapshot/grid.zig @@ -105,6 +105,7 @@ //! nonzero base codepoint. const std = @import("std"); +const test_fixture = @import("fixture.zig"); const io = @import("io.zig"); const kitty = @import("../kitty.zig"); const terminal_hyperlink = @import("../hyperlink.zig"); @@ -543,3 +544,164 @@ const CellHeader = struct { _padding: u5 = 0, }; }; + +const test_golden_fixture = test_fixture.parse( + @embedFile("testdata/grid-v1.hex"), +); + +test "grid golden encoding and decoding" { + const capacity: terminal_page.Capacity = .{ + .cols = 3, + .rows = 2, + .styles = 0, + .hyperlink_bytes = 0, + .grapheme_bytes = 64, + .string_bytes = 0, + }; + var source = try TerminalPage.init(capacity); + defer source.deinit(); + + // The first row covers semantic metadata, a wide-cell pair, and a + // multi-codepoint grapheme. + const wide = source.getRowAndCell(0, 0); + wide.row.semantic_prompt = .prompt; + wide.cell.* = .init('A'); + wide.cell.wide = .wide; + wide.cell.protected = true; + wide.cell.semantic_content = .prompt; + + const tail = source.getRowAndCell(1, 0); + tail.cell.wide = .spacer_tail; + tail.cell.semantic_content = .input; + + const grapheme = source.getRowAndCell(2, 0); + grapheme.cell.* = .init('x'); + try source.setGraphemes( + grapheme.row, + grapheme.cell, + &.{ 0x0301, 0x0302 }, + ); + + // The second row covers both background-color kinds and a wrapped spacer + // head with the remaining row semantic value. + const palette = source.getRowAndCell(0, 1); + palette.cell.content_tag = .bg_color_palette; + palette.cell.content = .{ .color_palette = .{ .data = 7 } }; + + const rgb = source.getRowAndCell(1, 1); + rgb.cell.content_tag = .bg_color_rgb; + rgb.cell.content = .{ .color_rgb = .{ + .r = 0xaa, + .g = 0xbb, + .b = 0xcc, + } }; + rgb.cell.protected = true; + + const head = source.getRowAndCell(2, 1); + head.cell.wide = .spacer_head; + head.row.wrap = true; + head.row.wrap_continuation = true; + head.row.semantic_prompt = .prompt_continuation; + + var encoded: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&encoded); + try encode(&source, &writer); + try test_fixture.expectEqual( + .bytes, + "src/terminal/snapshot/testdata/grid-v1.hex", + "snapshot_fixture-grid-v1.hex", + &test_golden_fixture, + writer.buffered(), + ); + + var destination = try TerminalPage.init(capacity); + defer destination.deinit(); + var style_remap = StyleRemap.init(std.testing.allocator); + defer style_remap.deinit(); + var hyperlink_remap = HyperlinkRemap.init(std.testing.allocator); + defer hyperlink_remap.deinit(); + + // Decode the checked-in reference through a one-byte reader buffer. + var fixture_reader: std.Io.Reader = .fixed(&test_golden_fixture); + var read_buffer: [1]u8 = undefined; + var limited = fixture_reader.limited(.unlimited, &read_buffer); + try decode( + &destination, + &limited.interface, + &style_remap, + &hyperlink_remap, + ); + try destination.verifyIntegrity(std.testing.allocator); + + const decoded_wide = destination.getRowAndCell(0, 0); + try std.testing.expectEqual(@as(u21, 'A'), decoded_wide.cell.codepoint()); + try std.testing.expectEqual(TerminalCell.Wide.wide, decoded_wide.cell.wide); + try std.testing.expect(decoded_wide.cell.protected); + try std.testing.expectEqual( + TerminalCell.SemanticContent.prompt, + decoded_wide.cell.semantic_content, + ); + try std.testing.expectEqual( + TerminalRow.SemanticPrompt.prompt, + decoded_wide.row.semantic_prompt, + ); + + const decoded_tail = destination.getRowAndCell(1, 0); + try std.testing.expectEqual( + TerminalCell.Wide.spacer_tail, + decoded_tail.cell.wide, + ); + try std.testing.expectEqual( + TerminalCell.SemanticContent.input, + decoded_tail.cell.semantic_content, + ); + + const decoded_grapheme = destination.getRowAndCell(2, 0); + try std.testing.expectEqualSlices( + u21, + &.{ 0x0301, 0x0302 }, + destination.lookupGrapheme(decoded_grapheme.cell).?, + ); + + const decoded_palette = destination.getRowAndCell(0, 1); + try std.testing.expectEqual( + TerminalCell.ContentTag.bg_color_palette, + decoded_palette.cell.content_tag, + ); + try std.testing.expectEqual( + @as(u8, 7), + decoded_palette.cell.content.color_palette.data, + ); + + const decoded_rgb = destination.getRowAndCell(1, 1); + try std.testing.expectEqual( + TerminalCell.ContentTag.bg_color_rgb, + decoded_rgb.cell.content_tag, + ); + try std.testing.expectEqual( + TerminalCell.RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, + decoded_rgb.cell.content.color_rgb, + ); + try std.testing.expect(decoded_rgb.cell.protected); + + const decoded_head = destination.getRowAndCell(2, 1); + try std.testing.expectEqual( + TerminalCell.Wide.spacer_head, + decoded_head.cell.wide, + ); + try std.testing.expect(decoded_head.row.wrap); + try std.testing.expect(decoded_head.row.wrap_continuation); + try std.testing.expectEqual( + TerminalRow.SemanticPrompt.prompt_continuation, + decoded_head.row.semantic_prompt, + ); + + // A re-encode proves the decoded native page retains every wire field. + var reencoded: [128]u8 = undefined; + var rewriter: std.Io.Writer = .fixed(&reencoded); + try encode(&destination, &rewriter); + try std.testing.expectEqualStrings( + &test_golden_fixture, + rewriter.buffered(), + ); +} diff --git a/src/terminal/snapshot/page.zig b/src/terminal/snapshot/page.zig index 4466f0663..2c6c1b4d6 100644 --- a/src/terminal/snapshot/page.zig +++ b/src/terminal/snapshot/page.zig @@ -764,13 +764,6 @@ test "framed PAGE encode and decode a sparse native page" { try std.testing.expectEqual(null, hyperlink_it.next()); const decoded_first = decoded.getRowAndCell(0, 0); - try std.testing.expectEqual(@as(u21, 'A'), decoded_first.cell.codepoint()); - try std.testing.expectEqual(TerminalCell.Wide.wide, decoded_first.cell.wide); - try std.testing.expect(decoded_first.cell.protected); - try std.testing.expectEqual( - TerminalCell.SemanticContent.prompt, - decoded_first.cell.semantic_content, - ); try std.testing.expectEqual(@as(TerminalStyleId, 1), decoded_first.cell.style_id); try std.testing.expectEqual( @as(TerminalHyperlinkId, 1), @@ -778,39 +771,12 @@ test "framed PAGE encode and decode a sparse native page" { ); const decoded_second = decoded.getRowAndCell(1, 0); - try std.testing.expectEqual(TerminalCell.Wide.spacer_tail, decoded_second.cell.wide); try std.testing.expectEqual(@as(TerminalStyleId, 2), decoded_second.cell.style_id); try std.testing.expectEqual( @as(TerminalHyperlinkId, 2), decoded.lookupHyperlink(decoded_second.cell).?, ); - const decoded_grapheme = decoded.getRowAndCell(0, 1); - try std.testing.expectEqualSlices( - u21, - &.{ 0x0301, 0x0302 }, - decoded.lookupGrapheme(decoded_grapheme.cell).?, - ); - - const decoded_rgb = decoded.getRowAndCell(1, 1); - try std.testing.expectEqual(TerminalCell.ContentTag.bg_color_rgb, decoded_rgb.cell.content_tag); - try std.testing.expectEqual( - TerminalCell.RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, - decoded_rgb.cell.content.color_rgb, - ); - - const decoded_spacer_head = decoded.getRowAndCell(2, 1); - try std.testing.expectEqual( - TerminalCell.Wide.spacer_head, - decoded_spacer_head.cell.wide, - ); - try std.testing.expect(decoded_spacer_head.row.wrap); - try std.testing.expect(decoded_spacer_head.row.wrap_continuation); - try std.testing.expectEqual( - TerminalRow.SemanticPrompt.prompt_continuation, - decoded_spacer_head.row.semantic_prompt, - ); - // The first re-encode reflects compacted IDs; subsequent round trips must // stabilize byte-for-byte. var reencoded: [512]u8 = undefined; diff --git a/src/terminal/snapshot/testdata/checkpoint-ready-v1.hex b/src/terminal/snapshot/testdata/checkpoint-ready-v1.hex new file mode 100644 index 000000000..b3a24104b --- /dev/null +++ b/src/terminal/snapshot/testdata/checkpoint-ready-v1.hex @@ -0,0 +1,14 @@ +# Ghostty snapshot fixture +# Wire version: 1 +# Generated by its snapshot test; review before replacing. +# On mismatch, the candidate is copied to the repository root. + +# prefix covered by READY +61 62 63 + +# READY record header: tag, payload length, CRC32C +05 00 20 00 00 00 90 5b 2d 06 + +# BLAKE3-256 digest of the prefix +64 37 b3 ac 38 46 51 33 ff b6 3b 75 27 3a 8d b5 +48 c5 58 46 5d 79 db 03 fd 35 9c 6c d5 bd 9d 85 diff --git a/src/terminal/snapshot/testdata/grid-v1.hex b/src/terminal/snapshot/testdata/grid-v1.hex new file mode 100644 index 000000000..83a668326 --- /dev/null +++ b/src/terminal/snapshot/testdata/grid-v1.hex @@ -0,0 +1,29 @@ +# Ghostty snapshot fixture +# Wire version: 1 +# Generated by its snapshot test; review before replacing. +# On mismatch, the candidate is copied to the repository root. + +# row 0: prompt +04 + +# row 0, cell 0: protected prompt, wide "A" +00 01 05 00 00 00 00 00 41 00 00 00 00 00 00 00 + +# row 0, cell 1: input spacer tail +00 02 02 00 00 00 00 00 00 00 00 00 00 00 00 00 + +# row 0, cell 2: "x" with two grapheme suffixes +00 00 00 00 00 00 00 00 78 00 00 00 02 00 00 00 +01 03 00 00 02 03 00 00 + +# row 1: wrapped prompt continuation +0b + +# row 1, cell 0: palette background 7 +01 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00 + +# row 1, cell 1: protected RGB background +02 00 01 00 00 00 00 00 aa bb cc 00 00 00 00 00 + +# row 1, cell 2: spacer head +00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00