mach/src/testing.zig
Stephen Gutekanst fb69b0cda7 testing: improve printing on testing.expect inequality
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
2024-01-06 13:11:23 -07:00

220 lines
8 KiB
Zig

const std = @import("std");
const testing = std.testing;
const mach = @import("main.zig");
const math = mach.math;
fn ExpectFloat(comptime T: type) type {
return struct {
expected: T,
/// Approximate (absolute epsilon tolerance) equality
pub fn eql(e: *const @This(), actual: T) !void {
try e.eqlApprox(actual, math.eps(T));
}
/// Approximate (absolute tolerance) equality
pub fn eqlApprox(e: *const @This(), actual: T, tolerance: T) !void {
// Note: testing.expectApproxEqAbs does the same thing, but does not print floating
// point values as decimal (prefers scientific notation)
if (!math.eql(T, e.expected, actual, tolerance)) {
std.debug.print("actual float {d}, expected {d} (not within absolute epsilon tolerance {d})\n", .{ actual, e.expected, tolerance });
return error.TestExpectEqualEps;
}
}
/// Bitwise equality
pub fn eqlBinary(e: *const @This(), actual: T) !void {
try testing.expectEqual(e.expected, actual);
}
};
}
fn ExpectVector(comptime T: type) type {
const Elem = std.meta.Elem(T);
const len = @typeInfo(T).Vector.len;
return struct {
expected: T,
/// Approximate (absolute epsilon tolerance) equality
pub fn eql(e: *const @This(), actual: T) !void {
try e.eqlApprox(actual, math.eps(Elem));
}
/// Approximate (absolute tolerance) equality
pub fn eqlApprox(e: *const @This(), actual: T, tolerance: Elem) !void {
var i: usize = 0;
while (i < len) : (i += 1) {
if (!math.eql(Elem, e.expected[i], actual[i], tolerance)) {
std.debug.print("actual vector {d}, expected {d} (not within absolute epsilon tolerance {d})\n", .{ actual, e.expected, tolerance });
std.debug.print("actual vector[{}] = {d}, expected {d} (not within absolute epsilon tolerance {d})\n", .{ i, actual[i], e.expected[i], tolerance });
return error.TestExpectEqualEps;
}
}
}
/// Bitwise equality
pub fn eqlBinary(e: *const @This(), actual: T) !void {
try testing.expectEqual(e.expected, actual);
}
};
}
fn ExpectVecMat(comptime T: type) type {
return struct {
expected: T,
/// Approximate (absolute epsilon tolerance) equality
pub fn eql(e: *const @This(), actual: T) !void {
try e.eqlApprox(actual, math.eps(T.T));
}
/// Approximate (absolute tolerance) equality
pub fn eqlApprox(e: *const @This(), actual: T, tolerance: T.T) !void {
var i: usize = 0;
while (i < T.n) : (i += 1) {
if (!math.eql(T.T, e.expected.v[i], actual.v[i], tolerance)) {
std.debug.print("actual vector {d}, expected {d} (not within absolute epsilon tolerance {d})\n", .{ actual.v, e.expected.v, tolerance });
std.debug.print("actual vector[{}] = {d}, expected {d} (not within absolute epsilon tolerance {d})\n", .{ i, actual.v[i], e.expected.v[i], tolerance });
return error.TestExpectEqualEps;
}
}
}
/// Bitwise equality
pub fn eqlBinary(e: *const @This(), actual: T) !void {
try testing.expectEqual(e.expected.v, actual.v);
}
};
}
fn ExpectComptime(comptime T: type) type {
return struct {
expected: T,
pub fn eql(comptime e: *const @This(), comptime actual: T) !void {
try testing.expectEqual(e.expected, actual);
}
};
}
fn ExpectBytes(comptime T: type) type {
return struct {
expected: T,
pub fn eql(comptime e: *const @This(), comptime actual: T) !void {
try testing.expectEqualStrings(e.expected, actual);
}
pub fn eqlBinary(comptime e: *const @This(), comptime actual: T) !void {
try testing.expectEqual(e.expected, actual);
}
};
}
fn Expect(comptime T: type) type {
if (T == type) return ExpectComptime(T);
if (T == f16 or T == f32 or T == f64) return ExpectFloat(T);
if (T == []const u8) return ExpectBytes(T);
if (@typeInfo(T) == .Vector) return ExpectVector(T);
// Vector and matrix equality
const is_vec2 = T == math.Vec2 or T == math.Vec2h or T == math.Vec2d;
const is_vec3 = T == math.Vec3 or T == math.Vec3h or T == math.Vec3d;
const is_vec4 = T == math.Vec4 or T == math.Vec4h or T == math.Vec4d;
if (is_vec2 or is_vec3 or is_vec4) return ExpectVecMat(T);
// TODO(testing): TODO(math): handle Mat, []Vec, []Mat without generic equality below.
// We can look at how std.testing handles slices, e.g. we should have equal or better output than
// what generic equality below gets us:
//
// ```
// ============ expected this output: ============= len: 4 (0x4)
//
// [0]: math.vec.Vec(4,f32){ .v = { 1.0e+00, 0.0e+00, 0.0e+00, 0.0e+00 } }
// [1]: math.vec.Vec(4,f32){ .v = { 0.0e+00, 1.0e+00, 0.0e+00, 0.0e+00 } }
// [2]: math.vec.Vec(4,f32){ .v = { 0.0e+00, 0.0e+00, 1.0e+00, 0.0e+00 } }
// [3]: math.vec.Vec(4,f32){ .v = { 0.0e+00, 0.0e+00, 0.0e+00, 1.0e+00 } }
//
// ============= instead found this: ============== len: 4 (0x4)
//
// [0]: math.vec.Vec(4,f32){ .v = { 1.0e+00, 0.0e+00, 0.0e+00, 0.0e+00 } }
// [1]: math.vec.Vec(4,f32){ .v = { 0.0e+00, 1.0e+00, 0.0e+00, 0.0e+00 } }
// [2]: math.vec.Vec(4,f32){ .v = { 0.0e+00, 0.0e+00, 1.0e+00, 0.0e+00 } }
// [3]: math.vec.Vec(4,f32){ .v = { 0.0e+00, 0.0e+00, 1.0e+00, 1.0e+00 } }
// ```
//
// Generic equality
return struct {
expected: T,
pub fn eql(e: *const @This(), actual: T) !void {
try testing.expectEqual(e.expected, actual);
}
};
}
/// Alternative to std.testing equality methods with:
///
/// * Less ambiguity about order of parameters
/// * Approximate absolute float equality by default
/// * Handling of vector and matrix types
///
/// Floats, mach.math.Vec, and mach.math.Mat types support:
///
/// * `.eql(v)` (epsilon equality)
/// * `.eqlApprox(v, tolerance)` (specific tolerance equality)
/// * `.eqlBinary(v)` binary equality
///
/// All other types support only `.eql(v)` binary equality.
///
/// Comparisons with std.testing:
///
/// ```diff
/// -std.testing.expectEqual(@as(u32, 1337), actual())
/// +mach.testing.expect(u32, 1337).eql(actual())
/// ```
///
/// ```diff
/// -std.testing.expectApproxEqAbs(@as(f32, 1.0), actual(), std.math.floatEps(f32))
/// +mach.testing.expect(f32, 1.0).eql(actual())
/// ```
///
/// ```diff
/// -std.testing.expectApproxEqAbs(@as(f32, 1.0), actual(), 0.1)
/// +mach.testing.expect(f32, 1.0).eqlApprox(actual(), 0.1)
/// ```
///
/// ```diff
/// -std.testing.expectEqual(@as(f32, 1.0), actual())
/// +mach.testing.expect(f32, 1.0).eqlBinary(actual())
/// ```
///
/// ```diff
/// -std.testing.expectEqual(@as([]const u8, byte_array), actual())
/// +mach.testing.expect([]const u8, byte_array).eqlBinary(actual())
/// ```
///
/// ```diff
/// -std.testing.expectEqualStrings("foo", actual())
/// +mach.testing.expect([]const u8, "foo").eql(actual())
/// ```
///
/// Note that std.testing cannot handle @Vector approximate equality at all, while mach.testing uses
/// approx equality of mach.Vec and mach.Mat by default.
pub fn expect(comptime T: type, expected: T) Expect(T) {
return Expect(T){ .expected = expected };
}
test {
testing.refAllDeclsRecursive(Expect(u32));
testing.refAllDeclsRecursive(Expect(f32));
testing.refAllDeclsRecursive(Expect([]const u8));
testing.refAllDeclsRecursive(Expect(@Vector(3, f32)));
testing.refAllDeclsRecursive(Expect(mach.math.Vec2h));
testing.refAllDeclsRecursive(Expect(mach.math.Vec3));
testing.refAllDeclsRecursive(Expect(mach.math.Vec4d));
testing.refAllDeclsRecursive(Expect(mach.math.Ray));
// testing.refAllDeclsRecursive(Expect(mach.math.Mat4h));
// testing.refAllDeclsRecursive(Expect(mach.math.Mat4));
// testing.refAllDeclsRecursive(Expect(mach.math.Mat4d));
}