mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-09 03:16:33 +00:00
Implement and use generic approx equality tester (#8979)
Seems like there needs to be a general, easy-to-use solution for approximate equality testing of containers holding floats (see, e.g., https://github.com/ghostty-org/ghostty/pull/8563#pullrequestreview-3281357931). How's this?
This commit is contained in:
147
src/datastruct/comparison.zig
Normal file
147
src/datastruct/comparison.zig
Normal file
@@ -0,0 +1,147 @@
|
||||
// The contents of this file is largely based on testing.zig from the Zig 0.15.1
|
||||
// stdlib, distributed under the MIT license, copyright (c) Zig contributors
|
||||
const std = @import("std");
|
||||
|
||||
/// Generic, recursive equality testing utility using approximate comparison for
|
||||
/// floats and equality for everything else
|
||||
///
|
||||
/// Based on `std.testing.expectEqual` and `std.testing.expectEqualSlices`.
|
||||
///
|
||||
/// The relative tolerance is currently hardcoded to `sqrt(eps(float_type))`.
|
||||
pub inline fn expectApproxEqual(expected: anytype, actual: anytype) !void {
|
||||
const T = @TypeOf(expected, actual);
|
||||
return expectApproxEqualInner(T, expected, actual);
|
||||
}
|
||||
|
||||
fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void {
|
||||
switch (@typeInfo(T)) {
|
||||
// check approximate equality for floats
|
||||
.float => {
|
||||
const sqrt_eps = comptime std.math.sqrt(std.math.floatEps(T));
|
||||
if (!std.math.approxEqRel(T, expected, actual, sqrt_eps)) {
|
||||
print("expected approximately {any}, found {any}\n", .{ expected, actual });
|
||||
return error.TestExpectedApproxEqual;
|
||||
}
|
||||
},
|
||||
|
||||
// recurse into containers
|
||||
.array => {
|
||||
const diff_index: usize = diff_index: {
|
||||
const shortest = @min(expected.len, actual.len);
|
||||
var index: usize = 0;
|
||||
while (index < shortest) : (index += 1) {
|
||||
expectApproxEqual(actual[index], expected[index]) catch break :diff_index index;
|
||||
}
|
||||
break :diff_index if (expected.len == actual.len) return else shortest;
|
||||
};
|
||||
print("slices not approximately equal. first significant difference occurs at index {d} (0x{X})\n", .{ diff_index, diff_index });
|
||||
return error.TestExpectedApproxEqual;
|
||||
},
|
||||
.vector => |info| {
|
||||
var i: usize = 0;
|
||||
while (i < info.len) : (i += 1) {
|
||||
expectApproxEqual(expected[i], actual[i]) catch {
|
||||
print("index {d} incorrect. expected approximately {any}, found {any}\n", .{
|
||||
i, expected[i], actual[i],
|
||||
});
|
||||
return error.TestExpectedApproxEqual;
|
||||
};
|
||||
}
|
||||
},
|
||||
.@"struct" => |structType| {
|
||||
inline for (structType.fields) |field| {
|
||||
try expectApproxEqual(@field(expected, field.name), @field(actual, field.name));
|
||||
}
|
||||
},
|
||||
|
||||
// unwrap unions, optionals, and error unions
|
||||
.@"union" => |union_info| {
|
||||
if (union_info.tag_type == null) {
|
||||
// untagged unions can only be compared bitwise,
|
||||
// so expectEqual is all we need
|
||||
std.testing.expectEqual(expected, actual) catch {
|
||||
return error.TestExpectedApproxEqual;
|
||||
};
|
||||
}
|
||||
|
||||
const Tag = std.meta.Tag(@TypeOf(expected));
|
||||
|
||||
const expectedTag = @as(Tag, expected);
|
||||
const actualTag = @as(Tag, actual);
|
||||
|
||||
std.testing.expectEqual(expectedTag, actualTag) catch {
|
||||
return error.TestExpectedApproxEqual;
|
||||
};
|
||||
|
||||
// we only reach this switch if the tags are equal
|
||||
switch (expected) {
|
||||
inline else => |val, tag| try expectApproxEqual(val, @field(actual, @tagName(tag))),
|
||||
}
|
||||
},
|
||||
.optional, .error_union => {
|
||||
if (expected) |expected_payload| if (actual) |actual_payload| {
|
||||
return expectApproxEqual(expected_payload, actual_payload);
|
||||
};
|
||||
// we only reach this point if there's at least one null or error,
|
||||
// in which case expectEqual is all we need
|
||||
std.testing.expectEqual(expected, actual) catch {
|
||||
return error.TestExpectedApproxEqual;
|
||||
};
|
||||
},
|
||||
|
||||
// fall back to expectEqual for everything else
|
||||
else => std.testing.expectEqual(expected, actual) catch {
|
||||
return error.TestExpectedApproxEqual;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy of std.testing.print (not public)
|
||||
fn print(comptime fmt: []const u8, args: anytype) void {
|
||||
if (@inComptime()) {
|
||||
@compileError(std.fmt.comptimePrint(fmt, args));
|
||||
} else if (std.testing.backend_can_print) {
|
||||
std.debug.print(fmt, args);
|
||||
}
|
||||
}
|
||||
|
||||
// Tests based on the `expectEqual` tests in the Zig stdlib
|
||||
test "expectApproxEqual.union(enum)" {
|
||||
const T = union(enum) {
|
||||
a: i32,
|
||||
b: f32,
|
||||
};
|
||||
|
||||
const b10 = T{ .b = 10.0 };
|
||||
const b10plus = T{ .b = 10.000001 };
|
||||
|
||||
try expectApproxEqual(b10, b10plus);
|
||||
}
|
||||
|
||||
test "expectApproxEqual nested array" {
|
||||
const a = [2][2]f32{
|
||||
[_]f32{ 1.0, 0.0 },
|
||||
[_]f32{ 0.0, 1.0 },
|
||||
};
|
||||
|
||||
const b = [2][2]f32{
|
||||
[_]f32{ 1.000001, 0.0 },
|
||||
[_]f32{ 0.0, 0.999999 },
|
||||
};
|
||||
|
||||
try expectApproxEqual(a, b);
|
||||
}
|
||||
|
||||
test "expectApproxEqual vector" {
|
||||
const a: @Vector(4, f32) = @splat(4.0);
|
||||
const b: @Vector(4, f32) = @splat(4.000001);
|
||||
|
||||
try expectApproxEqual(a, b);
|
||||
}
|
||||
|
||||
test "expectApproxEqual struct" {
|
||||
const a = .{ 1, @as(f32, 1.0) };
|
||||
const b = .{ 1, @as(f32, 0.999999) };
|
||||
|
||||
try expectApproxEqual(a, b);
|
||||
}
|
@@ -19,6 +19,7 @@ const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const config = @import("../config.zig");
|
||||
const comparison = @import("../datastruct/comparison.zig");
|
||||
const font = @import("main.zig");
|
||||
const options = font.options;
|
||||
const DeferredFace = font.DeferredFace;
|
||||
@@ -1199,7 +1200,7 @@ test "metrics" {
|
||||
|
||||
try c.updateMetrics();
|
||||
|
||||
try std.testing.expectEqual(font.Metrics{
|
||||
try comparison.expectApproxEqual(font.Metrics{
|
||||
.cell_width = 8,
|
||||
// The cell height is 17 px because the calculation is
|
||||
//
|
||||
@@ -1229,12 +1230,12 @@ test "metrics" {
|
||||
.icon_height = 12.24,
|
||||
.face_width = 8.0,
|
||||
.face_height = 16.784,
|
||||
.face_y = @round(3.04) - @as(f64, 3.04), // use f64, not comptime float, for exact match with runtime value
|
||||
.face_y = -0.04,
|
||||
}, c.metrics);
|
||||
|
||||
// Resize should change metrics
|
||||
try c.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 });
|
||||
try std.testing.expectEqual(font.Metrics{
|
||||
try comparison.expectApproxEqual(font.Metrics{
|
||||
.cell_width = 16,
|
||||
.cell_height = 34,
|
||||
.cell_baseline = 6,
|
||||
@@ -1249,7 +1250,7 @@ test "metrics" {
|
||||
.icon_height = 24.48,
|
||||
.face_width = 16.0,
|
||||
.face_height = 33.568,
|
||||
.face_y = @round(6.08) - @as(f64, 6.08), // use f64, not comptime float, for exact match with runtime value
|
||||
.face_y = -0.08,
|
||||
}, c.metrics);
|
||||
}
|
||||
|
||||
@@ -1493,29 +1494,7 @@ test "face metrics" {
|
||||
.{ narrowMetricsExpected, wideMetricsExpected },
|
||||
.{ narrowMetrics, wideMetrics },
|
||||
) |metricsExpected, metricsActual| {
|
||||
inline for (@typeInfo(font.Metrics.FaceMetrics).@"struct".fields) |field| {
|
||||
const expected = @field(metricsExpected, field.name);
|
||||
const actual = @field(metricsActual, field.name);
|
||||
// Unwrap optional fields
|
||||
const expectedValue, const actualValue = unwrap: switch (@typeInfo(field.type)) {
|
||||
.optional => {
|
||||
if (expected) |expectedValue| if (actual) |actualValue| {
|
||||
break :unwrap .{ expectedValue, actualValue };
|
||||
};
|
||||
// Null values can be compared directly
|
||||
try std.testing.expectEqual(expected, actual);
|
||||
continue;
|
||||
},
|
||||
else => break :unwrap .{ expected, actual },
|
||||
};
|
||||
// All non-null values are floats
|
||||
const eps = std.math.floatEps(@TypeOf(actualValue - expectedValue));
|
||||
try std.testing.expectApproxEqRel(
|
||||
expectedValue,
|
||||
actualValue,
|
||||
std.math.sqrt(eps),
|
||||
);
|
||||
}
|
||||
try comparison.expectApproxEqual(metricsExpected, metricsActual);
|
||||
}
|
||||
|
||||
// Verify estimated metrics. icWidth() should equal the smaller of
|
||||
|
Reference in New Issue
Block a user