| ... | ... | @@ -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). |