diff --git a/lib/std/testing.zig b/lib/std/testing.zig index 6bef15e8a196bfaf19da01b4eaccff86ee561b15..0ee3617878d9aa6a580d30d82d7d5550e58c7bca 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -18,10 +18,7 @@ var failing_allocator_instance = FailingAllocator.init(base_allocator_instance.a var base_allocator_instance = std.heap.FixedBufferAllocator.init(""); pub var allocator_instance: std.heap.SafeAllocator = undefined; -pub const allocator = if (builtin.is_test) - allocator_instance.allocator() -else - @compileError("not testing"); +pub const allocator = if (builtin.is_test) allocator_instance.allocator() else @compileError("not testing"); pub var io_instance: Io.Threaded = undefined; pub const io = if (builtin.is_test) io_instance.io() else @compileError("not testing"); @@ -42,7 +39,8 @@ pub const backend_can_print = switch (builtin.zig_backend) { else => true, }; -fn print(comptime fmt: []const u8, args: anytype) void { +/// Helper function for printing test failure information. +pub fn failPrint(comptime fmt: []const u8, args: anytype) void { if (@inComptime()) { @compileError(std.fmt.comptimePrint(fmt, args)); } else if (backend_can_print) { @@ -50,23 +48,6 @@ fn print(comptime fmt: []const u8, args: anytype) void { } } -/// This function is intended to be used only in tests. It prints diagnostics to stderr -/// and then returns a test failure error when actual_error_union is not expected_error. -pub fn expectError(expected_error: anyerror, actual_error_union: anytype) !void { - if (actual_error_union) |actual_payload| { - print("expected error.{s}, found {any}\n", .{ @errorName(expected_error), actual_payload }); - return error.TestExpectedError; - } else |actual_error| { - if (expected_error != actual_error) { - print("expected error.{s}, found error.{s}\n", .{ - @errorName(expected_error), - @errorName(actual_error), - }); - return error.TestUnexpectedError; - } - } -} - /// This function is intended to be used only in tests. When the two values are not /// equal, prints diagnostics to stderr to show exactly how they are not equal, /// then returns a test failure error. @@ -92,7 +73,7 @@ fn expectEqualInner(comptime T: type, expected: T, actual: T) !void { .type => { if (actual != expected) { - print("expected type {s}, found type {s}\n", .{ @typeName(expected), @typeName(actual) }); + failPrint("expected type {s}, found type {s}\n", .{ @typeName(expected), @typeName(actual) }); return error.TestExpectedEqual; } }, @@ -108,7 +89,7 @@ fn expectEqualInner(comptime T: type, expected: T, actual: T) !void { .error_set, => { if (actual != expected) { - print("expected {any}, found {any}\n", .{ expected, actual }); + failPrint("expected {any}, found {any}\n", .{ expected, actual }); return error.TestExpectedEqual; } }, @@ -117,17 +98,17 @@ fn expectEqualInner(comptime T: type, expected: T, actual: T) !void { switch (pointer.size) { .one, .many, .c => { if (actual != expected) { - print("expected {*}, found {*}\n", .{ expected, actual }); + failPrint("expected {*}, found {*}\n", .{ expected, actual }); return error.TestExpectedEqual; } }, .slice => { if (actual.ptr != expected.ptr) { - print("expected slice ptr {*}, found {*}\n", .{ expected.ptr, actual.ptr }); + failPrint("expected slice ptr {*}, found {*}\n", .{ expected.ptr, actual.ptr }); return error.TestExpectedEqual; } if (actual.len != expected.len) { - print("expected slice len {}, found {}\n", .{ expected.len, actual.len }); + failPrint("expected slice len {}, found {}\n", .{ expected.len, actual.len }); return error.TestExpectedEqual; } }, @@ -188,12 +169,12 @@ fn expectEqualInner(comptime T: type, expected: T, actual: T) !void { if (actual) |actual_payload| { try expectEqual(expected_payload, actual_payload); } else { - print("expected {any}, found null\n", .{expected_payload}); + failPrint("expected {any}, found null\n", .{expected_payload}); return error.TestExpectedEqual; } } else { if (actual) |actual_payload| { - print("expected null, found {any}\n", .{actual_payload}); + failPrint("expected null, found {any}\n", .{actual_payload}); return error.TestExpectedEqual; } } @@ -204,12 +185,12 @@ fn expectEqualInner(comptime T: type, expected: T, actual: T) !void { if (actual) |actual_payload| { try expectEqual(expected_payload, actual_payload); } else |actual_err| { - print("expected {any}, found {}\n", .{ expected_payload, actual_err }); + failPrint("expected {any}, found {}\n", .{ expected_payload, actual_err }); return error.TestExpectedEqual; } } else |expected_err| { if (actual) |actual_payload| { - print("expected {}, found {any}\n", .{ expected_err, actual_payload }); + failPrint("expected {}, found {any}\n", .{ expected_err, actual_payload }); return error.TestExpectedEqual; } else |actual_err| { try expectEqual(expected_err, actual_err); @@ -219,7 +200,7 @@ fn expectEqualInner(comptime T: type, expected: T, actual: T) !void { } } -test "expectEqual.union(enum)" { +test "expectEqual union(enum)" { const T = union(enum) { a: i32, b: f32, @@ -268,20 +249,6 @@ test "expectEqual null" { try expectEqual(a, b); } -/// This function is intended to be used only in tests. When the formatted result of the template -/// and its arguments does not equal the expected text, it prints diagnostics to stderr to show how -/// they are not equal, then returns an error. It depends on `expectEqualStrings` for printing -/// diagnostics. -pub fn expectFmt(expected: []const u8, comptime template: []const u8, args: anytype) !void { - if (@inComptime()) { - var buffer: [std.fmt.count(template, args)]u8 = undefined; - return expectEqualStrings(expected, try std.fmt.bufPrint(&buffer, template, args)); - } - const actual = try std.fmt.allocPrint(allocator, template, args); - defer allocator.free(actual); - return expectEqualStrings(expected, actual); -} - /// This function is intended to be used only in tests. When the actual value is /// not approximately equal to the expected value, prints diagnostics to stderr /// to show exactly how they are not equal, then returns a test failure error. @@ -296,7 +263,7 @@ pub inline fn expectApproxEqAbs(expected: anytype, actual: anytype, tolerance: a fn expectApproxEqAbsInner(comptime T: type, expected: T, actual: T, tolerance: T) !void { switch (@typeInfo(T)) { .float => if (!math.approxEqAbs(T, expected, actual, tolerance)) { - print("actual {}, not within absolute tolerance {} of expected {}\n", .{ actual, tolerance, expected }); + failPrint("actual {}, not within absolute tolerance {} of expected {}\n", .{ actual, tolerance, expected }); return error.TestExpectedApproxEqAbs; }, @@ -332,7 +299,7 @@ pub inline fn expectApproxEqRel(expected: anytype, actual: anytype, tolerance: a fn expectApproxEqRelInner(comptime T: type, expected: T, actual: T, tolerance: T) !void { switch (@typeInfo(T)) { .float => if (!math.approxEqRel(T, expected, actual, tolerance)) { - print("actual {}, not within relative tolerance {} of expected {}\n", .{ actual, tolerance, expected }); + failPrint("actual {}, not within relative tolerance {} of expected {}\n", .{ actual, tolerance, expected }); return error.TestExpectedApproxEqRel; }, @@ -358,7 +325,7 @@ test expectApproxEqRel { } /// This function is intended to be used only in tests. When the two slices are -/// not equal, prints diagnostics to stderr to show exactly how they are not +/// not equal, it prints diagnostics to stderr to show exactly how they are not /// equal (with the differences highlighted in red), then returns a test /// failure error. pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const T) !void { @@ -553,7 +520,7 @@ const BytesDiffer = struct { } }; -test { +test expectEqualSlices { try expectEqualSlices(u8, "foo\x00", "foo\x00"); try expectEqualSlices(u16, &[_]u16{ 100, 200, 300, 400 }, &[_]u16{ 100, 200, 300, 400 }); const E = enum { foo, bar }; @@ -567,8 +534,10 @@ test { ); } -/// This function is intended to be used only in tests. Checks that two slices or two arrays are equal, -/// including that their sentinel (if any) are the same. Will error if given another type. +/// This function is intended to be used only in tests. When the two slices or two arrays are not equal, +/// or their sentinel (if any) are not the same, it prints diagnostics to stderr to show exactly how +/// they are not equal (with the differences highlighted in red), then returns a test failure error. +/// It partially depends on `expectEquaSlices` for printing diagnostics. pub fn expectEqualSentinel(comptime T: type, comptime sentinel: T, expected: [:sentinel]const T, actual: [:sentinel]const T) !void { try expectEqualSlices(T, expected, actual); @@ -599,12 +568,12 @@ pub fn expectEqualSentinel(comptime T: type, comptime sentinel: T, expected: [:s }; if (!std.meta.eql(sentinel, expected_value_sentinel)) { - print("expectEqualSentinel: 'expected' sentinel in memory is different from its type sentinel. type sentinel {}, in memory sentinel {}\n", .{ sentinel, expected_value_sentinel }); + failPrint("expectEqualSentinel: 'expected' sentinel in memory is different from its type sentinel. type sentinel {}, in memory sentinel {}\n", .{ sentinel, expected_value_sentinel }); return error.TestExpectedEqual; } if (!std.meta.eql(sentinel, actual_value_sentinel)) { - print("expectEqualSentinel: 'actual' sentinel in memory is different from its type sentinel. type sentinel {}, in memory sentinel {}\n", .{ sentinel, actual_value_sentinel }); + failPrint("expectEqualSentinel: 'actual' sentinel in memory is different from its type sentinel. type sentinel {}, in memory sentinel {}\n", .{ sentinel, actual_value_sentinel }); return error.TestExpectedEqual; } } @@ -654,6 +623,48 @@ pub fn tmpDir(opts: Io.Dir.OpenOptions) TmpDir { }; } +/// This function is intended to be used only in tests. When `actual_error_union` is not +/// `expected_error`, it prints diagnostics to stderr, then returns a test failure error. +pub fn expectError(expected_error: anyerror, actual_error_union: anytype) !void { + if (actual_error_union) |actual_payload| { + failPrint("expected error.{s}, found {any}\n", .{ @errorName(expected_error), actual_payload }); + return error.TestExpectedError; + } else |actual_error| { + if (expected_error != actual_error) { + failPrint("expected error.{s}, found error.{s}\n", .{ + @errorName(expected_error), + @errorName(actual_error), + }); + return error.TestUnexpectedError; + } + } +} + +fn returnErrorUnion() !u8 { + return error.Expected; +} + +test expectError { + const actualErrorUnion = returnErrorUnion(); + try expectError(error.Expected, actualErrorUnion); +} + +/// This function is intended to be used only in tests. When the formatted result of the template +/// and its arguments does not equal the expected text, it prints diagnostics to stderr to show how +/// they are not equal, then returns an error. It depends on `expectEqualStrings` for printing +/// diagnostics. +pub fn expectFmt(expected: []const u8, comptime template: []const u8, args: anytype) !void { + if (@inComptime()) { + var buffer: [std.fmt.count(template, args)]u8 = undefined; + return expectEqualStrings(expected, try std.fmt.bufPrint(&buffer, template, args)); + } + const actual = try std.fmt.allocPrint(allocator, template, args); + defer allocator.free(actual); + return expectEqualStrings(expected, actual); +} + +// This function is intended to be used only in test. When the two strings are not equal, +/// it prints diagnostics to stderr to show how they are not equal, then returns an error. pub fn expectEqualStrings(expected: []const u8, actual: []const u8) !void { if (std.mem.findDiff(u8, actual, expected)) |diff_index| { if (@inComptime()) { @@ -661,28 +672,34 @@ pub fn expectEqualStrings(expected: []const u8, actual: []const u8) !void { expected, actual, diff_index, })); } - print("\n====== expected this output: =========\n", .{}); - printWithVisibleNewlines(expected); - print("\n======== instead found this: =========\n", .{}); - printWithVisibleNewlines(actual); - print("\n======================================\n", .{}); + failPrint("\n====== expected this output: =========\n", .{}); + failPrintWithVisibleNewlines(expected); + failPrint("\n======== instead found this: =========\n", .{}); + failPrintWithVisibleNewlines(actual); + failPrint("\n======================================\n", .{}); var diff_line_number: usize = 1; for (expected[0..diff_index]) |value| { if (value == '\n') diff_line_number += 1; } - print("First difference occurs on line {d}:\n", .{diff_line_number}); + failPrint("First difference occurs on line {d}:\n", .{diff_line_number}); - print("expected:\n", .{}); - printIndicatorLine(expected, diff_index); + failPrint("expected:\n", .{}); + failPrintIndicatorLine(expected, diff_index); - print("found:\n", .{}); - printIndicatorLine(actual, diff_index); + failPrint("found:\n", .{}); + failPrintIndicatorLine(actual, diff_index); return error.TestExpectedEqual; } } +test expectEqualStrings { + try expectEqualStrings("foo", "foo"); +} + +/// This function is intended to be used only in test. When the start of `actual` and `expected_starts_with` +/// are not equal, it prints diagnostics to stderr to show how they are not equal, then returns an error. pub fn expectStringStartsWith(actual: []const u8, expected_starts_with: []const u8) !void { if (std.mem.startsWith(u8, actual, expected_starts_with)) return; @@ -692,17 +709,23 @@ pub fn expectStringStartsWith(actual: []const u8, expected_starts_with: []const else actual; - print("\n====== expected to start with: =========\n", .{}); - printWithVisibleNewlines(expected_starts_with); - print("\n====== instead started with: ===========\n", .{}); - printWithVisibleNewlines(shortened_actual); - print("\n========= full output: ==============\n", .{}); - printWithVisibleNewlines(actual); - print("\n======================================\n", .{}); + failPrint("\n====== expected to start with: =========\n", .{}); + failPrintWithVisibleNewlines(expected_starts_with); + failPrint("\n====== instead started with: ===========\n", .{}); + failPrintWithVisibleNewlines(shortened_actual); + failPrint("\n========= full output: ==============\n", .{}); + failPrintWithVisibleNewlines(actual); + failPrint("\n======================================\n", .{}); return error.TestExpectedStartsWith; } +test expectStringStartsWith { + try expectStringStartsWith("foobar", "foo"); +} + +/// This function is intended to be used only in test. When the end of `actual` and `expected_ends_with` +/// are not equal, it prints diagnostics to stderr to show how they are not equal, then returns an error. pub fn expectStringEndsWith(actual: []const u8, expected_ends_with: []const u8) !void { if (std.mem.endsWith(u8, actual, expected_ends_with)) return; @@ -712,17 +735,21 @@ pub fn expectStringEndsWith(actual: []const u8, expected_ends_with: []const u8) else actual; - print("\n====== expected to end with: =========\n", .{}); - printWithVisibleNewlines(expected_ends_with); - print("\n====== instead ended with: ===========\n", .{}); - printWithVisibleNewlines(shortened_actual); - print("\n========= full output: ==============\n", .{}); - printWithVisibleNewlines(actual); - print("\n======================================\n", .{}); + failPrint("\n====== expected to end with: =========\n", .{}); + failPrintWithVisibleNewlines(expected_ends_with); + failPrint("\n====== instead ended with: ===========\n", .{}); + failPrintWithVisibleNewlines(shortened_actual); + failPrint("\n========= full output: ==============\n", .{}); + failPrintWithVisibleNewlines(actual); + failPrint("\n======================================\n", .{}); return error.TestExpectedEndsWith; } +test expectStringEndsWith { + try expectStringEndsWith("foobar", "bar"); +} + /// This function is intended to be used only in tests. When the two values are not /// deeply equal, prints diagnostics to stderr to show exactly how they are not equal, /// then returns a test failure error. @@ -757,7 +784,7 @@ fn expectEqualDeepInner(comptime T: type, expected: T, actual: T) error{TestExpe .type => { if (actual != expected) { - print("expected type {s}, found type {s}\n", .{ @typeName(expected), @typeName(actual) }); + failPrint("expected type {s}, found type {s}\n", .{ @typeName(expected), @typeName(actual) }); return error.TestExpectedEqual; } }, @@ -773,7 +800,7 @@ fn expectEqualDeepInner(comptime T: type, expected: T, actual: T) error{TestExpe .error_set, => { if (actual != expected) { - print("expected {any}, found {any}\n", .{ expected, actual }); + failPrint("expected {any}, found {any}\n", .{ expected, actual }); return error.TestExpectedEqual; } }, @@ -783,7 +810,7 @@ fn expectEqualDeepInner(comptime T: type, expected: T, actual: T) error{TestExpe // We have no idea what is behind those pointers, so the best we can do is `==` check. .c, .many => { if (actual != expected) { - print("expected {*}, found {*}\n", .{ expected, actual }); + failPrint("expected {*}, found {*}\n", .{ expected, actual }); return error.TestExpectedEqual; } }, @@ -792,7 +819,7 @@ fn expectEqualDeepInner(comptime T: type, expected: T, actual: T) error{TestExpe switch (@typeInfo(pointer.child)) { .@"fn", .@"opaque" => { if (actual != expected) { - print("expected {*}, found {*}\n", .{ expected, actual }); + failPrint("expected {*}, found {*}\n", .{ expected, actual }); return error.TestExpectedEqual; } }, @@ -801,13 +828,13 @@ fn expectEqualDeepInner(comptime T: type, expected: T, actual: T) error{TestExpe }, .slice => { if (expected.len != actual.len) { - print("Slice len not the same, expected {d}, found {d}\n", .{ expected.len, actual.len }); + failPrint("Slice len not the same, expected {d}, found {d}\n", .{ expected.len, actual.len }); return error.TestExpectedEqual; } var i: usize = 0; while (i < expected.len) : (i += 1) { expectEqualDeep(expected[i], actual[i]) catch |e| { - print("index {d} incorrect. expected {any}, found {any}\n", .{ + failPrint("index {d} incorrect. expected {any}, found {any}\n", .{ i, expected[i], actual[i], }); return e; @@ -819,13 +846,13 @@ fn expectEqualDeepInner(comptime T: type, expected: T, actual: T) error{TestExpe .array => { if (expected.len != actual.len) { - print("Array len not the same, expected {d}, found {d}\n", .{ expected.len, actual.len }); + failPrint("Array len not the same, expected {d}, found {d}\n", .{ expected.len, actual.len }); return error.TestExpectedEqual; } var i: usize = 0; while (i < expected.len) : (i += 1) { expectEqualDeep(expected[i], actual[i]) catch |e| { - print("index {d} incorrect. expected {any}, found {any}\n", .{ + failPrint("index {d} incorrect. expected {any}, found {any}\n", .{ i, expected[i], actual[i], }); return e; @@ -835,12 +862,12 @@ fn expectEqualDeepInner(comptime T: type, expected: T, actual: T) error{TestExpe .vector => |info| { if (info.len != @typeInfo(@TypeOf(actual)).vector.len) { - print("Vector len not the same, expected {d}, found {d}\n", .{ info.len, @typeInfo(@TypeOf(actual)).vector.len }); + failPrint("Vector len not the same, expected {d}, found {d}\n", .{ info.len, @typeInfo(@TypeOf(actual)).vector.len }); return error.TestExpectedEqual; } inline for (0..info.len) |i| { expectEqualDeep(expected[i], actual[i]) catch |e| { - print("index {d} incorrect. expected {any}, found {any}\n", .{ + failPrint("index {d} incorrect. expected {any}, found {any}\n", .{ i, expected[i], actual[i], }); return e; @@ -851,7 +878,7 @@ fn expectEqualDeepInner(comptime T: type, expected: T, actual: T) error{TestExpe .@"struct" => |structType| { inline for (structType.field_names) |field_name| { expectEqualDeep(@field(expected, field_name), @field(actual, field_name)) catch |e| { - print("Field {s} incorrect. expected {any}, found {any}\n", .{ field_name, @field(expected, field_name), @field(actual, field_name) }); + failPrint("Field {s} incorrect. expected {any}, found {any}\n", .{ field_name, @field(expected, field_name), @field(actual, field_name) }); return e; }; } @@ -882,12 +909,12 @@ fn expectEqualDeepInner(comptime T: type, expected: T, actual: T) error{TestExpe if (actual) |actual_payload| { try expectEqualDeep(expected_payload, actual_payload); } else { - print("expected {any}, found null\n", .{expected_payload}); + failPrint("expected {any}, found null\n", .{expected_payload}); return error.TestExpectedEqual; } } else { if (actual) |actual_payload| { - print("expected null, found {any}\n", .{actual_payload}); + failPrint("expected null, found {any}\n", .{actual_payload}); return error.TestExpectedEqual; } } @@ -898,12 +925,12 @@ fn expectEqualDeepInner(comptime T: type, expected: T, actual: T) error{TestExpe if (actual) |actual_payload| { try expectEqualDeep(expected_payload, actual_payload); } else |actual_err| { - print("expected {any}, found {any}\n", .{ expected_payload, actual_err }); + failPrint("expected {any}, found {any}\n", .{ expected_payload, actual_err }); return error.TestExpectedEqual; } } else |expected_err| { if (actual) |actual_payload| { - print("expected {any}, found {any}\n", .{ expected_err, actual_payload }); + failPrint("expected {any}, found {any}\n", .{ expected_err, actual_payload }); return error.TestExpectedEqual; } else |actual_err| { try expectEqualDeep(expected_err, actual_err); @@ -999,7 +1026,8 @@ test "expectEqualDeep composite type" { ); } -fn printIndicatorLine(source: []const u8, indicator_index: usize) void { +/// Helper function for printing test failure information. +pub fn failPrintIndicatorLine(source: []const u8, indicator_index: usize) void { const line_begin_index = if (std.mem.findScalarLast(u8, source[0..indicator_index], '\n')) |line_begin| line_begin + 1 else @@ -1009,33 +1037,31 @@ fn printIndicatorLine(source: []const u8, indicator_index: usize) void { else source.len; - printLine(source[line_begin_index..line_end_index]); + failPrintLine(source[line_begin_index..line_end_index]); for (line_begin_index..indicator_index) |_| - print(" ", .{}); + failPrint(" ", .{}); if (indicator_index >= source.len) - print("^ (end of string)\n", .{}) + failPrint("^ (end of string)\n", .{}) else - print("^ ('\\x{x:0>2}')\n", .{source[indicator_index]}); + failPrint("^ ('\\x{x:0>2}')\n", .{source[indicator_index]}); } -fn printWithVisibleNewlines(source: []const u8) void { +/// Helper function for printing test failure information. +pub fn failPrintWithVisibleNewlines(source: []const u8) void { var i: usize = 0; while (std.mem.findScalar(u8, source[i..], '\n')) |nl| : (i += nl + 1) { - printLine(source[i..][0..nl]); + failPrintLine(source[i..][0..nl]); } - print("{s}␃\n", .{source[i..]}); // End of Text symbol (ETX) + failPrint("{s}␃\n", .{source[i..]}); // End of Text symbol (ETX) } -fn printLine(line: []const u8) void { +/// Helper function for printing test failure information. +pub fn failPrintLine(line: []const u8) void { if (line.len != 0) switch (line[line.len - 1]) { - ' ', '\t' => return print("{s}⏎\n", .{line}), // Return symbol + ' ', '\t' => return failPrint("{s}⏎\n", .{line}), // Return symbol else => {}, }; - print("{s}\n", .{line}); -} - -test { - try expectEqualStrings("foo", "foo"); + failPrint("{s}\n", .{line}); } /// Exhaustively check that allocation failures within `test_fn` are handled without @@ -1140,7 +1166,7 @@ pub fn checkAllAllocationFailures( } else |err| switch (err) { error.OutOfMemory => { if (failing_allocator_inst.allocated_bytes != failing_allocator_inst.freed_bytes) { - print( + failPrint( "\nfail_index: {d}/{d}\nallocated bytes: {d}\nfreed bytes: {d}\nallocations: {d}\ndeallocations: {d}\nallocation that was made to fail: {f}", .{ fail_index,