Skip to content

Commit db616b4

Browse files
committed
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 225ed65 commit db616b4

File tree

1 file changed

+176
-133
lines changed

1 file changed

+176
-133
lines changed

lib/std/testing.zig

Lines changed: 176 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -278,30 +278,188 @@ test "expectApproxEqRel" {
278278
}
279279

280280
/// This function is intended to be used only in tests. When the two slices are not
281-
/// equal, prints diagnostics to stderr to show exactly how they are not equal,
282-
/// then returns a test failure error.
281+
/// equal, prints diagnostics to stderr to show exactly how they are not equal (with
282+
/// the differences highlighted in red), then returns a test failure error.
283+
/// The colorized output is optional and controlled by the return of `std.debug.detectTTYConfig()`.
283284
/// If your inputs are UTF-8 encoded strings, consider calling `expectEqualStrings` instead.
284-
/// If your inputs are slices of bytes, consider calling `expectEqualBytes` instead (this
285-
/// function calls `expectEqualBytes` implicitly when `T` is `u8`).
286285
pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const T) !void {
287-
if (T == u8) {
288-
return expectEqualBytes(expected, actual);
286+
if (expected.ptr == actual.ptr and expected.len == actual.len) {
287+
return;
289288
}
290-
// TODO better printing of the difference
291-
// If the arrays are small enough we could print the whole thing
292-
// If the child type is u8 and no weird bytes, we could print it as strings
293-
// Even for the length difference, it would be useful to see the values of the slices probably.
294-
if (expected.len != actual.len) {
295-
std.debug.print("slice lengths differ. expected {d}, found {d}\n", .{ expected.len, actual.len });
296-
return error.TestExpectedEqual;
289+
const diff_index: usize = diff_index: {
290+
const shortest = @min(expected.len, actual.len);
291+
var index: usize = 0;
292+
while (index < shortest) : (index += 1) {
293+
if (!std.meta.eql(actual[index], expected[index])) break :diff_index index;
294+
}
295+
break :diff_index if (expected.len == actual.len) return else shortest;
296+
};
297+
298+
std.debug.print("slices differ. first difference occurs at index {d} (0x{X})\n", .{ diff_index, diff_index });
299+
300+
// TODO: Should this be configurable by the caller?
301+
const max_lines: usize = 16;
302+
const max_window_size: usize = if (T == u8) max_lines * 16 else max_lines;
303+
304+
// Print a maximum of max_window_size items of each input, starting just before the
305+
// first difference to give a bit of context.
306+
var window_start: usize = 0;
307+
if (@max(actual.len, expected.len) > max_window_size) {
308+
const alignment = if (T == u8) 16 else 2;
309+
window_start = std.mem.alignBackward(diff_index - @min(diff_index, alignment), alignment);
297310
}
298-
var i: usize = 0;
299-
while (i < expected.len) : (i += 1) {
300-
if (!std.meta.eql(expected[i], actual[i])) {
301-
std.debug.print("index {} incorrect. expected {any}, found {any}\n", .{ i, expected[i], actual[i] });
302-
return error.TestExpectedEqual;
311+
const expected_window = expected[window_start..@min(expected.len, window_start + max_window_size)];
312+
const expected_truncated = window_start + expected_window.len < expected.len;
313+
const actual_window = actual[window_start..@min(actual.len, window_start + max_window_size)];
314+
const actual_truncated = window_start + actual_window.len < actual.len;
315+
316+
const ttyconf = std.debug.detectTTYConfig();
317+
var differ = if (T == u8) BytesDiffer{
318+
.expected = expected_window,
319+
.actual = actual_window,
320+
.ttyconf = ttyconf,
321+
} else SliceDiffer(T){
322+
.start_index = window_start,
323+
.expected = expected_window,
324+
.actual = actual_window,
325+
.ttyconf = ttyconf,
326+
};
327+
const stderr = std.io.getStdErr();
328+
329+
// Print indexes as hex for slices of u8 since it's more likely to be binary data where
330+
// that is usually useful.
331+
const index_fmt = if (T == u8) "0x{X}" else "{}";
332+
333+
std.debug.print("\n============ expected this output: ============= len: {} (0x{X})\n\n", .{ expected.len, expected.len });
334+
if (window_start > 0) {
335+
if (T == u8) {
336+
std.debug.print("... truncated, start index: " ++ index_fmt ++ " ...\n", .{window_start});
337+
} else {
338+
std.debug.print("... truncated ...\n", .{});
339+
}
340+
}
341+
differ.write(stderr.writer()) catch {};
342+
if (expected_truncated) {
343+
const end_offset = window_start + expected_window.len;
344+
const num_missing_items = expected.len - (window_start + expected_window.len);
345+
if (T == u8) {
346+
std.debug.print("... truncated, indexes [" ++ index_fmt ++ "..] not shown, remaining bytes: " ++ index_fmt ++ " ...\n", .{ end_offset, num_missing_items });
347+
} else {
348+
std.debug.print("... truncated, remaining items: " ++ index_fmt ++ " ...\n", .{num_missing_items});
349+
}
350+
}
351+
352+
// now reverse expected/actual and print again
353+
differ.expected = actual_window;
354+
differ.actual = expected_window;
355+
std.debug.print("\n============= instead found this: ============== len: {} (0x{X})\n\n", .{ actual.len, actual.len });
356+
if (window_start > 0) {
357+
if (T == u8) {
358+
std.debug.print("... truncated, start index: " ++ index_fmt ++ " ...\n", .{window_start});
359+
} else {
360+
std.debug.print("... truncated ...\n", .{});
361+
}
362+
}
363+
differ.write(stderr.writer()) catch {};
364+
if (actual_truncated) {
365+
const end_offset = window_start + actual_window.len;
366+
const num_missing_items = actual.len - (window_start + actual_window.len);
367+
if (T == u8) {
368+
std.debug.print("... truncated, indexes [" ++ index_fmt ++ "..] not shown, remaining bytes: " ++ index_fmt ++ " ...\n", .{ end_offset, num_missing_items });
369+
} else {
370+
std.debug.print("... truncated, remaining items: " ++ index_fmt ++ " ...\n", .{num_missing_items});
371+
}
372+
}
373+
std.debug.print("\n================================================\n\n", .{});
374+
375+
return error.TestExpectedEqual;
376+
}
377+
378+
fn SliceDiffer(comptime T: type) type {
379+
return struct {
380+
start_index: usize,
381+
expected: []const T,
382+
actual: []const T,
383+
ttyconf: std.debug.TTY.Config,
384+
385+
const Self = @This();
386+
387+
pub fn write(self: Self, writer: anytype) !void {
388+
for (self.expected) |value, i| {
389+
var full_index = self.start_index + i;
390+
const diff = if (i < self.actual.len) !std.meta.eql(self.actual[i], value) else true;
391+
if (diff) self.ttyconf.setColor(writer, .Red);
392+
try writer.print("[{}]: {any}\n", .{ full_index, value });
393+
if (diff) self.ttyconf.setColor(writer, .Reset);
394+
}
395+
}
396+
};
397+
}
398+
399+
const BytesDiffer = struct {
400+
expected: []const u8,
401+
actual: []const u8,
402+
ttyconf: std.debug.TTY.Config,
403+
404+
pub fn write(self: BytesDiffer, writer: anytype) !void {
405+
var expected_iterator = ChunkIterator{ .bytes = self.expected };
406+
while (expected_iterator.next()) |chunk| {
407+
// to avoid having to calculate diffs twice per chunk
408+
var diffs: std.bit_set.IntegerBitSet(16) = .{ .mask = 0 };
409+
for (chunk) |byte, i| {
410+
var absolute_byte_index = (expected_iterator.index - chunk.len) + i;
411+
const diff = if (absolute_byte_index < self.actual.len) self.actual[absolute_byte_index] != byte else true;
412+
if (diff) diffs.set(i);
413+
try self.writeByteDiff(writer, "{X:0>2} ", byte, diff);
414+
if (i == 7) try writer.writeByte(' ');
415+
}
416+
try writer.writeByte(' ');
417+
if (chunk.len < 16) {
418+
var missing_columns = (16 - chunk.len) * 3;
419+
if (chunk.len < 8) missing_columns += 1;
420+
try writer.writeByteNTimes(' ', missing_columns);
421+
}
422+
for (chunk) |byte, i| {
423+
const byte_to_print = if (std.ascii.isPrint(byte)) byte else '.';
424+
try self.writeByteDiff(writer, "{c}", byte_to_print, diffs.isSet(i));
425+
}
426+
try writer.writeByte('\n');
303427
}
304428
}
429+
430+
fn writeByteDiff(self: BytesDiffer, writer: anytype, comptime fmt: []const u8, byte: u8, diff: bool) !void {
431+
if (diff) self.ttyconf.setColor(writer, .Red);
432+
try writer.print(fmt, .{byte});
433+
if (diff) self.ttyconf.setColor(writer, .Reset);
434+
}
435+
436+
const ChunkIterator = struct {
437+
bytes: []const u8,
438+
index: usize = 0,
439+
440+
pub fn next(self: *ChunkIterator) ?[]const u8 {
441+
if (self.index == self.bytes.len) return null;
442+
443+
const start_index = self.index;
444+
const end_index = @min(self.bytes.len, start_index + 16);
445+
self.index = end_index;
446+
return self.bytes[start_index..end_index];
447+
}
448+
};
449+
};
450+
451+
test {
452+
try expectEqualSlices(u8, "foo\x00", "foo\x00");
453+
try expectEqualSlices(u16, &[_]u16{ 100, 200, 300, 400 }, &[_]u16{ 100, 200, 300, 400 });
454+
const E = enum { foo, bar };
455+
const S = struct {
456+
v: E,
457+
};
458+
try expectEqualSlices(
459+
S,
460+
&[_]S{ .{ .v = .foo }, .{ .v = .bar }, .{ .v = .foo }, .{ .v = .bar } },
461+
&[_]S{ .{ .v = .foo }, .{ .v = .bar }, .{ .v = .foo }, .{ .v = .bar } },
462+
);
305463
}
306464

307465
/// This function is intended to be used only in tests. Checks that two slices or two arrays are equal,
@@ -555,121 +713,6 @@ test {
555713
try expectEqualStrings("foo", "foo");
556714
}
557715

558-
/// This function is intended to be used only in tests. When the two slices are not
559-
/// equal, prints hexdumps of the inputs with the differences highlighted in red to stderr,
560-
/// then returns a test failure error. The colorized output is optional and controlled
561-
/// by the return of `std.debug.detectTTYConfig()`.
562-
pub fn expectEqualBytes(expected: []const u8, actual: []const u8) !void {
563-
if (std.mem.indexOfDiff(u8, actual, expected)) |diff_index| {
564-
std.debug.print("byte slices differ. first difference occurs at offset {d} (0x{X})\n", .{ diff_index, diff_index });
565-
566-
// TODO: Should this be configurable by the caller?
567-
const max_window_size: usize = 256;
568-
569-
// Print a maximum of max_window_size bytes of each input, starting just before the
570-
// first difference.
571-
var window_start: usize = 0;
572-
if (@max(actual.len, expected.len) > max_window_size) {
573-
window_start = std.mem.alignBackward(diff_index - @min(diff_index, 16), 16);
574-
}
575-
const expected_window = expected[window_start..@min(expected.len, window_start + max_window_size)];
576-
const expected_truncated = window_start + expected_window.len < expected.len;
577-
const actual_window = actual[window_start..@min(actual.len, window_start + max_window_size)];
578-
const actual_truncated = window_start + actual_window.len < actual.len;
579-
580-
var differ = BytesDiffer{
581-
.expected = expected_window,
582-
.actual = actual_window,
583-
.ttyconf = std.debug.detectTTYConfig(),
584-
};
585-
const stderr = std.io.getStdErr();
586-
587-
std.debug.print("\n============ expected this output: ============= len: {} (0x{X})\n\n", .{ expected.len, expected.len });
588-
if (window_start > 0) {
589-
std.debug.print("... truncated, start offset: 0x{X} ...\n", .{window_start});
590-
}
591-
differ.write(stderr.writer()) catch {};
592-
if (expected_truncated) {
593-
const end_offset = window_start + expected_window.len;
594-
const num_missing_bytes = expected.len - (window_start + expected_window.len);
595-
std.debug.print("... truncated, end offset: 0x{X}, remaining bytes: 0x{X} ...\n", .{ end_offset, num_missing_bytes });
596-
}
597-
598-
// now reverse expected/actual and print again
599-
differ.expected = actual_window;
600-
differ.actual = expected_window;
601-
std.debug.print("\n============= instead found this: ============== len: {} (0x{X})\n\n", .{ actual.len, actual.len });
602-
if (window_start > 0) {
603-
std.debug.print("... truncated, start offset: 0x{X} ...\n", .{window_start});
604-
}
605-
differ.write(stderr.writer()) catch {};
606-
if (actual_truncated) {
607-
const end_offset = window_start + actual_window.len;
608-
const num_missing_bytes = actual.len - (window_start + actual_window.len);
609-
std.debug.print("... truncated, end offset: 0x{X}, remaining bytes: 0x{X} ...\n", .{ end_offset, num_missing_bytes });
610-
}
611-
std.debug.print("\n================================================\n\n", .{});
612-
613-
return error.TestExpectedEqual;
614-
}
615-
}
616-
617-
const BytesDiffer = struct {
618-
expected: []const u8,
619-
actual: []const u8,
620-
ttyconf: std.debug.TTY.Config,
621-
622-
pub fn write(self: BytesDiffer, writer: anytype) !void {
623-
var expected_iterator = ChunkIterator{ .bytes = self.expected };
624-
while (expected_iterator.next()) |chunk| {
625-
// to avoid having to calculate diffs twice per chunk
626-
var diffs: std.bit_set.IntegerBitSet(16) = .{ .mask = 0 };
627-
for (chunk) |byte, i| {
628-
var absolute_byte_index = (expected_iterator.index - chunk.len) + i;
629-
const diff = if (absolute_byte_index < self.actual.len) self.actual[absolute_byte_index] != byte else true;
630-
if (diff) diffs.set(i);
631-
try self.writeByteDiff(writer, "{X:0>2} ", byte, diff);
632-
if (i == 7) try writer.writeByte(' ');
633-
}
634-
try writer.writeByte(' ');
635-
if (chunk.len < 16) {
636-
var missing_columns = (16 - chunk.len) * 3;
637-
if (chunk.len < 8) missing_columns += 1;
638-
try writer.writeByteNTimes(' ', missing_columns);
639-
}
640-
for (chunk) |byte, i| {
641-
const byte_to_print = if (std.ascii.isPrint(byte)) byte else '.';
642-
try self.writeByteDiff(writer, "{c}", byte_to_print, diffs.isSet(i));
643-
}
644-
try writer.writeByte('\n');
645-
}
646-
}
647-
648-
fn writeByteDiff(self: BytesDiffer, writer: anytype, comptime fmt: []const u8, byte: u8, diff: bool) !void {
649-
if (diff) self.ttyconf.setColor(writer, .Red);
650-
try writer.print(fmt, .{byte});
651-
if (diff) self.ttyconf.setColor(writer, .Reset);
652-
}
653-
654-
const ChunkIterator = struct {
655-
bytes: []const u8,
656-
index: usize = 0,
657-
658-
pub fn next(self: *ChunkIterator) ?[]const u8 {
659-
if (self.index == self.bytes.len) return null;
660-
661-
const start_index = self.index;
662-
const end_index = @min(self.bytes.len, start_index + 16);
663-
self.index = end_index;
664-
return self.bytes[start_index..end_index];
665-
}
666-
};
667-
};
668-
669-
test {
670-
try expectEqualBytes("foo\x00", "foo\x00");
671-
}
672-
673716
/// Exhaustively check that allocation failures within `test_fn` are handled without
674717
/// introducing memory leaks. If used with the `testing.allocator` as the `backing_allocator`,
675718
/// it will also be able to detect double frees, etc (when runtime safety is enabled).

0 commit comments

Comments
 (0)