Skip to content

std.testing: Add expectEqualBytes that outputs hexdumps with diffs highlighted in red #13720

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 30, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions lib/std/testing.zig
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ test "expectApproxEqRel" {
/// equal, prints diagnostics to stderr to show exactly how they are not equal,
/// then returns a test failure error.
/// If your inputs are UTF-8 encoded strings, consider calling `expectEqualStrings` instead.
/// If your inputs are slices of bytes, consider calling `expectEqualBytes` instead.
pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const T) !void {
// TODO better printing of the difference
// If the arrays are small enough we could print the whole thing
Expand Down Expand Up @@ -550,6 +551,89 @@ test {
try expectEqualStrings("foo", "foo");
}

/// This function is intended to be used only in tests. When the two slices are not
/// equal, prints hexdumps of the inputs with the differences highlighted in red to stderr,
/// then returns a test failure error. The colorized output is optional and controlled
/// by the return of `std.debug.detectTTYConfig()`.
pub fn expectEqualBytes(expected: []const u8, actual: []const u8) !void {
std.testing.expectEqualSlices(u8, expected, actual) catch |err| {
var differ = BytesDiffer{
.expected = expected,
.actual = actual,
.ttyconf = std.debug.detectTTYConfig(),
};
const stderr = std.io.getStdErr();

std.debug.print("\n============ expected this output: =============\n\n", .{});
differ.write(stderr.writer()) catch {};

// now reverse expected/actual and print again
differ.expected = actual;
differ.actual = expected;
std.debug.print("\n============= instead found this: ==============\n\n", .{});
differ.write(stderr.writer()) catch {};
std.debug.print("\n================================================\n\n", .{});

return err;
};
}

const BytesDiffer = struct {
expected: []const u8,
actual: []const u8,
ttyconf: std.debug.TTY.Config,

pub fn write(self: BytesDiffer, writer: anytype) !void {
var expected_iterator = ChunkIterator{ .bytes = self.expected };
while (expected_iterator.next()) |chunk| {
// to avoid having to calculate diffs twice per chunk
var diffs: std.bit_set.IntegerBitSet(16) = .{ .mask = 0 };
for (chunk) |byte, i| {
var absolute_byte_index = (expected_iterator.index - chunk.len) + i;
const diff = if (absolute_byte_index < self.actual.len) self.actual[absolute_byte_index] != byte else true;
if (diff) diffs.set(i);
try self.writeByteDiff(writer, "{X:0>2} ", byte, diff);
if (i == 7) try writer.writeByte(' ');
}
try writer.writeByte(' ');
if (chunk.len < 16) {
var missing_columns = (16 - chunk.len) * 3;
if (chunk.len < 8) missing_columns += 1;
try writer.writeByteNTimes(' ', missing_columns);
}
for (chunk) |byte, i| {
const byte_to_print = if (std.ascii.isPrint(byte)) byte else '.';
try self.writeByteDiff(writer, "{c}", byte_to_print, diffs.isSet(i));
}
try writer.writeByte('\n');
}
}

fn writeByteDiff(self: BytesDiffer, writer: anytype, comptime fmt: []const u8, byte: u8, diff: bool) !void {
if (diff) self.ttyconf.setColor(writer, .Red);
try writer.print(fmt, .{byte});
if (diff) self.ttyconf.setColor(writer, .Reset);
}

const ChunkIterator = struct {
bytes: []const u8,
index: usize = 0,

pub fn next(self: *ChunkIterator) ?[]const u8 {
if (self.index == self.bytes.len) return null;

const start_index = self.index;
const end_index = @min(self.bytes.len, start_index + 16);
self.index = end_index;
return self.bytes[start_index..end_index];
}
};
};

test {
try expectEqualBytes("foo\x00", "foo\x00");
}

/// Exhaustively check that allocation failures within `test_fn` are handled without
/// introducing memory leaks. If used with the `testing.allocator` as the `backing_allocator`,
/// it will also be able to detect double frees, etc (when runtime safety is enabled).
Expand Down