Add render diagnostics guardrails
This commit is contained in:
+185
@@ -31,6 +31,26 @@ pub const Viewport = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const RenderDiagnostics = struct {
|
||||
viewport_width: usize,
|
||||
viewport_height: usize,
|
||||
frame_bytes: usize,
|
||||
frame_lines: usize,
|
||||
max_line_cells: usize,
|
||||
clipped_sources: usize,
|
||||
};
|
||||
|
||||
pub const RenderResult = struct {
|
||||
frame: []u8,
|
||||
diagnostics: RenderDiagnostics,
|
||||
warning: []u8,
|
||||
|
||||
pub fn deinit(self: RenderResult, allocator: std.mem.Allocator) void {
|
||||
allocator.free(self.frame);
|
||||
allocator.free(self.warning);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn runTrace(allocator: std.mem.Allocator, viewport: Viewport, trace: []const u8) !TraceResult {
|
||||
var client = try Client.init(allocator, viewport);
|
||||
defer client.deinit();
|
||||
@@ -127,6 +147,15 @@ pub const Client = struct {
|
||||
return self.renderEditor(allocator, snap);
|
||||
}
|
||||
|
||||
pub fn renderWithDiagnostics(self: *const Client, allocator: std.mem.Allocator) !RenderResult {
|
||||
const frame = try self.render(allocator);
|
||||
errdefer allocator.free(frame);
|
||||
var diagnostics = diagnoseFrame(self.viewport, frame);
|
||||
diagnostics.clipped_sources = try self.countClippedSources(allocator);
|
||||
const warning = try renderWarningAlloc(allocator, diagnostics);
|
||||
return .{ .frame = frame, .diagnostics = diagnostics, .warning = warning };
|
||||
}
|
||||
|
||||
fn renderEditor(self: *const Client, allocator: std.mem.Allocator, snap: session_mod.Snapshot) ![]u8 {
|
||||
var out = std.ArrayList(u8).empty;
|
||||
errdefer out.deinit(allocator);
|
||||
@@ -334,6 +363,51 @@ pub const Client = struct {
|
||||
self.saved_bytes = copy;
|
||||
self.message = "saved";
|
||||
}
|
||||
|
||||
fn countClippedSources(self: *const Client, allocator: std.mem.Allocator) !usize {
|
||||
const snap = try self.session.snapshot();
|
||||
var clipped: usize = 0;
|
||||
if (snap.active_panel_title == null) {
|
||||
var line_iter = std.mem.splitScalar(u8, snap.bytes, '\n');
|
||||
while (line_iter.next()) |line| {
|
||||
if (session_mod.cellWidth(line) > self.viewport.width) clipped += 1;
|
||||
}
|
||||
if (self.message) |message| {
|
||||
if (session_mod.cellWidth(message) > self.viewport.width) clipped += 1;
|
||||
}
|
||||
return clipped;
|
||||
}
|
||||
|
||||
const path = try self.session.panelPathAlloc(allocator);
|
||||
defer allocator.free(path);
|
||||
const header = try std.fmt.allocPrint(allocator, "panel {s}", .{path});
|
||||
defer allocator.free(header);
|
||||
if (session_mod.cellWidth(header) > self.viewport.width) clipped += 1;
|
||||
|
||||
const remaining_rows = self.viewport.height - 1;
|
||||
const rows = self.session.activeListRowsAlloc(allocator, remaining_rows) catch null;
|
||||
if (rows) |list_rows| {
|
||||
defer {
|
||||
for (list_rows) |row| allocator.free(row);
|
||||
allocator.free(list_rows);
|
||||
}
|
||||
for (list_rows) |row| {
|
||||
if (session_mod.cellWidth(row) > self.viewport.width) clipped += 1;
|
||||
}
|
||||
} else if (snap.active_panel_title) |title| {
|
||||
const detail = try std.fmt.allocPrint(allocator, "{s}: no content yet", .{title});
|
||||
defer allocator.free(detail);
|
||||
if (session_mod.cellWidth(detail) > self.viewport.width) clipped += 1;
|
||||
}
|
||||
|
||||
const status = if (self.message) |message|
|
||||
try std.fmt.allocPrint(allocator, "{s}", .{message})
|
||||
else
|
||||
try std.fmt.allocPrint(allocator, "panel {d}/{d} x close", .{ snap.active_panel_index.? + 1, snap.panel_depth });
|
||||
defer allocator.free(status);
|
||||
if (session_mod.cellWidth(status) > self.viewport.width) clipped += 1;
|
||||
return clipped;
|
||||
}
|
||||
};
|
||||
|
||||
fn appendVisibleCells(allocator: std.mem.Allocator, out: *std.ArrayList(u8), bytes: []const u8, max_cells: usize) !void {
|
||||
@@ -369,6 +443,52 @@ fn columnAt(bytes: []const u8, cursor_byte: usize) usize {
|
||||
return session_mod.cellWidth(prefix[line_start..]);
|
||||
}
|
||||
|
||||
fn diagnoseFrame(viewport: Viewport, frame: []const u8) RenderDiagnostics {
|
||||
var diagnostics = RenderDiagnostics{
|
||||
.viewport_width = viewport.width,
|
||||
.viewport_height = viewport.height,
|
||||
.frame_bytes = frame.len,
|
||||
.frame_lines = 0,
|
||||
.max_line_cells = 0,
|
||||
.clipped_sources = 0,
|
||||
};
|
||||
var lines = std.mem.splitScalar(u8, frame, '\n');
|
||||
while (lines.next()) |line| {
|
||||
diagnostics.frame_lines += 1;
|
||||
diagnostics.max_line_cells = @max(diagnostics.max_line_cells, session_mod.cellWidth(line));
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
fn renderWarningAlloc(allocator: std.mem.Allocator, diagnostics: RenderDiagnostics) ![]u8 {
|
||||
if (diagnostics.frame_lines != diagnostics.viewport_height) {
|
||||
return std.fmt.allocPrint(
|
||||
allocator,
|
||||
"render warning: expected {d} lines, rendered {d}",
|
||||
.{ diagnostics.viewport_height, diagnostics.frame_lines },
|
||||
);
|
||||
}
|
||||
if (diagnostics.max_line_cells > diagnostics.viewport_width) {
|
||||
return std.fmt.allocPrint(
|
||||
allocator,
|
||||
"render warning: line width {d} exceeds viewport {d}",
|
||||
.{ diagnostics.max_line_cells, diagnostics.viewport_width },
|
||||
);
|
||||
}
|
||||
if (diagnostics.clipped_sources > 0) {
|
||||
return std.fmt.allocPrint(
|
||||
allocator,
|
||||
"render warning: clipped {d} source line(s) to fit {d} columns",
|
||||
.{ diagnostics.clipped_sources, diagnostics.viewport_width },
|
||||
);
|
||||
}
|
||||
return std.fmt.allocPrint(
|
||||
allocator,
|
||||
"render ok: {d} lines, {d} bytes, max {d}/{d} cells",
|
||||
.{ diagnostics.frame_lines, diagnostics.frame_bytes, diagnostics.max_line_cells, diagnostics.viewport_width },
|
||||
);
|
||||
}
|
||||
|
||||
fn assertLinesFit(frame: []const u8, width: usize) !void {
|
||||
var lines = std.mem.splitScalar(u8, frame, '\n');
|
||||
while (lines.next()) |line| {
|
||||
@@ -737,3 +857,68 @@ test "adversarial: long feedback messages are clipped to viewport width" {
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "this-message-is-") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "phone-width") == null);
|
||||
}
|
||||
|
||||
test "regular: render diagnostics report ok for representative narrow editor frame" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 32, .height = 5 });
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("open fn main() {}");
|
||||
try client.handleTraceLine("key space");
|
||||
try client.handleTraceLine("key p");
|
||||
try client.handleTraceLine("key c");
|
||||
try client.handleTraceLine("type x");
|
||||
const result = try client.renderWithDiagnostics(std.testing.allocator);
|
||||
defer result.deinit(std.testing.allocator);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 5), result.diagnostics.frame_lines);
|
||||
try std.testing.expect(result.diagnostics.frame_bytes <= 32 * 5 * 4);
|
||||
try std.testing.expect(result.diagnostics.max_line_cells <= 32);
|
||||
try std.testing.expectEqual(@as(usize, 0), result.diagnostics.clipped_sources);
|
||||
try std.testing.expect(std.mem.indexOf(u8, result.warning, "render ok") != null);
|
||||
}
|
||||
|
||||
test "regular: render diagnostics report ok for representative list panel frame" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 36, .height = 5 });
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("list_open files src/main.zig|src/panel.zig|README.md");
|
||||
try client.handleTraceLine("list_filter src");
|
||||
try client.handleTraceLine("list_down");
|
||||
const result = try client.renderWithDiagnostics(std.testing.allocator);
|
||||
defer result.deinit(std.testing.allocator);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 5), result.diagnostics.frame_lines);
|
||||
try std.testing.expect(result.diagnostics.frame_bytes <= 36 * 5 * 4);
|
||||
try std.testing.expect(result.diagnostics.max_line_cells <= 36);
|
||||
try std.testing.expectEqual(@as(usize, 0), result.diagnostics.clipped_sources);
|
||||
try std.testing.expect(std.mem.indexOf(u8, result.warning, "render ok") != null);
|
||||
}
|
||||
|
||||
test "adversarial: render diagnostics name clipped editor sources" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 12, .height = 4 });
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("open this-line-is-far-too-long-for-phone");
|
||||
const result = try client.renderWithDiagnostics(std.testing.allocator);
|
||||
defer result.deinit(std.testing.allocator);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 4), result.diagnostics.frame_lines);
|
||||
try std.testing.expect(result.diagnostics.max_line_cells <= 12);
|
||||
try std.testing.expect(result.diagnostics.clipped_sources >= 1);
|
||||
try std.testing.expect(std.mem.indexOf(u8, result.warning, "clipped") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, result.warning, "12 columns") != null);
|
||||
}
|
||||
|
||||
test "adversarial: render diagnostics name clipped panel sources" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 14, .height = 4 });
|
||||
defer client.deinit();
|
||||
|
||||
try client.handleTraceLine("list_open files src/very-long-mobile-render-file-name.zig|b.zig");
|
||||
const result = try client.renderWithDiagnostics(std.testing.allocator);
|
||||
defer result.deinit(std.testing.allocator);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 4), result.diagnostics.frame_lines);
|
||||
try std.testing.expect(result.diagnostics.max_line_cells <= 14);
|
||||
try std.testing.expect(result.diagnostics.clipped_sources >= 1);
|
||||
try std.testing.expect(std.mem.indexOf(u8, result.warning, "clipped") != null);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user