From fb8d065bd12a0abfcb40298a4e31d25060bee000 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Wed, 7 Dec 2022 22:50:32 -0800 Subject: [PATCH] std.testing: Fully absorb expectEqualBytes into expectEqualSlices - In #13720, expectEqualBytes was added as a standalone function - In #13723, expectEqualSlices was made to use expectEqualBytes when the type was u8 - In this commit, expectEqualSlices has fully absorbed expectEqualBytes, and expectEqualBytes itself has been removed For non-`u8` types, expectEqualSlices will now work similarly to expectEqualBytes (highlighting diffs in red), but will use a full line for each index and therefore will only print a maximum of 16 indexes. --- lib/std/testing.zig | 309 +++++++++++++++++++++++++------------------- 1 file changed, 176 insertions(+), 133 deletions(-) diff --git a/lib/std/testing.zig b/lib/std/testing.zig index 1a7d3fd73210..6882a19f1da3 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -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, @@ -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).