authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2022-12-07 22:50:32-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-12-09 04:36:27-05:00
log71c82393ebba8b40266551781d4b2c09b1fca1dc
tree988ead2ed0792d3b990112a49648c44560e53594
parent6966fb8ca5608cae122434a50ff2cd264502c0eb

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 files changed, 176 insertions(+), 133 deletions(-)

lib/std/testing.zig+176-133
......@@ -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`).
286285pub 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
378fn 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
399const 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
451test {
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()`.
562pub 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
617const 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
669test {
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).