mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-13 19:15:48 +00:00
extract deepEqual
This commit is contained in:
@@ -17,6 +17,7 @@ const assert = @import("../quirks.zig").inlineAssert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const global_state = &@import("../global.zig").state;
|
||||
const deepEqual = @import("../datastruct/comparison.zig").deepEqual;
|
||||
const fontpkg = @import("../font/main.zig");
|
||||
const inputpkg = @import("../input.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
@@ -4120,7 +4121,7 @@ pub fn changeConditionalState(
|
||||
|
||||
// Conditional set contains the keys that this config uses. So we
|
||||
// only continue if we use this key.
|
||||
if (self._conditional_set.contains(key) and !equalField(
|
||||
if (self._conditional_set.contains(key) and !deepEqual(
|
||||
@TypeOf(@field(self._conditional_state, field.name)),
|
||||
@field(self._conditional_state, field.name),
|
||||
@field(new, field.name),
|
||||
@@ -4826,7 +4827,7 @@ pub fn changed(self: *const Config, new: *const Config, comptime key: Key) bool
|
||||
|
||||
const old_value = @field(self, field.name);
|
||||
const new_value = @field(new, field.name);
|
||||
return !equalField(field.type, old_value, new_value);
|
||||
return !deepEqual(field.type, old_value, new_value);
|
||||
}
|
||||
|
||||
/// This yields a key for every changed field between old and new.
|
||||
@@ -4854,91 +4855,6 @@ pub const ChangeIterator = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// A config-specific helper to determine if two values of the same
|
||||
/// type are equal. This isn't the same as std.mem.eql or std.testing.equals
|
||||
/// because we expect structs to implement their own equality.
|
||||
///
|
||||
/// This also doesn't support ALL Zig types, because we only add to it
|
||||
/// as we need types for the config.
|
||||
fn equalField(comptime T: type, old: T, new: T) bool {
|
||||
// Do known named types first
|
||||
switch (T) {
|
||||
inline []const u8,
|
||||
[:0]const u8,
|
||||
=> return std.mem.eql(u8, old, new),
|
||||
|
||||
[]const [:0]const u8,
|
||||
=> {
|
||||
if (old.len != new.len) return false;
|
||||
for (old, new) |a, b| {
|
||||
if (!std.mem.eql(u8, a, b)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
// Back into types of types
|
||||
switch (@typeInfo(T)) {
|
||||
.void => return true,
|
||||
|
||||
inline .bool,
|
||||
.int,
|
||||
.float,
|
||||
.@"enum",
|
||||
=> return old == new,
|
||||
|
||||
.optional => |info| {
|
||||
if (old == null and new == null) return true;
|
||||
if (old == null or new == null) return false;
|
||||
return equalField(info.child, old.?, new.?);
|
||||
},
|
||||
|
||||
.@"struct" => |info| {
|
||||
if (@hasDecl(T, "equal")) return old.equal(new);
|
||||
|
||||
// If a struct doesn't declare an "equal" function, we fall back
|
||||
// to a recursive field-by-field compare.
|
||||
inline for (info.fields) |field_info| {
|
||||
if (!equalField(
|
||||
field_info.type,
|
||||
@field(old, field_info.name),
|
||||
@field(new, field_info.name),
|
||||
)) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
.@"union" => |info| {
|
||||
if (@hasDecl(T, "equal")) return old.equal(new);
|
||||
|
||||
const tag_type = info.tag_type.?;
|
||||
const old_tag = std.meta.activeTag(old);
|
||||
const new_tag = std.meta.activeTag(new);
|
||||
if (old_tag != new_tag) return false;
|
||||
|
||||
inline for (info.fields) |field_info| {
|
||||
if (@field(tag_type, field_info.name) == old_tag) {
|
||||
return equalField(
|
||||
field_info.type,
|
||||
@field(old, field_info.name),
|
||||
@field(new, field_info.name),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unreachable;
|
||||
},
|
||||
|
||||
else => {
|
||||
@compileLog(T);
|
||||
@compileError("unsupported field type");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// This runs a heuristic to determine if we are likely running
|
||||
/// Ghostty in a CLI environment. We need this to change some behaviors.
|
||||
/// We should keep the set of behaviors that depend on this as small
|
||||
@@ -6885,7 +6801,7 @@ pub const Keybinds = struct {
|
||||
const self_leaf = self_entry.value_ptr.*.leaf;
|
||||
const other_leaf = other_entry.value_ptr.*.leaf;
|
||||
|
||||
if (!equalField(
|
||||
if (!deepEqual(
|
||||
inputpkg.Binding.Set.Leaf,
|
||||
self_leaf,
|
||||
other_leaf,
|
||||
@@ -6899,7 +6815,7 @@ pub const Keybinds = struct {
|
||||
if (self_chain.flags != other_chain.flags) return false;
|
||||
if (self_chain.actions.items.len != other_chain.actions.items.len) return false;
|
||||
for (self_chain.actions.items, other_chain.actions.items) |a1, a2| {
|
||||
if (!equalField(
|
||||
if (!deepEqual(
|
||||
inputpkg.Binding.Action,
|
||||
a1,
|
||||
a2,
|
||||
|
||||
@@ -1,11 +1,102 @@
|
||||
// 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");
|
||||
const testing = std.testing;
|
||||
|
||||
/// A deep equality comparison function that works for most types. We
|
||||
/// add types as necessary. It defers to `equal` decls on types that support
|
||||
/// decls.
|
||||
pub fn deepEqual(comptime T: type, old: T, new: T) bool {
|
||||
// Do known named types first
|
||||
switch (T) {
|
||||
inline []const u8,
|
||||
[:0]const u8,
|
||||
=> return std.mem.eql(u8, old, new),
|
||||
|
||||
[]const [:0]const u8,
|
||||
=> {
|
||||
if (old.len != new.len) return false;
|
||||
for (old, new) |a, b| {
|
||||
if (!std.mem.eql(u8, a, b)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
// Back into types of types
|
||||
switch (@typeInfo(T)) {
|
||||
.void => return true,
|
||||
|
||||
inline .bool,
|
||||
.int,
|
||||
.float,
|
||||
.@"enum",
|
||||
=> return old == new,
|
||||
|
||||
.optional => |info| {
|
||||
if (old == null and new == null) return true;
|
||||
if (old == null or new == null) return false;
|
||||
return deepEqual(info.child, old.?, new.?);
|
||||
},
|
||||
|
||||
.array => |info| for (old, new) |old_elem, new_elem| {
|
||||
if (!deepEqual(
|
||||
info.child,
|
||||
old_elem,
|
||||
new_elem,
|
||||
)) return false;
|
||||
} else return true,
|
||||
|
||||
.@"struct" => |info| {
|
||||
if (@hasDecl(T, "equal")) return old.equal(new);
|
||||
|
||||
// If a struct doesn't declare an "equal" function, we fall back
|
||||
// to a recursive field-by-field compare.
|
||||
inline for (info.fields) |field_info| {
|
||||
if (!deepEqual(
|
||||
field_info.type,
|
||||
@field(old, field_info.name),
|
||||
@field(new, field_info.name),
|
||||
)) return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
.@"union" => |info| {
|
||||
if (@hasDecl(T, "equal")) return old.equal(new);
|
||||
|
||||
const tag_type = info.tag_type.?;
|
||||
const old_tag = std.meta.activeTag(old);
|
||||
const new_tag = std.meta.activeTag(new);
|
||||
if (old_tag != new_tag) return false;
|
||||
|
||||
inline for (info.fields) |field_info| {
|
||||
if (@field(tag_type, field_info.name) == old_tag) {
|
||||
return deepEqual(
|
||||
field_info.type,
|
||||
@field(old, field_info.name),
|
||||
@field(new, field_info.name),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
unreachable;
|
||||
},
|
||||
|
||||
else => {
|
||||
@compileLog(T);
|
||||
@compileError("unsupported field type");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic, recursive equality testing utility using approximate comparison for
|
||||
/// floats and equality for everything else
|
||||
///
|
||||
/// Based on `std.testing.expectEqual` and `std.testing.expectEqualSlices`.
|
||||
/// Based on `testing.expectEqual` and `testing.expectEqualSlices`.
|
||||
///
|
||||
/// The relative tolerance is currently hardcoded to `sqrt(eps(float_type))`.
|
||||
pub inline fn expectApproxEqual(expected: anytype, actual: anytype) !void {
|
||||
@@ -59,7 +150,7 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void {
|
||||
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 {
|
||||
testing.expectEqual(expected, actual) catch {
|
||||
return error.TestExpectedApproxEqual;
|
||||
};
|
||||
}
|
||||
@@ -69,7 +160,7 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void {
|
||||
const expectedTag = @as(Tag, expected);
|
||||
const actualTag = @as(Tag, actual);
|
||||
|
||||
std.testing.expectEqual(expectedTag, actualTag) catch {
|
||||
testing.expectEqual(expectedTag, actualTag) catch {
|
||||
return error.TestExpectedApproxEqual;
|
||||
};
|
||||
|
||||
@@ -84,23 +175,23 @@ fn expectApproxEqualInner(comptime T: type, expected: T, actual: T) !void {
|
||||
};
|
||||
// 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 {
|
||||
testing.expectEqual(expected, actual) catch {
|
||||
return error.TestExpectedApproxEqual;
|
||||
};
|
||||
},
|
||||
|
||||
// fall back to expectEqual for everything else
|
||||
else => std.testing.expectEqual(expected, actual) catch {
|
||||
else => testing.expectEqual(expected, actual) catch {
|
||||
return error.TestExpectedApproxEqual;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy of std.testing.print (not public)
|
||||
/// Copy of 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) {
|
||||
} else if (testing.backend_can_print) {
|
||||
std.debug.print(fmt, args);
|
||||
}
|
||||
}
|
||||
@@ -145,3 +236,195 @@ test "expectApproxEqual struct" {
|
||||
|
||||
try expectApproxEqual(a, b);
|
||||
}
|
||||
|
||||
test "deepEqual void" {
|
||||
try testing.expect(deepEqual(void, {}, {}));
|
||||
}
|
||||
|
||||
test "deepEqual bool" {
|
||||
try testing.expect(deepEqual(bool, true, true));
|
||||
try testing.expect(deepEqual(bool, false, false));
|
||||
try testing.expect(!deepEqual(bool, true, false));
|
||||
try testing.expect(!deepEqual(bool, false, true));
|
||||
}
|
||||
|
||||
test "deepEqual int" {
|
||||
try testing.expect(deepEqual(i32, 42, 42));
|
||||
try testing.expect(deepEqual(i32, -100, -100));
|
||||
try testing.expect(!deepEqual(i32, 42, 43));
|
||||
try testing.expect(deepEqual(u64, 0, 0));
|
||||
try testing.expect(!deepEqual(u64, 0, 1));
|
||||
}
|
||||
|
||||
test "deepEqual float" {
|
||||
try testing.expect(deepEqual(f32, 1.0, 1.0));
|
||||
try testing.expect(!deepEqual(f32, 1.0, 1.1));
|
||||
try testing.expect(deepEqual(f64, 3.14159, 3.14159));
|
||||
try testing.expect(!deepEqual(f64, 3.14159, 3.14158));
|
||||
}
|
||||
|
||||
test "deepEqual enum" {
|
||||
const Color = enum { red, green, blue };
|
||||
try testing.expect(deepEqual(Color, .red, .red));
|
||||
try testing.expect(deepEqual(Color, .blue, .blue));
|
||||
try testing.expect(!deepEqual(Color, .red, .green));
|
||||
try testing.expect(!deepEqual(Color, .green, .blue));
|
||||
}
|
||||
|
||||
test "deepEqual []const u8" {
|
||||
try testing.expect(deepEqual([]const u8, "hello", "hello"));
|
||||
try testing.expect(deepEqual([]const u8, "", ""));
|
||||
try testing.expect(!deepEqual([]const u8, "hello", "world"));
|
||||
try testing.expect(!deepEqual([]const u8, "hello", "hell"));
|
||||
try testing.expect(!deepEqual([]const u8, "hello", "hello!"));
|
||||
}
|
||||
|
||||
test "deepEqual [:0]const u8" {
|
||||
try testing.expect(deepEqual([:0]const u8, "foo", "foo"));
|
||||
try testing.expect(!deepEqual([:0]const u8, "foo", "bar"));
|
||||
try testing.expect(!deepEqual([:0]const u8, "foo", "fo"));
|
||||
}
|
||||
|
||||
test "deepEqual []const [:0]const u8" {
|
||||
const a: []const [:0]const u8 = &.{ "one", "two", "three" };
|
||||
const b: []const [:0]const u8 = &.{ "one", "two", "three" };
|
||||
const c: []const [:0]const u8 = &.{ "one", "two" };
|
||||
const d: []const [:0]const u8 = &.{ "one", "two", "four" };
|
||||
const e: []const [:0]const u8 = &.{};
|
||||
|
||||
try testing.expect(deepEqual([]const [:0]const u8, a, b));
|
||||
try testing.expect(!deepEqual([]const [:0]const u8, a, c));
|
||||
try testing.expect(!deepEqual([]const [:0]const u8, a, d));
|
||||
try testing.expect(deepEqual([]const [:0]const u8, e, e));
|
||||
try testing.expect(!deepEqual([]const [:0]const u8, a, e));
|
||||
}
|
||||
|
||||
test "deepEqual optional" {
|
||||
try testing.expect(deepEqual(?i32, null, null));
|
||||
try testing.expect(deepEqual(?i32, 42, 42));
|
||||
try testing.expect(!deepEqual(?i32, null, 42));
|
||||
try testing.expect(!deepEqual(?i32, 42, null));
|
||||
try testing.expect(!deepEqual(?i32, 42, 43));
|
||||
}
|
||||
|
||||
test "deepEqual optional nested" {
|
||||
const Nested = struct { x: i32, y: i32 };
|
||||
try testing.expect(deepEqual(?Nested, null, null));
|
||||
try testing.expect(deepEqual(?Nested, .{ .x = 1, .y = 2 }, .{ .x = 1, .y = 2 }));
|
||||
try testing.expect(!deepEqual(?Nested, .{ .x = 1, .y = 2 }, .{ .x = 1, .y = 3 }));
|
||||
try testing.expect(!deepEqual(?Nested, .{ .x = 1, .y = 2 }, null));
|
||||
}
|
||||
|
||||
test "deepEqual array" {
|
||||
try testing.expect(deepEqual([3]i32, .{ 1, 2, 3 }, .{ 1, 2, 3 }));
|
||||
try testing.expect(!deepEqual([3]i32, .{ 1, 2, 3 }, .{ 1, 2, 4 }));
|
||||
try testing.expect(!deepEqual([3]i32, .{ 1, 2, 3 }, .{ 0, 2, 3 }));
|
||||
try testing.expect(deepEqual([0]i32, .{}, .{}));
|
||||
}
|
||||
|
||||
test "deepEqual nested array" {
|
||||
const a = [2][2]i32{ .{ 1, 2 }, .{ 3, 4 } };
|
||||
const b = [2][2]i32{ .{ 1, 2 }, .{ 3, 4 } };
|
||||
const c = [2][2]i32{ .{ 1, 2 }, .{ 3, 5 } };
|
||||
|
||||
try testing.expect(deepEqual([2][2]i32, a, b));
|
||||
try testing.expect(!deepEqual([2][2]i32, a, c));
|
||||
}
|
||||
|
||||
test "deepEqual struct" {
|
||||
const Point = struct { x: i32, y: i32 };
|
||||
try testing.expect(deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 10, .y = 20 }));
|
||||
try testing.expect(!deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 10, .y = 21 }));
|
||||
try testing.expect(!deepEqual(Point, .{ .x = 10, .y = 20 }, .{ .x = 11, .y = 20 }));
|
||||
}
|
||||
|
||||
test "deepEqual struct nested" {
|
||||
const Inner = struct { value: i32 };
|
||||
const Outer = struct { a: Inner, b: Inner };
|
||||
|
||||
const x = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 2 } };
|
||||
const y = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 2 } };
|
||||
const z = Outer{ .a = .{ .value = 1 }, .b = .{ .value = 3 } };
|
||||
|
||||
try testing.expect(deepEqual(Outer, x, y));
|
||||
try testing.expect(!deepEqual(Outer, x, z));
|
||||
}
|
||||
|
||||
test "deepEqual struct with equal decl" {
|
||||
const Custom = struct {
|
||||
value: i32,
|
||||
|
||||
pub fn equal(self: @This(), other: @This()) bool {
|
||||
return @mod(self.value, 10) == @mod(other.value, 10);
|
||||
}
|
||||
};
|
||||
|
||||
try testing.expect(deepEqual(Custom, .{ .value = 5 }, .{ .value = 15 }));
|
||||
try testing.expect(deepEqual(Custom, .{ .value = 100 }, .{ .value = 0 }));
|
||||
try testing.expect(!deepEqual(Custom, .{ .value = 5 }, .{ .value = 6 }));
|
||||
}
|
||||
|
||||
test "deepEqual union" {
|
||||
const Value = union(enum) {
|
||||
int: i32,
|
||||
float: f32,
|
||||
none,
|
||||
};
|
||||
|
||||
try testing.expect(deepEqual(Value, .{ .int = 42 }, .{ .int = 42 }));
|
||||
try testing.expect(!deepEqual(Value, .{ .int = 42 }, .{ .int = 43 }));
|
||||
try testing.expect(!deepEqual(Value, .{ .int = 42 }, .{ .float = 42.0 }));
|
||||
try testing.expect(deepEqual(Value, .none, .none));
|
||||
try testing.expect(!deepEqual(Value, .none, .{ .int = 0 }));
|
||||
}
|
||||
|
||||
test "deepEqual union with equal decl" {
|
||||
const Value = union(enum) {
|
||||
num: i32,
|
||||
str: []const u8,
|
||||
|
||||
pub fn equal(self: @This(), other: @This()) bool {
|
||||
return switch (self) {
|
||||
.num => |n| switch (other) {
|
||||
.num => |m| @mod(n, 10) == @mod(m, 10),
|
||||
else => false,
|
||||
},
|
||||
.str => |s| switch (other) {
|
||||
.str => |t| s.len == t.len,
|
||||
else => false,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
try testing.expect(deepEqual(Value, .{ .num = 5 }, .{ .num = 25 }));
|
||||
try testing.expect(!deepEqual(Value, .{ .num = 5 }, .{ .num = 6 }));
|
||||
try testing.expect(deepEqual(Value, .{ .str = "abc" }, .{ .str = "xyz" }));
|
||||
try testing.expect(!deepEqual(Value, .{ .str = "abc" }, .{ .str = "ab" }));
|
||||
}
|
||||
|
||||
test "deepEqual array of structs" {
|
||||
const Item = struct { id: i32, name: []const u8 };
|
||||
const a = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "two" } };
|
||||
const b = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "two" } };
|
||||
const c = [2]Item{ .{ .id = 1, .name = "one" }, .{ .id = 2, .name = "TWO" } };
|
||||
|
||||
try testing.expect(deepEqual([2]Item, a, b));
|
||||
try testing.expect(!deepEqual([2]Item, a, c));
|
||||
}
|
||||
|
||||
test "deepEqual struct with optional field" {
|
||||
const Config = struct { name: []const u8, port: ?u16 };
|
||||
|
||||
try testing.expect(deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = 8080 }));
|
||||
try testing.expect(deepEqual(Config, .{ .name = "app", .port = null }, .{ .name = "app", .port = null }));
|
||||
try testing.expect(!deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = null }));
|
||||
try testing.expect(!deepEqual(Config, .{ .name = "app", .port = 8080 }, .{ .name = "app", .port = 8081 }));
|
||||
}
|
||||
|
||||
test "deepEqual struct with array field" {
|
||||
const Data = struct { values: [3]i32 };
|
||||
|
||||
try testing.expect(deepEqual(Data, .{ .values = .{ 1, 2, 3 } }, .{ .values = .{ 1, 2, 3 } }));
|
||||
try testing.expect(!deepEqual(Data, .{ .values = .{ 1, 2, 3 } }, .{ .values = .{ 1, 2, 4 } }));
|
||||
}
|
||||
|
||||
@@ -19,4 +19,6 @@ pub const SplitTree = split_tree.SplitTree;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
||||
_ = @import("comparison.zig");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user