terminal: add page.verifyIntegrity function

This commit is contained in:
Mitchell Hashimoto
2024-03-21 16:31:18 -07:00
parent 8142eb9678
commit 1be06e8f3f

View File

@ -1,6 +1,7 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const assert = std.debug.assert;
const testing = std.testing;
const fastmem = @import("../fastmem.zig");
@ -17,6 +18,8 @@ const AutoOffsetHashMap = hash_map.AutoOffsetHashMap;
const alignForward = std.mem.alignForward;
const alignBackward = std.mem.alignBackward;
const log = std.log.scoped(.page);
/// The allocator to use for multi-codepoint grapheme data. We use
/// a chunk size of 4 codepoints. It'd be best to set this empirically
/// but it is currently set based on vibes. My thinking around 4 codepoints
@ -171,6 +174,138 @@ pub const Page = struct {
self.* = undefined;
}
pub const IntegrityError = error{
UnmarkedGraphemeRow,
MarkedGraphemeRow,
MissingGraphemeData,
InvalidGraphemeCount,
MissingStyle,
UnmarkedStyleRow,
MismatchedStyleRef,
InvalidStyleCount,
};
/// Verifies the integrity of the page data. This is not fast,
/// but it is useful for assertions, deserialization, etc. The
/// allocator is only used for temporary allocations -- all memory
/// is freed before this function returns.
///
/// Integrity errors are also logged as warnings.
pub fn verifyIntegrity(self: *Page, alloc_gpa: Allocator) !void {
var arena = ArenaAllocator.init(alloc_gpa);
defer arena.deinit();
const alloc = arena.allocator();
var graphemes_seen: usize = 0;
var styles_seen = std.AutoHashMap(style.Id, usize).init(alloc);
defer styles_seen.deinit();
const rows = self.rows.ptr(self.memory)[0..self.size.rows];
for (rows, 0..) |*row, y| {
const graphemes_start = graphemes_seen;
const cells = row.cells.ptr(self.memory)[0..self.size.cols];
for (cells, 0..) |*cell, x| {
if (cell.hasGrapheme()) {
// If a cell has grapheme data, it must be present in
// the grapheme map.
_ = self.lookupGrapheme(cell) orelse {
log.warn(
"page integrity violation y={} x={} grapheme data missing",
.{ y, x },
);
return IntegrityError.MissingGraphemeData;
};
graphemes_seen += 1;
}
if (cell.style_id != style.default_id) {
// If a cell has a style, it must be present in the styles
// set.
_ = self.styles.lookupId(
self.memory,
cell.style_id,
) orelse {
log.warn(
"page integrity violation y={} x={} style missing id={}",
.{ y, x, cell.style_id },
);
return IntegrityError.MissingStyle;
};
if (!row.styled) {
log.warn(
"page integrity violation y={} x={} row not marked as styled",
.{ y, x },
);
return IntegrityError.UnmarkedStyleRow;
}
const gop = try styles_seen.getOrPut(cell.style_id);
if (!gop.found_existing) gop.value_ptr.* = 0;
gop.value_ptr.* += 1;
}
}
// Check row grapheme data
if (graphemes_seen > graphemes_start) {
// If a cell in a row has grapheme data, the row must
// be marked as having grapheme data.
if (!row.grapheme) {
log.warn(
"page integrity violation y={} grapheme data but row not marked",
.{y},
);
return IntegrityError.UnmarkedGraphemeRow;
}
} else {
// If no cells in a row have grapheme data, the row must
// not be marked as having grapheme data.
if (row.grapheme) {
log.warn(
"page integrity violation y={} row marked but no grapheme data",
.{y},
);
return IntegrityError.MarkedGraphemeRow;
}
}
}
// Our graphemes seen should exactly match the grapheme count
if (graphemes_seen != self.graphemeCount()) {
log.warn(
"page integrity violation grapheme count mismatch expected={} actual={}",
.{ graphemes_seen, self.graphemeCount() },
);
return IntegrityError.InvalidGraphemeCount;
}
// Our unique styles seen should exactly match the style count.
if (styles_seen.count() != self.styles.count(self.memory)) {
log.warn(
"page integrity violation style count mismatch expected={} actual={}",
.{ styles_seen.count(), self.styles.count(self.memory) },
);
return IntegrityError.InvalidStyleCount;
}
// Verify all our styles have the correct ref count.
{
var it = styles_seen.iterator();
while (it.next()) |entry| {
const style_val = self.styles.lookupId(self.memory, entry.key_ptr.*).?.*;
const md = self.styles.upsert(self.memory, style_val) catch unreachable;
if (md.ref != entry.value_ptr.*) {
log.warn(
"page integrity violation style ref count mismatch id={} expected={} actual={}",
.{ entry.key_ptr.*, entry.value_ptr.*, md.ref },
);
return IntegrityError.MismatchedStyleRef;
}
}
}
}
/// Clone the contents of this page. This will allocate new memory
/// using the page allocator. If you want to manage memory manually,
/// use cloneBuf.
@ -1444,3 +1579,174 @@ test "Page moveCells graphemes" {
);
}
}
test "Page verifyIntegrity graphemes good" {
var page = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page.deinit();
// Write
for (0..page.capacity.cols) |x| {
const rac = page.getRowAndCell(x, 0);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = @intCast(x + 1) },
};
try page.appendGrapheme(rac.row, rac.cell, 0x0A);
}
try page.verifyIntegrity(testing.allocator);
}
test "Page verifyIntegrity grapheme row not marked" {
var page = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page.deinit();
// Write
for (0..page.capacity.cols) |x| {
const rac = page.getRowAndCell(x, 0);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = @intCast(x + 1) },
};
try page.appendGrapheme(rac.row, rac.cell, 0x0A);
}
// Make invalid by unmarking the row
page.getRow(0).grapheme = false;
try testing.expectError(
Page.IntegrityError.UnmarkedGraphemeRow,
page.verifyIntegrity(testing.allocator),
);
}
test "Page verifyIntegrity text row marked as grapheme" {
var page = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page.deinit();
// Write
for (0..page.capacity.cols) |x| {
const rac = page.getRowAndCell(x, 0);
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = @intCast(x + 1) },
};
}
// Make invalid by unmarking the row
page.getRow(0).grapheme = true;
try testing.expectError(
Page.IntegrityError.MarkedGraphemeRow,
page.verifyIntegrity(testing.allocator),
);
}
test "Page verifyIntegrity styles good" {
var page = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page.deinit();
// Upsert a style we'll use
const md = try page.styles.upsert(page.memory, .{ .flags = .{
.bold = true,
} });
// Write
for (0..page.capacity.cols) |x| {
const rac = page.getRowAndCell(x, 0);
rac.row.styled = true;
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = @intCast(x + 1) },
.style_id = md.id,
};
md.ref += 1;
}
try page.verifyIntegrity(testing.allocator);
}
test "Page verifyIntegrity styles ref count mismatch" {
var page = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page.deinit();
// Upsert a style we'll use
const md = try page.styles.upsert(page.memory, .{ .flags = .{
.bold = true,
} });
// Write
for (0..page.capacity.cols) |x| {
const rac = page.getRowAndCell(x, 0);
rac.row.styled = true;
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = @intCast(x + 1) },
.style_id = md.id,
};
md.ref += 1;
}
// Miss a ref
md.ref -= 1;
try testing.expectError(
Page.IntegrityError.MismatchedStyleRef,
page.verifyIntegrity(testing.allocator),
);
}
test "Page verifyIntegrity styles extra" {
var page = try Page.init(.{
.cols = 10,
.rows = 10,
.styles = 8,
});
defer page.deinit();
// Upsert a style we'll use
const md = try page.styles.upsert(page.memory, .{ .flags = .{
.bold = true,
} });
_ = try page.styles.upsert(page.memory, .{ .flags = .{
.italic = true,
} });
// Write
for (0..page.capacity.cols) |x| {
const rac = page.getRowAndCell(x, 0);
rac.row.styled = true;
rac.cell.* = .{
.content_tag = .codepoint,
.content = .{ .codepoint = @intCast(x + 1) },
.style_id = md.id,
};
md.ref += 1;
}
try testing.expectError(
Page.IntegrityError.InvalidStyleCount,
page.verifyIntegrity(testing.allocator),
);
}