Skip to content

std.testing: Fully absorb expectEqualBytes into expectEqualSlices #13816

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
Dec 9, 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
309 changes: 176 additions & 133 deletions lib/std/testing.zig
Original file line number Diff line number Diff line change
Expand Up @@ -278,30 +278,188 @@ test "expectApproxEqRel" {
}

/// This function is intended to be used only in tests. When the two slices are not
/// equal, prints diagnostics to stderr to show exactly how they are not equal,
/// then returns a test failure error.
/// equal, prints diagnostics to stderr to show exactly how they are not equal (with
/// the differences highlighted in red), then returns a test failure error.
/// The colorized output is optional and controlled by the return of `std.debug.detectTTYConfig()`.
/// If your inputs are UTF-8 encoded strings, consider calling `expectEqualStrings` instead.
/// If your inputs are slices of bytes, consider calling `expectEqualBytes` instead (this
/// function calls `expectEqualBytes` implicitly when `T` is `u8`).
pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const T) !void {
if (T == u8) {
return expectEqualBytes(expected, actual);
if (expected.ptr == actual.ptr and expected.len == actual.len) {
return;
}
// TODO better printing of the difference
// If the arrays are small enough we could print the whole thing
// If the child type is u8 and no weird bytes, we could print it as strings
// Even for the length difference, it would be useful to see the values of the slices probably.
if (expected.len != actual.len) {
std.debug.print("slice lengths differ. expected {d}, found {d}\n", .{ expected.len, actual.len });
return error.TestExpectedEqual;
const diff_index: usize = diff_index: {
const shortest = @min(expected.len, actual.len);
var index: usize = 0;
while (index < shortest) : (index += 1) {
if (!std.meta.eql(actual[index], expected[index])) break :diff_index index;
}
break :diff_index if (expected.len == actual.len) return else shortest;
};

std.debug.print("slices differ. first difference occurs at index {d} (0x{X})\n", .{ diff_index, diff_index });

// TODO: Should this be configurable by the caller?
const max_lines: usize = 16;
const max_window_size: usize = if (T == u8) max_lines * 16 else max_lines;

// Print a maximum of max_window_size items of each input, starting just before the
// first difference to give a bit of context.
var window_start: usize = 0;
if (@max(actual.len, expected.len) > max_window_size) {
const alignment = if (T == u8) 16 else 2;
window_start = std.mem.alignBackward(diff_index - @min(diff_index, alignment), alignment);
}
var i: usize = 0;
while (i < expected.len) : (i += 1) {
if (!std.meta.eql(expected[i], actual[i])) {
std.debug.print("index {} incorrect. expected {any}, found {any}\n", .{ i, expected[i], actual[i] });
return error.TestExpectedEqual;
const expected_window = expected[window_start..@min(expected.len, window_start + max_window_size)];
const expected_truncated = window_start + expected_window.len < expected.len;
const actual_window = actual[window_start..@min(actual.len, window_start + max_window_size)];
const actual_truncated = window_start + actual_window.len < actual.len;

const ttyconf = std.debug.detectTTYConfig();
var differ = if (T == u8) BytesDiffer{
.expected = expected_window,
.actual = actual_window,
.ttyconf = ttyconf,
} else SliceDiffer(T){
.start_index = window_start,
.expected = expected_window,
.actual = actual_window,
.ttyconf = ttyconf,
};
const stderr = std.io.getStdErr();

// Print indexes as hex for slices of u8 since it's more likely to be binary data where
// that is usually useful.
const index_fmt = if (T == u8) "0x{X}" else "{}";

std.debug.print("\n============ expected this output: ============= len: {} (0x{X})\n\n", .{ expected.len, expected.len });
if (window_start > 0) {
if (T == u8) {
std.debug.print("... truncated, start index: " ++ index_fmt ++ " ...\n", .{window_start});
} else {
std.debug.print("... truncated ...\n", .{});
}
}
differ.write(stderr.writer()) catch {};
if (expected_truncated) {
const end_offset = window_start + expected_window.len;
const num_missing_items = expected.len - (window_start + expected_window.len);
if (T == u8) {
std.debug.print("... truncated, indexes [" ++ index_fmt ++ "..] not shown, remaining bytes: " ++ index_fmt ++ " ...\n", .{ end_offset, num_missing_items });
} else {
std.debug.print("... truncated, remaining items: " ++ index_fmt ++ " ...\n", .{num_missing_items});
}
}

// now reverse expected/actual and print again
differ.expected = actual_window;
differ.actual = expected_window;
std.debug.print("\n============= instead found this: ============== len: {} (0x{X})\n\n", .{ actual.len, actual.len });
if (window_start > 0) {
if (T == u8) {
std.debug.print("... truncated, start index: " ++ index_fmt ++ " ...\n", .{window_start});
} else {
std.debug.print("... truncated ...\n", .{});
}
}
differ.write(stderr.writer()) catch {};
if (actual_truncated) {
const end_offset = window_start + actual_window.len;
const num_missing_items = actual.len - (window_start + actual_window.len);
if (T == u8) {
std.debug.print("... truncated, indexes [" ++ index_fmt ++ "..] not shown, remaining bytes: " ++ index_fmt ++ " ...\n", .{ end_offset, num_missing_items });
} else {
std.debug.print("... truncated, remaining items: " ++ index_fmt ++ " ...\n", .{num_missing_items});
}
}
std.debug.print("\n================================================\n\n", .{});

return error.TestExpectedEqual;
}

fn SliceDiffer(comptime T: type) type {
return struct {
start_index: usize,
expected: []const T,
actual: []const T,
ttyconf: std.debug.TTY.Config,

const Self = @This();

pub fn write(self: Self, writer: anytype) !void {
for (self.expected) |value, i| {
var full_index = self.start_index + i;
const diff = if (i < self.actual.len) !std.meta.eql(self.actual[i], value) else true;
if (diff) self.ttyconf.setColor(writer, .Red);
try writer.print("[{}]: {any}\n", .{ full_index, value });
if (diff) self.ttyconf.setColor(writer, .Reset);
}
}
};
}

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 expectEqualSlices(u8, "foo\x00", "foo\x00");
try expectEqualSlices(u16, &[_]u16{ 100, 200, 300, 400 }, &[_]u16{ 100, 200, 300, 400 });
const E = enum { foo, bar };
const S = struct {
v: E,
};
try expectEqualSlices(
S,
&[_]S{ .{ .v = .foo }, .{ .v = .bar }, .{ .v = .foo }, .{ .v = .bar } },
&[_]S{ .{ .v = .foo }, .{ .v = .bar }, .{ .v = .foo }, .{ .v = .bar } },
);
}

/// This function is intended to be used only in tests. Checks that two slices or two arrays are equal,
Expand Down Expand Up @@ -555,121 +713,6 @@ 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 {
if (std.mem.indexOfDiff(u8, actual, expected)) |diff_index| {
std.debug.print("byte slices differ. first difference occurs at offset {d} (0x{X})\n", .{ diff_index, diff_index });

// TODO: Should this be configurable by the caller?
const max_window_size: usize = 256;

// Print a maximum of max_window_size bytes of each input, starting just before the
// first difference.
var window_start: usize = 0;
if (@max(actual.len, expected.len) > max_window_size) {
window_start = std.mem.alignBackward(diff_index - @min(diff_index, 16), 16);
}
const expected_window = expected[window_start..@min(expected.len, window_start + max_window_size)];
const expected_truncated = window_start + expected_window.len < expected.len;
const actual_window = actual[window_start..@min(actual.len, window_start + max_window_size)];
const actual_truncated = window_start + actual_window.len < actual.len;

var differ = BytesDiffer{
.expected = expected_window,
.actual = actual_window,
.ttyconf = std.debug.detectTTYConfig(),
};
const stderr = std.io.getStdErr();

std.debug.print("\n============ expected this output: ============= len: {} (0x{X})\n\n", .{ expected.len, expected.len });
if (window_start > 0) {
std.debug.print("... truncated, start offset: 0x{X} ...\n", .{window_start});
}
differ.write(stderr.writer()) catch {};
if (expected_truncated) {
const end_offset = window_start + expected_window.len;
const num_missing_bytes = expected.len - (window_start + expected_window.len);
std.debug.print("... truncated, end offset: 0x{X}, remaining bytes: 0x{X} ...\n", .{ end_offset, num_missing_bytes });
}

// now reverse expected/actual and print again
differ.expected = actual_window;
differ.actual = expected_window;
std.debug.print("\n============= instead found this: ============== len: {} (0x{X})\n\n", .{ actual.len, actual.len });
if (window_start > 0) {
std.debug.print("... truncated, start offset: 0x{X} ...\n", .{window_start});
}
differ.write(stderr.writer()) catch {};
if (actual_truncated) {
const end_offset = window_start + actual_window.len;
const num_missing_bytes = actual.len - (window_start + actual_window.len);
std.debug.print("... truncated, end offset: 0x{X}, remaining bytes: 0x{X} ...\n", .{ end_offset, num_missing_bytes });
}
std.debug.print("\n================================================\n\n", .{});

return error.TestExpectedEqual;
}
}

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