| ... | @@ -278,30 +278,188 @@ test "expectApproxEqRel" { | ... | @@ -278,30 +278,188 @@ test "expectApproxEqRel" { |
| 278 | } | 278 | } |
| 279 | | 279 | |
| 280 | /// This function is intended to be used only in tests. When the two slices are not | 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, | 281 | /// equal, prints diagnostics to stderr to show exactly how they are not equal (with |
| 282 | /// then returns a test failure error. | 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 | /// If your inputs are UTF-8 encoded strings, consider calling `expectEqualStrings` instead. | 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 | pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const T) !void { | 285 | pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const T) !void { |
| 287 | if (T == u8) { | 286 | if (expected.ptr == actual.ptr and expected.len == actual.len) { |
| 288 | return expectEqualBytes(expected, actual); | 287 | return; |
| 289 | } | 288 | } |
| 290 | // TODO better printing of the difference | 289 | const diff_index: usize = diff_index: { |
| 291 | // If the arrays are small enough we could print the whole thing | 290 | const shortest = @min(expected.len, actual.len); |
| 292 | // If the child type is u8 and no weird bytes, we could print it as strings | 291 | var index: usize = 0; |
| 293 | // Even for the length difference, it would be useful to see the values of the slices probably. | 292 | while (index < shortest) : (index += 1) { |
| 294 | if (expected.len != actual.len) { | 293 | if (!std.meta.eql(actual[index], expected[index])) break :diff_index index; |
| 295 | std.debug.print("slice lengths differ. expected {d}, found {d}\n", .{ expected.len, actual.len }); | 294 | } |
| 296 | return error.TestExpectedEqual; | 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; | 311 | const expected_window = expected[window_start..@min(expected.len, window_start + max_window_size)]; |
| 299 | while (i < expected.len) : (i += 1) { | 312 | const expected_truncated = window_start + expected_window.len < expected.len; |
| 300 | if (!std.meta.eql(expected[i], actual[i])) { | 313 | const actual_window = actual[window_start..@min(actual.len, window_start + max_window_size)]; |
| 301 | std.debug.print("index {} incorrect. expected {any}, found {any}\n", .{ i, expected[i], actual[i] }); | 314 | const actual_truncated = window_start + actual_window.len < actual.len; |
| 302 | return error.TestExpectedEqual; | 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 | /// This function is intended to be used only in tests. Checks that two slices or two arrays are equal, | 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,121 +713,6 @@ test { |
| 555 | try expectEqualStrings("foo", "foo"); | 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 | /// Exhaustively check that allocation failures within `test_fn` are handled without | 716 | /// Exhaustively check that allocation failures within `test_fn` are handled without |
| 674 | /// introducing memory leaks. If used with the `testing.allocator` as the `backing_allocator`, | 717 | /// introducing memory leaks. If used with the `testing.allocator` as the `backing_allocator`, |
| 675 | /// it will also be able to detect double frees, etc (when runtime safety is enabled). | 718 | /// it will also be able to detect double frees, etc (when runtime safety is enabled). |