Commit 71c82393eb

Ryan Liptak <squeek502@hotmail.com>
2022-12-08 07:50:32
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.
1 parent 6966fb8
Changed files (1)
lib
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).