@@ -278,30 +278,188 @@ test "expectApproxEqRel" {
278
278
}
279
279
280
280
/// 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()`.
283
284
/// 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`).
286
285
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 ;
289
288
}
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 );
297
310
}
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 ' );
303
427
}
304
428
}
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
+ );
305
463
}
306
464
307
465
/// This function is intended to be used only in tests. Checks that two slices or two arrays are equal,
@@ -555,121 +713,6 @@ test {
555
713
try expectEqualStrings ("foo" , "foo" );
556
714
}
557
715
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
-
673
716
/// Exhaustively check that allocation failures within `test_fn` are handled without
674
717
/// introducing memory leaks. If used with the `testing.allocator` as the `backing_allocator`,
675
718
/// it will also be able to detect double frees, etc (when runtime safety is enabled).
0 commit comments