summaryrefslogtreecommitdiff
path: root/test/image_harness.zig
diff options
context:
space:
mode:
Diffstat (limited to 'test/image_harness.zig')
-rw-r--r--test/image_harness.zig278
1 files changed, 133 insertions, 145 deletions
diff --git a/test/image_harness.zig b/test/image_harness.zig
index f51839ae..4b1c527c 100644
--- a/test/image_harness.zig
+++ b/test/image_harness.zig
@@ -234,78 +234,79 @@ fn runKittyPdf(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8)
try h.expectRawContains("\x1b_Ga=p,i=", "first PDF page was not placed in the pane body");
try h.expectContains("pdf 1/4 width PdfFit", "PDF pane did not expose its initial width fit");
- // Fit-width is the initial PDF policy. It must use Kitty's explicit column
- // scaling rather than Vaxis contain. The PDF shares this test session with
- // the initial shell pane, making its body slightly narrower than the page:
- // the full source is retained and vertically letterboxed by a pixel offset.
+ // Fit-width is the initial PDF policy. The short fixture pages make the
+ // first boundary visible immediately, so both page rasters must coexist.
+ // Page one fills its own document box instead of being letterboxed inside
+ // the whole pane viewport.
+ const initial_transmits = std.mem.count(u8, h.raw.items, transmit);
+ if (initial_transmits < 2) return error.KittyPdfBoundaryPageMissing;
const width_apc = kittyPlacementApc(h.raw.items, 0) orelse return error.KittyPdfWidthPlacementMissing;
+ const width_clip_h = kittyParam(width_apc, 'h') orelse return error.KittyPdfWidthCropWrong;
if (kittyParam(width_apc, 'x') != 0 or kittyParam(width_apc, 'y') != 0 or
- kittyParam(width_apc, 'w') != 96)
+ kittyParam(width_apc, 'w') != 96 or width_clip_h == 0 or width_clip_h > 64 or
+ kittyParam(width_apc, 'c') == null)
{
std.debug.print("unexpected Kitty fit-width placement: {s}\n", .{width_apc});
return error.KittyPdfWidthCropWrong;
}
- const source_h = kittyParam(width_apc, 'h') orelse return error.KittyPdfWidthCropMissing;
- const pixel_y = kittyParam(width_apc, 'Y') orelse return error.KittyPdfWidthLetterboxMissing;
- if (source_h != 64 or pixel_y == 0 or kittyParam(width_apc, 'c') == null) {
- std.debug.print("unexpected Kitty fit-width letterbox: {s}\n", .{width_apc});
- return error.KittyPdfWidthLetterboxWrong;
- }
+ const initial_aspect_h = (38 * 8 * width_clip_h + 95) / 96;
+ if (((initial_aspect_h + 15) / 16) * 16 > 202)
+ return error.KittyPdfWidthPlacementOverdraw;
- // Placement-only state reuses the terminal image. Middle-click the
- // visible PdfFit word in the PDF tag, wait for a fit-height placement
- // with explicit rows and a full source image, and prove that no second
- // transmit happened.
- const transmit_before_fit = std.mem.count(u8, h.raw.items, transmit);
- const fit_raw_start = h.raw.items.len;
- const tag = try h.screenText();
- defer gpa.free(tag);
- const fit_at = std.mem.indexOf(u8, tag, "PdfFit") orelse
- return error.PdfFitMissing;
- const line_at = std.mem.lastIndexOfScalar(u8, tag[0..fit_at], '\n');
- const mouse_col = fit_at - if (line_at) |at| at + 1 else 0;
- const mouse_row = std.mem.count(u8, tag[0..fit_at], "\n");
- const click = try std.fmt.allocPrint(
- arena,
- "\x1b[<1;{d};{d}M\x1b[<1;{d};{d}m",
- .{ mouse_col + 1, mouse_row + 1, mouse_col + 1, mouse_row + 1 },
- );
- try h.send(click);
- const fit_deadline = eh.nowMs() + 3000;
- while (eh.nowMs() < fit_deadline) {
+ // A one-row document scroll changes placements, not pixel generations or
+ // the active page. This is the cache-reuse seam the old page-at-a-time
+ // harness could not exercise.
+ const placements_before = std.mem.count(u8, h.raw.items, "\x1b_Ga=p,i=");
+ const placement_start = h.raw.items.len;
+ try h.send("j");
+ const placement_deadline = eh.nowMs() + 2000;
+ while (eh.nowMs() < placement_deadline and
+ std.mem.count(u8, h.raw.items, "\x1b_Ga=p,i=") <= placements_before)
_ = try h.pumpOnce(50);
- if (kittyPlacementApcWithParam(h.raw.items, fit_raw_start, 'r') != null) break;
- }
- try h.expectContains("pdf 1/4 height PdfFit", "PdfFit did not expose the active height fit");
- const height_apc = kittyPlacementApcWithParam(h.raw.items, fit_raw_start, 'r') orelse
- return error.KittyPdfHeightPlacementMissing;
- const height_crop_w = kittyParam(height_apc, 'w') orelse return error.KittyPdfHeightPlacementWrong;
- if (kittyParam(height_apc, 'x') != 0 or kittyParam(height_apc, 'y') != 0 or
- height_crop_w == 0 or height_crop_w >= 96 or kittyParam(height_apc, 'h') != 64 or
- kittyParam(height_apc, 'r') == null)
- return error.KittyPdfHeightPlacementWrong;
- if (std.mem.count(u8, h.raw.items, transmit) != transmit_before_fit)
- return error.KittyPdfFitRetransmitted;
+ if (std.mem.count(u8, h.raw.items, transmit) != initial_transmits)
+ return error.KittyPdfPlacementScrollRetransmitted;
+ try h.expectContains("pdf 1/4 width PdfFit", "one-row scroll snapped to another PDF page");
- // Same pane, different pixels: page navigation must delete the cached
- // Kitty image and transmit the new revision, not keep showing page one.
- try h.send("j");
- const second_deadline = eh.nowMs() + 5000;
- while (eh.nowMs() < second_deadline) {
+ // The newly exposed page-two head is 29px tall and begins three pixels
+ // into a cell. Kitty infers an integer row count when only c is supplied;
+ // the crop must therefore stay within one 16px row, not ceil into the gap.
+ const tail_deadline = eh.nowMs() + 2000;
+ while (eh.nowMs() < tail_deadline and
+ kittyPlacementApcWithParam(h.raw.items, placement_start, 'Y') == null)
_ = try h.pumpOnce(50);
- const deleted = std.mem.indexOf(u8, h.raw.items, "\x1b_Ga=d,d=I,i=") != null;
- if (deleted and std.mem.count(u8, h.raw.items, transmit) >= 2 and h.contains("pdf 2/4 height PdfFit")) break;
- }
- try h.expectRawContains("\x1b_Ga=d,d=I,i=", "page navigation did not release the old Kitty PDF image");
- if (std.mem.count(u8, h.raw.items, transmit) < 2) return error.KittyPdfRetransmitMissing;
- try h.expectContains("pdf 2/4 height PdfFit", "j did not retain height fit on PDF page two");
+ const tail_apc = kittyPlacementApcWithParam(h.raw.items, placement_start, 'Y') orelse
+ return error.KittyPdfSafeTailPlacementMissing;
+ const tail_cols = kittyParam(tail_apc, 'c') orelse return error.KittyPdfSafeTailPlacementWrong;
+ const tail_w = kittyParam(tail_apc, 'w') orelse return error.KittyPdfSafeTailPlacementWrong;
+ const tail_h = kittyParam(tail_apc, 'h') orelse return error.KittyPdfSafeTailPlacementWrong;
+ if (kittyParam(tail_apc, 'Y') != 3 or tail_cols != 38 or tail_w != 96)
+ return error.KittyPdfSafeTailPlacementWrong;
+ const tail_aspect_h = (tail_cols * 8 * tail_h + tail_w - 1) / tail_w;
+ const tail_declared_h = ((tail_aspect_h + 15) / 16) * 16;
+ if (tail_declared_h > 29) return error.KittyPdfTailOverpaintedGap;
+
+ // A viewport step crosses the boundary continuously: page one leaves and
+ // is deleted, page two remains cached, and newly intersecting page three
+ // is transmitted. Distinct fixture heights identify the latter on wire.
+ const page_step_start = h.raw.items.len;
+ const page_three_transmit = "\x1b_Gf=32,s=96,v=67,i=";
+ try h.send("\x06");
+ try h.expectWaitFor("pdf 2/4 width PdfFit", 5000, "continuous page-down did not enter PDF page two");
+ const page_step_deadline = eh.nowMs() + 5000;
+ while (eh.nowMs() < page_step_deadline and
+ std.mem.indexOf(u8, h.raw.items[page_step_start..], page_three_transmit) == null)
+ _ = try h.pumpOnce(50);
+ if (std.mem.indexOf(u8, h.raw.items[page_step_start..], "\x1b_Ga=d,d=I,i=") == null)
+ return error.KittyPdfOffscreenPageNotReleased;
+ if (std.mem.indexOf(u8, h.raw.items[page_step_start..], page_three_transmit) == null)
+ return error.KittyPdfNewVisiblePageMissing;
+ if (std.mem.count(u8, h.raw.items, transmit) != initial_transmits)
+ return error.KittyPdfRetainedNeighborRetransmitted;
- // Exercise the semantic seam too: return to page one, search an exact
- // occurrence across the document, then let n Look the page-two result.
- // That Look must change the page and therefore produce another pixel
- // generation.
- try h.send("k");
- try h.expectWaitFor("pdf 1/4 height PdfFit", 5000, "k did not return to PDF page one at height fit");
+ // Search still addresses semantic pages inside the strip. Returning one
+ // viewport, then revealing page two's hit, creates a decorated revision.
+ try h.send("\x02");
+ try h.expectWaitFor("pdf 1/4 width PdfFit", 5000, "continuous page-up did not return to PDF page one");
try h.send("/SECOND\r");
try h.expectWaitFor("pages.pdf:2:1 SECOND", 5000, "PDF search did not preserve the exact page-two query occurrence");
const before_look = std.mem.count(u8, h.raw.items, transmit);
@@ -313,85 +314,30 @@ fn runKittyPdf(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8)
const look_deadline = eh.nowMs() + 5000;
while (eh.nowMs() < look_deadline) {
_ = try h.pumpOnce(50);
- if (std.mem.count(u8, h.raw.items, transmit) > before_look and h.contains("pdf 2/4 height PdfFit")) break;
+ if (std.mem.count(u8, h.raw.items, transmit) > before_look and h.contains("pdf 2/4 width PdfFit")) break;
}
- try h.expectContains("pdf 2/4 height PdfFit", "n did not Look the PDF search result at page two");
+ try h.expectContains("pdf 2/4 width PdfFit", "n did not reveal the PDF search result at page two");
if (std.mem.count(u8, h.raw.items, transmit) <= before_look) return error.KittyPdfLookRetransmitMissing;
- // Kitty turns a larger wheel offset into adjacent SGR wheel presses. The
- // TTY shell may drain those presses in one input batch, but every native
- // PDF page transition must be rastered before the next press consumes the
- // new page's geometry. Distinct page heights make that ordering observable
- // in the actual Kitty transmit headers: page 2 is 96x64, page 3 96x67,
- // and page 4 96x69. This is also a direct one-detent check before the
- // burst and a direction-reversal check after it.
- const wheel_up = try std.fmt.allocPrint(arena, "\x1b[<64;{d};{d}M", .{ mouse_col + 1, mouse_row + 1 });
- const wheel_down = try std.fmt.allocPrint(arena, "\x1b[<65;{d};{d}M", .{ mouse_col + 1, mouse_row + 1 });
- const page_three_transmit = "\x1b_Gf=32,s=96,v=67,i=";
- const page_four_transmit = "\x1b_Gf=32,s=96,v=69,i=";
-
- try h.send(wheel_up);
- try h.expectWaitFor("pdf 1/4 height PdfFit", 5000, "wheel-up did not return to PDF page one");
- const single_start = h.raw.items.len;
- try h.send(wheel_down);
- try h.expectWaitFor("pdf 2/4 height PdfFit", 5000, "one wheel detent did not advance exactly one PDF page");
- try h.pump(100);
- if (std.mem.indexOf(u8, h.raw.items[single_start..], page_three_transmit) != null)
- return error.KittyPdfSingleWheelSkippedPage;
-
- try h.send(wheel_up);
- try h.expectWaitFor("pdf 1/4 height PdfFit", 5000, "wheel-up did not reset the burst fixture");
- const burst_start = h.raw.items.len;
- const forward_burst = try std.fmt.allocPrint(
- arena,
- "\x1b[<65;{d};{d}M\x1b[<65;{d};{d}M\x1b[<65;{d};{d}M",
- .{ mouse_col + 1, mouse_row + 1, mouse_col + 1, mouse_row + 1, mouse_col + 1, mouse_row + 1 },
- );
- try h.send(forward_burst);
- try h.expectWaitFor("pdf 4/4 height PdfFit", 8000, "forward Kitty wheel burst did not reach PDF page four");
- const forward_raw = h.raw.items[burst_start..];
- const page_two_at = std.mem.indexOf(u8, forward_raw, transmit) orelse
- return error.KittyPdfBurstSkippedPageTwoRender;
- const page_three_at = std.mem.indexOf(u8, forward_raw, page_three_transmit) orelse
- return error.KittyPdfBurstSkippedPageThreeRender;
- const page_four_at = std.mem.indexOf(u8, forward_raw, page_four_transmit) orelse
- return error.KittyPdfBurstSkippedPageFourRender;
- if (!(page_two_at < page_three_at and page_three_at < page_four_at))
- return error.KittyPdfBurstRenderOrderWrong;
-
- const reverse_start = h.raw.items.len;
- const reverse_burst = try std.fmt.allocPrint(
- arena,
- "\x1b[<64;{d};{d}M\x1b[<64;{d};{d}M\x1b[<65;{d};{d}M\x1b[<64;{d};{d}M",
- .{
- mouse_col + 1, mouse_row + 1,
- mouse_col + 1, mouse_row + 1,
- mouse_col + 1, mouse_row + 1,
- mouse_col + 1, mouse_row + 1,
- },
- );
- try h.send(reverse_burst);
- try h.expectWaitFor("pdf 2/4 height PdfFit", 8000, "mixed-direction Kitty wheel burst did not return to PDF page two");
- const reverse_deadline = eh.nowMs() + 5000;
- while (eh.nowMs() < reverse_deadline) {
- const emitted = h.raw.items[reverse_start..];
- if (std.mem.count(u8, emitted, page_three_transmit) >= 2 and
- std.mem.count(u8, emitted, transmit) >= 2) break;
+ // PdfFit changes only placement. The active decorated raster remains the
+ // same terminal image while height fit supplies an explicit row extent.
+ const before_fit = std.mem.count(u8, h.raw.items, transmit);
+ const fit_raw_start = h.raw.items.len;
+ try h.send(" tz");
+ try h.expectWaitFor("pdf 2/4 height PdfFit", 5000, "PdfFit did not expose height fit");
+ const fit_deadline = eh.nowMs() + 3000;
+ while (eh.nowMs() < fit_deadline and
+ kittyPlacementApcWithParam(h.raw.items, fit_raw_start, 'r') == null)
_ = try h.pumpOnce(50);
- }
- const reverse_raw = h.raw.items[reverse_start..];
- const reverse_three_1 = std.mem.indexOf(u8, reverse_raw, page_three_transmit) orelse
- return error.KittyPdfReverseBurstSkippedPageThreeRender;
- const reverse_two_1 = std.mem.indexOfPos(u8, reverse_raw, reverse_three_1 + page_three_transmit.len, transmit) orelse
- return error.KittyPdfReverseBurstSkippedPageTwoRender;
- const reverse_three_2 = std.mem.indexOfPos(u8, reverse_raw, reverse_two_1 + transmit.len, page_three_transmit) orelse
- return error.KittyPdfDirectionChangeSkippedPageThreeRender;
- const reverse_two_2 = std.mem.indexOfPos(u8, reverse_raw, reverse_three_2 + page_three_transmit.len, transmit) orelse
- return error.KittyPdfDirectionChangeSkippedPageTwoRender;
- if (!(reverse_three_1 < reverse_two_1 and reverse_two_1 < reverse_three_2 and reverse_three_2 < reverse_two_2))
- return error.KittyPdfReverseBurstRenderOrderWrong;
+ const height_apc = kittyPlacementApcWithParam(h.raw.items, fit_raw_start, 'r') orelse
+ return error.KittyPdfHeightPlacementMissing;
+ const height_crop_w = kittyParam(height_apc, 'w') orelse return error.KittyPdfHeightPlacementWrong;
+ if (height_crop_w == 0 or height_crop_w > 96 or kittyParam(height_apc, 'h') != 64)
+ return error.KittyPdfHeightPlacementWrong;
+ if (std.mem.count(u8, h.raw.items, transmit) != before_fit)
+ return error.KittyPdfFitRetransmitted;
- std.debug.print("PDF harness ok: Kitty fit/cache + page/search + metered wheel transitions\n", .{});
+ std.debug.print("PDF harness ok: Kitty continuous boundary + exact cache + search/fit\n", .{});
}
fn kittyPlacementApc(raw: []const u8, start: usize) ?[]const u8 {
@@ -474,11 +420,23 @@ fn runGuiPdf(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8) !v
var h = try eh.Harness.initArgs(gpa, exe, 10, 20, pdf_path);
defer h.deinit();
const fit_width = try waitForCaptureBounds(&h, capture_path, pdf_red, 500, 5000);
+ const initial_green = try waitForCaptureBounds(&h, capture_path, pdf_green, 500, 5000);
+ try expectThemeGap(capture_path, fit_width, initial_green);
- // The narrow PDF pane makes fit-width vertically letterboxed. Fit-height
- // keeps roughly the same colored width but fills/crops the full body
- // height. These are GPU readback extents, so this exercises SDL's UV crop
- // and destination rectangle rather than merely observing the page color.
+ // One row moves both native quads by the exact document distance. They
+ // remain together on screen, separated by the same theme-colored gap; a
+ // page-at-a-time implementation would replace red with green here.
+ _ = libc.unlink(capture_path);
+ try h.send("j");
+ const moved_red = try waitForCaptureBounds(&h, capture_path, pdf_red, 500, 5000);
+ const moved_green = try waitForCaptureBounds(&h, capture_path, pdf_green, 500, 5000);
+ if (moved_red.max_y >= fit_width.max_y or moved_green.min_y >= initial_green.min_y)
+ return error.SdlPdfContinuousStepDidNotMove;
+ try expectThemeGap(capture_path, moved_red, moved_green);
+
+ // Fit-height fills/crops the full body height. These are GPU readback
+ // extents, so this exercises SDL's UV crop and destination rectangle
+ // rather than merely observing the page color.
_ = libc.unlink(capture_path);
try h.send(" tz");
const fit_height = try waitForCaptureBounds(&h, capture_path, pdf_red, 500, 5000);
@@ -486,19 +444,28 @@ fn runGuiPdf(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8) !v
fit_width.width() - fit_height.width()
else
fit_height.width() - fit_width.width();
- if (fit_height.height() < fit_width.height() * 2 or width_delta > 8 or
- fit_height.min_y >= fit_width.min_y)
- {
+ if (fit_height.height() < fit_width.height() * 2 or width_delta > 8) {
std.debug.print("SDL PDF fit extents did not change as expected: width={any}, height={any}\n", .{ fit_width, fit_height });
return error.SdlPdfFitGeometryWrong;
}
- // Remove the old capture before the key so merely rereading page one's
- // file cannot satisfy page two. The app replaces this path atomically.
+ // In height fit one row exposes page two at the bottom but keeps page one
+ // active. Enough additional rows then move page one wholly offscreen and
+ // leave page two at the top, proving continuous traversal in GPU output.
_ = libc.unlink(capture_path);
try h.send("j");
+ const height_red = try waitForCaptureBounds(&h, capture_path, pdf_red, 500, 5000);
+ const height_green = try waitForCaptureBounds(&h, capture_path, pdf_green, 200, 5000);
+ try expectThemeGap(capture_path, height_red, height_green);
+
+ _ = libc.unlink(capture_path);
+ try h.send("jjjjjjjj");
const green = try waitForCaptureBounds(&h, capture_path, pdf_green, 500, 5000);
- std.debug.print("PDF harness ok: SDL fit geometry + red->green ({d}->{d} pixels)\n", .{ fit_width.count, green.count });
+ const final_capture = try readFile(gpa, capture_path);
+ defer gpa.free(final_capture);
+ const final_pixels = ppmPixels(final_capture) orelse return error.BadCapture;
+ if (countColor(final_pixels, pdf_red) != 0) return error.SdlPdfOffscreenPageStillDrawn;
+ std.debug.print("PDF harness ok: SDL continuous strip + theme gap + red->green ({d}->{d} pixels)\n", .{ fit_width.count, green.count });
}
const ColorBounds = struct {
@@ -542,6 +509,27 @@ fn waitForCaptureBounds(
return error.PdfPageColorMissing;
}
+fn expectThemeGap(path: [:0]const u8, upper: ColorBounds, lower: ColorBounds) !void {
+ if (lower.min_y <= upper.max_y + 1) return error.SdlPdfGapMissing;
+ const gap = lower.min_y - upper.max_y - 1;
+ if (gap < 7 or gap > 9) return error.SdlPdfGapWrongSize;
+ const capture = try readFile(gpa, path);
+ defer gpa.free(capture);
+ const ppm = ppmView(capture) orelse return error.BadCapture;
+ const y = upper.max_y + 1 + gap / 2;
+ const x0 = upper.min_x + 2;
+ const x1 = @min(upper.max_x, lower.max_x) -| 2;
+ if (x0 > x1 or y >= ppm.height) return error.SdlPdfGapOutOfBounds;
+ const first = ppm.pixels[(y * ppm.width + x0) * 3 ..][0..3];
+ if (std.mem.eql(u8, first, &pdf_red) or std.mem.eql(u8, first, &pdf_green))
+ return error.SdlPdfGapCovered;
+ var x = x0 + 1;
+ while (x <= x1) : (x += 1) {
+ const pixel = ppm.pixels[(y * ppm.width + x) * 3 ..][0..3];
+ if (!std.mem.eql(u8, pixel, first)) return error.SdlPdfGapNotThemeBackground;
+ }
+}
+
const PpmView = struct { width: usize, height: usize, pixels: []const u8 };
fn colorBounds(ppm: PpmView, color: [3]u8) ?ColorBounds {