Skip to content

Commit

Permalink
Move fuzz tests into fuzz.zig
Browse files Browse the repository at this point in the history
  • Loading branch information
pyotrk committed May 25, 2026
1 parent 18007e5 commit ff32289
Show file tree
Hide file tree
Showing 2 changed files with 215 additions and 218 deletions.
214 changes: 214 additions & 0 deletions crdt-lib/src/fuzz.zig
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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, .{});
}
Loading

0 comments on commit ff32289

Please sign in to comment.