From ff32289b8f1c4f47d042c8ef499f963eda6984cf Mon Sep 17 00:00:00 2001 From: Pyotr Karbovskiy-Kjellman Date: Mon, 25 May 2026 19:46:49 +0200 Subject: [PATCH] Move fuzz tests into fuzz.zig --- crdt-lib/src/fuzz.zig | 214 +++++++++++++++++++++++++++++++++++++++++ crdt-lib/src/root.zig | 219 +----------------------------------------- 2 files changed, 215 insertions(+), 218 deletions(-) diff --git a/crdt-lib/src/fuzz.zig b/crdt-lib/src/fuzz.zig index 859e492..d557f3a 100644 --- a/crdt-lib/src/fuzz.zig +++ b/crdt-lib/src/fuzz.zig @@ -1,13 +1,19 @@ const std = @import("std"); const crdt = @import("root.zig"); const Crdt = crdt.Crdt; +const Counter = crdt.Counter; +const LwwRegister = crdt.LwwRegister; const ReplicaId = crdt.ReplicaId; const Smith = std.testing.Smith; const Allocator = std.mem.Allocator; +const testing = std.testing; +const Stringify = std.json.Stringify; +const parseFromSlice = std.json.parseFromSlice; const namespace_bits = 2; const Namespace = std.meta.Int(.unsigned, namespace_bits); const NamespacedReplica = std.meta.Int(.unsigned, @bitSizeOf(ReplicaId) - namespace_bits); +const FuzzCtx = struct { alloc: Allocator }; fn namespacedReplica(comptime namespace: Namespace, replica: NamespacedReplica) ReplicaId { return (@as(ReplicaId, replica) << namespace_bits) | namespace; @@ -117,3 +123,211 @@ pub fn gen(comptime C: type, smith: *Smith, alloc: Allocator, comptime namespace } unreachable; } + +fn expectCounterEqual(comptime T: type, a: Counter(T), b: Counter(T)) !void { + try testing.expectEqual(a.counters.count(), b.counters.count()); + var iter = a.counters.iterator(); + while (iter.next()) |entry| { + const got = b.counters.get(entry.key_ptr.*); + try testing.expect(got != null); + try testing.expectEqual(entry.value_ptr.*, got.?); + } +} + +fn expectTagSetEqual(comptime K: type, a: std.hash_map.AutoHashMapUnmanaged(K, void), b: std.hash_map.AutoHashMapUnmanaged(K, void)) !void { + try testing.expectEqual(a.count(), b.count()); + var iter = a.keyIterator(); + while (iter.next()) |key| { + try testing.expect(b.contains(key.*)); + } +} + +fn expectTaggedMapEqual(comptime V: type, comptime K: type, a: std.hash_map.AutoHashMapUnmanaged(V, std.hash_map.AutoHashMapUnmanaged(K, void)), b: std.hash_map.AutoHashMapUnmanaged(V, std.hash_map.AutoHashMapUnmanaged(K, void))) !void { + try testing.expectEqual(a.count(), b.count()); + var iter = a.iterator(); + while (iter.next()) |entry| { + const got = b.get(entry.key_ptr.*); + try testing.expect(got != null); + if (got) |tags| try expectTagSetEqual(K, entry.value_ptr.*, tags); + } +} + +fn expectCrdtEqual(a: *Crdt, b: *Crdt) !void { + try testing.expectEqual(@intFromEnum(a.*), @intFromEnum(b.*)); + switch (a.*) { + .counter => { + try expectCounterEqual(@TypeOf(a.counter).Element, a.counter, b.counter); + }, + .pn_counter => { + try expectCounterEqual(@TypeOf(a.pn_counter.pos).Element, a.pn_counter.pos, b.pn_counter.pos); + try expectCounterEqual(@TypeOf(a.pn_counter.neg).Element, a.pn_counter.neg, b.pn_counter.neg); + }, + .set => { + try testing.expectEqual(a.set.hashset.count(), b.set.hashset.count()); + var iter = a.set.hashset.keyIterator(); + while (iter.next()) |entry| { + try testing.expect(b.set.hashset.contains(entry.*)); + } + }, + .or_set => { + const Element = @TypeOf(a.or_set).Element; + const Tag = @TypeOf(a.or_set).Tag; + try expectTaggedMapEqual(Element, Tag, a.or_set.elements, b.or_set.elements); + try expectTaggedMapEqual(Element, Tag, a.or_set.tombstone, b.or_set.tombstone); + }, + .lww_register => { + try testing.expectEqual(a.lww_register.value, b.lww_register.value); + try testing.expectEqual(a.lww_register.timestamp, b.lww_register.timestamp); + try testing.expectEqual(a.lww_register.replica, b.lww_register.replica); + }, + .mv_register => { + const T = @TypeOf(a.mv_register).ValueTag; + const lessThan = struct { + fn f(_: void, lhs: T, rhs: T) bool { + if (lhs.value != rhs.value) return lhs.value < rhs.value; + if (lhs.tag.replica != rhs.tag.replica) return lhs.tag.replica < rhs.tag.replica; + return lhs.tag.seq < rhs.tag.seq; + } + }.f; + + const a_vals = a.mv_register.values.items; + std.mem.sort(T, a_vals, {}, lessThan); + const b_vals = b.mv_register.values.items; + std.mem.sort(T, b_vals, {}, lessThan); + + try testing.expectEqual(a_vals.len, b_vals.len); + for (a_vals, b_vals) |av, bv| { + try testing.expectEqual(av.value, bv.value); + try testing.expectEqual(av.tag.replica, bv.tag.replica); + try testing.expectEqual(av.tag.seq, bv.tag.seq); + } + }, + .rga => { + try testing.expectEqual(a.rga.nodes.count(), b.rga.nodes.count()); + var iter = a.rga.nodes.iterator(); + while (iter.next()) |entry| { + const got = b.rga.nodes.get(entry.key_ptr.*); + try testing.expect(got != null); + if (got) |node| { + try testing.expectEqual(entry.value_ptr.id.replica, node.id.replica); + try testing.expectEqual(entry.value_ptr.id.seq, node.id.seq); + try testing.expectEqual(entry.value_ptr.origin, node.origin); + try testing.expectEqual(entry.value_ptr.value, node.value); + try testing.expectEqual(entry.value_ptr.removed, node.removed); + } + } + }, + } +} + +fn expectJsonOk(alloc: Allocator, a: *Crdt) !void { + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try Stringify.value(a.*, .{}, &buf.writer); + var parsed = try parseFromSlice(Crdt, alloc, buf.written(), .{}); + defer parsed.deinit(); + try expectCrdtEqual(a, &parsed.value); +} + +fn expectOom(comptime C: type, smith: *Smith, alloc: Allocator) !void { + if (C == LwwRegister(u64)) return; + var a = try gen(C, smith, alloc, 0); + defer a.deinit(alloc); + var buf: std.Io.Writer.Allocating = .init(alloc); + defer buf.deinit(); + try Stringify.value(a, .{}, &buf.writer); + for (0..10) |i| { + var failing = testing.FailingAllocator.init(alloc, .{ .fail_index = i }); + const parsed = parseFromSlice(Crdt, failing.allocator(), buf.written(), .{}); + if (parsed) |*p| { + p.deinit(); + } else |err| { + try testing.expectEqual(error.OutOfMemory, err); + } + } +} + +test "fuzz: all CRDTs merge commutatively" { + try testing.fuzz(FuzzCtx{ .alloc = testing.allocator }, struct { + fn run(ctx: FuzzCtx, smith: *Smith) !void { + inline for (std.meta.fields(Crdt)) |f| { + var a = try gen(f.type, smith, ctx.alloc, 0); + defer a.deinit(ctx.alloc); + var b = try gen(f.type, smith, ctx.alloc, 1); + defer b.deinit(ctx.alloc); + var ab = try a.clone(ctx.alloc); + defer ab.deinit(ctx.alloc); + var ba = try b.clone(ctx.alloc); + defer ba.deinit(ctx.alloc); + try ab.merge(b, ctx.alloc); + try ba.merge(a, ctx.alloc); + try expectCrdtEqual(&ab, &ba); + } + } + }.run, .{}); +} + +test "fuzz: all CRDTs merge idempotently" { + try testing.fuzz(FuzzCtx{ .alloc = testing.allocator }, struct { + fn run(ctx: FuzzCtx, smith: *Smith) !void { + inline for (std.meta.fields(Crdt)) |f| { + var a = try gen(f.type, smith, ctx.alloc, 0); + defer a.deinit(ctx.alloc); + var aa = try a.clone(ctx.alloc); + defer aa.deinit(ctx.alloc); + try aa.merge(a, ctx.alloc); + try expectCrdtEqual(&a, &aa); + } + } + }.run, .{}); +} + +test "fuzz: all CRDTs merge associatively" { + try testing.fuzz(FuzzCtx{ .alloc = testing.allocator }, struct { + fn run(ctx: FuzzCtx, smith: *Smith) !void { + inline for (std.meta.fields(Crdt)) |f| { + var a = try gen(f.type, smith, ctx.alloc, 0); + defer a.deinit(ctx.alloc); + var b = try gen(f.type, smith, ctx.alloc, 1); + defer b.deinit(ctx.alloc); + var c = try gen(f.type, smith, ctx.alloc, 2); + defer c.deinit(ctx.alloc); + var ab = try a.clone(ctx.alloc); + defer ab.deinit(ctx.alloc); + try ab.merge(b, ctx.alloc); + var ab_c = try ab.clone(ctx.alloc); + defer ab_c.deinit(ctx.alloc); + try ab_c.merge(c, ctx.alloc); + var bc = try b.clone(ctx.alloc); + defer bc.deinit(ctx.alloc); + try bc.merge(c, ctx.alloc); + var a_bc = try a.clone(ctx.alloc); + defer a_bc.deinit(ctx.alloc); + try a_bc.merge(bc, ctx.alloc); + try expectCrdtEqual(&ab_c, &a_bc); + } + } + }.run, .{}); +} + +test "fuzz: all CRDTs JSON roundtrip" { + try testing.fuzz(FuzzCtx{ .alloc = testing.allocator }, struct { + fn run(ctx: FuzzCtx, smith: *Smith) !void { + inline for (std.meta.fields(Crdt)) |f| { + var a = try gen(f.type, smith, ctx.alloc, 0); + defer a.deinit(ctx.alloc); + try expectJsonOk(ctx.alloc, &a); + } + } + }.run, .{}); +} + +test "fuzz: all CRDTs OOM" { + try testing.fuzz(FuzzCtx{ .alloc = testing.allocator }, struct { + fn run(ctx: FuzzCtx, smith: *Smith) !void { + inline for (std.meta.fields(Crdt)) |f| { + try expectOom(f.type, smith, ctx.alloc); + } + } + }.run, .{}); +} diff --git a/crdt-lib/src/root.zig b/crdt-lib/src/root.zig index 0d17cb8..7a55386 100644 --- a/crdt-lib/src/root.zig +++ b/crdt-lib/src/root.zig @@ -132,229 +132,12 @@ pub fn freeToken(allocator: Allocator, token: JsonToken) void { } const testing = std.testing; -const Smith = std.testing.Smith; -const Stringify = std.json.Stringify; -const FuzzCtx = struct { alloc: Allocator }; - -const fuzz = @import("fuzz.zig"); +pub const fuzz = @import("fuzz.zig"); test { testing.refAllDecls(@This()); } -fn expectCounterEqual(comptime T: type, a: Counter(T), b: Counter(T)) !void { - try testing.expectEqual(a.counters.count(), b.counters.count()); - var iter = a.counters.iterator(); - while (iter.next()) |entry| { - const got = b.counters.get(entry.key_ptr.*); - try testing.expect(got != null); - try testing.expectEqual(entry.value_ptr.*, got.?); - } -} - -fn expectTagSetEqual(comptime K: type, a: std.hash_map.AutoHashMapUnmanaged(K, void), b: std.hash_map.AutoHashMapUnmanaged(K, void)) !void { - try testing.expectEqual(a.count(), b.count()); - var iter = a.keyIterator(); - while (iter.next()) |key| { - try testing.expect(b.contains(key.*)); - } -} - -fn expectTaggedMapEqual(comptime V: type, comptime K: type, a: std.hash_map.AutoHashMapUnmanaged(V, std.hash_map.AutoHashMapUnmanaged(K, void)), b: std.hash_map.AutoHashMapUnmanaged(V, std.hash_map.AutoHashMapUnmanaged(K, void))) !void { - try testing.expectEqual(a.count(), b.count()); - var iter = a.iterator(); - while (iter.next()) |entry| { - const got = b.get(entry.key_ptr.*); - try testing.expect(got != null); - if (got) |tags| try expectTagSetEqual(K, entry.value_ptr.*, tags); - } -} - -fn expectCrdtEqual(a: *Crdt, b: *Crdt, alloc: Allocator) !void { - _ = alloc; - try testing.expectEqual(@intFromEnum(a.*), @intFromEnum(b.*)); - switch (a.*) { - .counter => { - try expectCounterEqual(@TypeOf(a.counter).Element, a.counter, b.counter); - }, - .pn_counter => { - try expectCounterEqual(@TypeOf(a.pn_counter.pos).Element, a.pn_counter.pos, b.pn_counter.pos); - try expectCounterEqual(@TypeOf(a.pn_counter.neg).Element, a.pn_counter.neg, b.pn_counter.neg); - }, - .set => { - try testing.expectEqual(a.set.hashset.count(), b.set.hashset.count()); - var iter = a.set.hashset.keyIterator(); - while (iter.next()) |entry| { - try testing.expect(b.set.hashset.contains(entry.*)); - } - }, - .or_set => { - const Element = @TypeOf(a.or_set).Element; - const Tag = @TypeOf(a.or_set).Tag; - try expectTaggedMapEqual(Element, Tag, a.or_set.elements, b.or_set.elements); - try expectTaggedMapEqual(Element, Tag, a.or_set.tombstone, b.or_set.tombstone); - }, - .lww_register => { - try testing.expectEqual(a.lww_register.value, b.lww_register.value); - try testing.expectEqual(a.lww_register.timestamp, b.lww_register.timestamp); - try testing.expectEqual(a.lww_register.replica, b.lww_register.replica); - }, - .mv_register => { - const T = @TypeOf(a.mv_register).ValueTag; - const lessThan = struct { - fn f(_: void, lhs: T, rhs: T) bool { - if (lhs.value != rhs.value) return lhs.value < rhs.value; - if (lhs.tag.replica != rhs.tag.replica) return lhs.tag.replica < rhs.tag.replica; - return lhs.tag.seq < rhs.tag.seq; - } - }.f; - - // Sort values as order is not guaranteed by the crdt. - const a_vals = a.mv_register.values.items; - std.mem.sort(T, a_vals, {}, lessThan); - const b_vals = b.mv_register.values.items; - std.mem.sort(T, b_vals, {}, lessThan); - - try testing.expectEqual(a_vals.len, b_vals.len); - for (a_vals, b_vals) |av, bv| { - try testing.expectEqual(av.value, bv.value); - try testing.expectEqual(av.tag.replica, bv.tag.replica); - try testing.expectEqual(av.tag.seq, bv.tag.seq); - } - }, - .rga => { - try testing.expectEqual(a.rga.nodes.count(), b.rga.nodes.count()); - var iter = a.rga.nodes.iterator(); - while (iter.next()) |entry| { - const got = b.rga.nodes.get(entry.key_ptr.*); - try testing.expect(got != null); - if (got) |node| { - try testing.expectEqual(entry.value_ptr.id.replica, node.id.replica); - try testing.expectEqual(entry.value_ptr.id.seq, node.id.seq); - try testing.expectEqual(entry.value_ptr.origin, node.origin); - try testing.expectEqual(entry.value_ptr.value, node.value); - try testing.expectEqual(entry.value_ptr.removed, node.removed); - } - } - }, - } -} - -fn expectJsonOk(alloc: Allocator, a: *Crdt) !void { - var buf: std.Io.Writer.Allocating = .init(alloc); - defer buf.deinit(); - try Stringify.value(a.*, .{}, &buf.writer); - var parsed = try parseFromSlice(Crdt, alloc, buf.written(), .{}); - defer parsed.deinit(); - try expectCrdtEqual(a, &parsed.value, alloc); -} - -fn expectOom(comptime C: type, smith: *Smith, alloc: Allocator) !void { - if (C == LwwRegister(u64)) return; - var a = try fuzz.gen(C, smith, alloc, 0); - defer a.deinit(alloc); - var buf: std.Io.Writer.Allocating = .init(alloc); - defer buf.deinit(); - try Stringify.value(a, .{}, &buf.writer); - for (0..10) |i| { - var failing = testing.FailingAllocator.init(alloc, .{ .fail_index = i }); - const parsed = parseFromSlice(Crdt, failing.allocator(), buf.written(), .{}); - if (parsed) |*p| { - p.deinit(); - } else |err| { - try testing.expectEqual(error.OutOfMemory, err); - } - } -} - -test "fuzz: all CRDTs merge commutatively" { - try testing.fuzz(FuzzCtx{ .alloc = testing.allocator }, struct { - fn run(ctx: FuzzCtx, smith: *Smith) !void { - inline for (std.meta.fields(Crdt)) |f| { - std.debug.print("{s}\n", .{f.name}); - var a = try fuzz.gen(f.type, smith, ctx.alloc, 0); - defer a.deinit(ctx.alloc); - var b = try fuzz.gen(f.type, smith, ctx.alloc, 1); - defer b.deinit(ctx.alloc); - var ab = try a.clone(ctx.alloc); - defer ab.deinit(ctx.alloc); - var ba = try b.clone(ctx.alloc); - defer ba.deinit(ctx.alloc); - try ab.merge(b, ctx.alloc); - try ba.merge(a, ctx.alloc); - // if (std.mem.eql(u8, f.name, "counter")) - // std.debug.print("a: {d}\nb: {d}\nab: {d}\nba: {d}\n\n", .{ a.counter.value(), b.counter.value(), ab.counter.value(), ba.counter.value() }); - try expectCrdtEqual(&ab, &ba, ctx.alloc); - } - } - }.run, .{}); -} - -test "fuzz: all CRDTs merge idempotently" { - try testing.fuzz(FuzzCtx{ .alloc = testing.allocator }, struct { - fn run(ctx: FuzzCtx, smith: *Smith) !void { - inline for (std.meta.fields(Crdt)) |f| { - var a = try fuzz.gen(f.type, smith, ctx.alloc, 0); - defer a.deinit(ctx.alloc); - var aa = try a.clone(ctx.alloc); - defer aa.deinit(ctx.alloc); - try aa.merge(a, ctx.alloc); - try expectCrdtEqual(&a, &aa, ctx.alloc); - } - } - }.run, .{}); -} - -test "fuzz: all CRDTs merge associatively" { - try testing.fuzz(FuzzCtx{ .alloc = testing.allocator }, struct { - fn run(ctx: FuzzCtx, smith: *Smith) !void { - inline for (std.meta.fields(Crdt)) |f| { - var a = try fuzz.gen(f.type, smith, ctx.alloc, 0); - defer a.deinit(ctx.alloc); - var b = try fuzz.gen(f.type, smith, ctx.alloc, 1); - defer b.deinit(ctx.alloc); - var c = try fuzz.gen(f.type, smith, ctx.alloc, 2); - defer c.deinit(ctx.alloc); - var ab = try a.clone(ctx.alloc); - defer ab.deinit(ctx.alloc); - try ab.merge(b, ctx.alloc); - var ab_c = try ab.clone(ctx.alloc); - defer ab_c.deinit(ctx.alloc); - try ab_c.merge(c, ctx.alloc); - var bc = try b.clone(ctx.alloc); - defer bc.deinit(ctx.alloc); - try bc.merge(c, ctx.alloc); - var a_bc = try a.clone(ctx.alloc); - defer a_bc.deinit(ctx.alloc); - try a_bc.merge(bc, ctx.alloc); - try expectCrdtEqual(&ab_c, &a_bc, ctx.alloc); - } - } - }.run, .{}); -} - -test "fuzz: all CRDTs JSON roundtrip" { - try testing.fuzz(FuzzCtx{ .alloc = testing.allocator }, struct { - fn run(ctx: FuzzCtx, smith: *Smith) !void { - inline for (std.meta.fields(Crdt)) |f| { - var a = try fuzz.gen(f.type, smith, ctx.alloc, 0); - defer a.deinit(ctx.alloc); - try expectJsonOk(ctx.alloc, &a); - } - } - }.run, .{}); -} - -test "fuzz: all CRDTs OOM" { - try testing.fuzz(FuzzCtx{ .alloc = testing.allocator }, struct { - fn run(ctx: FuzzCtx, smith: *Smith) !void { - inline for (std.meta.fields(Crdt)) |f| { - try expectOom(f.type, smith, ctx.alloc); - } - } - }.run, .{}); -} - test "Crdt merge type mismatch" { const allocator = std.testing.allocator; var counter = Crdt{ .counter = try Counter(u64).empty.clone(allocator) };