summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/datastruct/comparison.zig147
-rw-r--r--src/font/Collection.zig33
2 files changed, 153 insertions, 27 deletions
diff --git a/src/datastruct/comparison.zig b/src/datastruct/comparison.zig
new file mode 100644
index 000000000..4427c143c
--- /dev/null
+++ b/src/datastruct/comparison.zig
@@ -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);
+}
diff --git a/src/font/Collection.zig b/src/font/Collection.zig
index 04b9882dc..e91fe03ae 100644
--- a/src/font/Collection.zig
+++ b/src/font/Collection.zig
@@ -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