|
| 1 | +// The contents of this file is largely based on testing.zig from the Zig 0.15.1 |
| 2 | +// stdlib, distributed under the MIT license, copyright (c) Zig contributors |
| 3 | +const std = @import("std"); |
| 4 | + |
| 5 | +/// Generic, recursive equality testing utility using approximate comparison for |
| 6 | +/// floats and equality for everything else |
| 7 | +/// |
| 8 | +/// Based on `std.testing.expectEqual` and `std.testing.expectEqualSlices`. |
| 9 | +/// |
| 10 | +/// The relative tolerance is currently hardcoded to `sqrt(eps(float_type))`. |
| 11 | +pub inline fn expectApproxEqual(expected: anytype, actual: anytype) !void { |
| 12 | + const T = @TypeOf(expected, actual); |
| 13 | + return expectApproxEqualInner(T, expected, actual); |
| 14 | +} |
| 15 | + |
| 16 | +fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void { |
| 17 | + switch (@typeInfo(T)) { |
| 18 | + // check approximate equality for floats |
| 19 | + .float => { |
| 20 | + const sqrt_eps = comptime std.math.sqrt(std.math.floatEps(T)); |
| 21 | + if (!std.math.approxEqRel(T, expected, actual, sqrt_eps)) { |
| 22 | + print("expected approximately {any}, found {any}\n", .{ expected, actual }); |
| 23 | + return error.TestExpectedApproxEqual; |
| 24 | + } |
| 25 | + }, |
| 26 | + |
| 27 | + // recurse into containers |
| 28 | + .array => { |
| 29 | + const diff_index: usize = diff_index: { |
| 30 | + const shortest = @min(expected.len, actual.len); |
| 31 | + var index: usize = 0; |
| 32 | + while (index < shortest) : (index += 1) { |
| 33 | + expectApproxEqual(actual[index], expected[index]) catch break :diff_index index; |
| 34 | + } |
| 35 | + break :diff_index if (expected.len == actual.len) return else shortest; |
| 36 | + }; |
| 37 | + print("slices not approximately equal. first significant difference occurs at index {d} (0x{X})\n", .{ diff_index, diff_index }); |
| 38 | + return error.TestExpectedApproxEqual; |
| 39 | + }, |
| 40 | + .vector => |info| { |
| 41 | + var i: usize = 0; |
| 42 | + while (i < info.len) : (i += 1) { |
| 43 | + expectApproxEqual(expected[i], actual[i]) catch { |
| 44 | + print("index {d} incorrect. expected approximately {any}, found {any}\n", .{ |
| 45 | + i, expected[i], actual[i], |
| 46 | + }); |
| 47 | + return error.TestExpectedApproxEqual; |
| 48 | + }; |
| 49 | + } |
| 50 | + }, |
| 51 | + .@"struct" => |structType| { |
| 52 | + inline for (structType.fields) |field| { |
| 53 | + try expectApproxEqual(@field(expected, field.name), @field(actual, field.name)); |
| 54 | + } |
| 55 | + }, |
| 56 | + |
| 57 | + // unwrap unions, optionals, and error unions |
| 58 | + .@"union" => |union_info| { |
| 59 | + if (union_info.tag_type == null) { |
| 60 | + // untagged unions can only be compared bitwise, |
| 61 | + // so expectEqual is all we need |
| 62 | + std.testing.expectEqual(expected, actual) catch { |
| 63 | + return error.TestExpectedApproxEqual; |
| 64 | + }; |
| 65 | + } |
| 66 | + |
| 67 | + const Tag = std.meta.Tag(@TypeOf(expected)); |
| 68 | + |
| 69 | + const expectedTag = @as(Tag, expected); |
| 70 | + const actualTag = @as(Tag, actual); |
| 71 | + |
| 72 | + std.testing.expectEqual(expectedTag, actualTag) catch { |
| 73 | + return error.TestExpectedApproxEqual; |
| 74 | + }; |
| 75 | + |
| 76 | + // we only reach this switch if the tags are equal |
| 77 | + switch (expected) { |
| 78 | + inline else => |val, tag| try expectApproxEqual(val, @field(actual, @tagName(tag))), |
| 79 | + } |
| 80 | + }, |
| 81 | + .optional, .error_union => { |
| 82 | + if (expected) |expected_payload| if (actual) |actual_payload| { |
| 83 | + return expectApproxEqual(expected_payload, actual_payload); |
| 84 | + }; |
| 85 | + // we only reach this point if there's at least one null or error, |
| 86 | + // in which case expectEqual is all we need |
| 87 | + std.testing.expectEqual(expected, actual) catch { |
| 88 | + return error.TestExpectedApproxEqual; |
| 89 | + }; |
| 90 | + }, |
| 91 | + |
| 92 | + // fall back to expectEqual for everything else |
| 93 | + else => std.testing.expectEqual(expected, actual) catch { |
| 94 | + return error.TestExpectedApproxEqual; |
| 95 | + }, |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +/// Copy of std.testing.print (not public) |
| 100 | +fn print(comptime fmt: []const u8, args: anytype) void { |
| 101 | + if (@inComptime()) { |
| 102 | + @compileError(std.fmt.comptimePrint(fmt, args)); |
| 103 | + } else if (std.testing.backend_can_print) { |
| 104 | + std.debug.print(fmt, args); |
| 105 | + } |
| 106 | +} |
| 107 | + |
| 108 | +// Tests based on the `expectEqual` tests in the Zig stdlib |
| 109 | +test "expectApproxEqual.union(enum)" { |
| 110 | + const T = union(enum) { |
| 111 | + a: i32, |
| 112 | + b: f32, |
| 113 | + }; |
| 114 | + |
| 115 | + const b10 = T{ .b = 10.0 }; |
| 116 | + const b10plus = T{ .b = 10.000001 }; |
| 117 | + |
| 118 | + try expectApproxEqual(b10, b10plus); |
| 119 | +} |
| 120 | + |
| 121 | +test "expectApproxEqual nested array" { |
| 122 | + const a = [2][2]f32{ |
| 123 | + [_]f32{ 1.0, 0.0 }, |
| 124 | + [_]f32{ 0.0, 1.0 }, |
| 125 | + }; |
| 126 | + |
| 127 | + const b = [2][2]f32{ |
| 128 | + [_]f32{ 1.000001, 0.0 }, |
| 129 | + [_]f32{ 0.0, 0.999999 }, |
| 130 | + }; |
| 131 | + |
| 132 | + try expectApproxEqual(a, b); |
| 133 | +} |
| 134 | + |
| 135 | +test "expectApproxEqual vector" { |
| 136 | + const a: @Vector(4, f32) = @splat(4.0); |
| 137 | + const b: @Vector(4, f32) = @splat(4.000001); |
| 138 | + |
| 139 | + try expectApproxEqual(a, b); |
| 140 | +} |
| 141 | + |
| 142 | +test "expectApproxEqual struct" { |
| 143 | + const a = .{ 1, @as(f32, 1.0) }; |
| 144 | + const b = .{ 1, @as(f32, 0.999999) }; |
| 145 | + |
| 146 | + try expectApproxEqual(a, b); |
| 147 | +} |
0 commit comments