Artifact 11022df8a07c00113c94b4b4695060b2487c7a4623562d9895e616759fc6d1fc:
- File
src/logstore_fuzz.zig
— part of check-in
[d859c6a9a5]
at
2026-07-17 22:05:15
on branch storage
— feat(§5): pickBase deep module + putArtifact lineage+chrono-window wiring with <75% accept threshold
fossil-scm-clone: zit=117528088B fossil=72245248B ratio=1.6268 [zlib 9647 frames / 92,073,728 B (78.3%, avg 9544 B); delta 57852 frames / 25,454,120 B (21.7%, avg 439 B); config 2 frames / 176 B; raw 0 frames. Header+padding overhead 3,711,845 B (3.2%). Exact accounting: 64 + zlib + delta + config == 117,528,088. Artifact types: checkin/manifest 20848 / 26,721,968 B (22.7%), other 6929 / 4,835,264 B (4.1%), file-blob 39722 / 85,970,616 B (73.1%).] | sqlite: zit=207344544B fossil=147709952B ratio=1.4037 [zlib 10847 frames / 140,422,768 B (67.7%, avg 12945 B); delta 120147 frames / 66,921,536 B (32.3%, avg 556 B); config 2 frames / 176 B; raw 0 frames. Header+padding overhead 7,212,808 B (3.5%). Exact accounting: 64 + zlib + delta + config == 207,344,544. Artifact types: checkin/manifest 36515 / 81,006,048 B (39.1%), other 4775 / 5,531,960 B (2.7%), file-blob 89704 / 120,806,296 B (58.3%).]
vs baseline: fossil-scm-clone: zit=117528088B fossil=72245248B ratio=1.627 [raw=0B/0f; zlib=92073728B/9647f (78.3%); delta=25454120B/57852f (21.7%); config=176B/2f. Header+padding overhead 3711845B (3.2%). Artifact-type: checkin/manifest 26721968B/20848, other 4835264B/6929, file-blob 85970616B/39722. Accounting exact.] | sqlite: zit=207344544B fossil=147709952B ratio=1.404 [raw=0B/0f; zlib=140422768B/10847f (67.7%); delta=66921536B/120147f (32.3%); config=176B/2f. Header+padding overhead 7212808B (3.5%). Artifact-type: checkin/manifest 81006048B/36515, other 5531960B/4775, file-blob 120806296B/89704. Accounting exact.] (user: dmestas, size: 12470) [annotate] [blame] [check-ins using]
//! Seeded, parameterized fuzz targets for the framed, content-addressed log //! (storage-track-plan.md §C/§E) over a deterministic in-memory backend. Each //! target takes its seed and round count from the caller (`zig build fuzz -- //! <target> [--seed N] [--rounds N]`); a fixed-seed smoke `test` per target //! (below) delegates to the same pub fn so the same invariant also runs under //! `zig build test`. Registered in fuzz_tests.zig. //! //! The disk is a zero-fault sim/FaultyFile (the healthy-disk default config, so //! nothing tears or flips), which makes the round-trip invariants deterministic //! and filesystem-free. delta is reached through the public `zit` module: a //! relative import would pull delta.zig into this compilation root a second //! time, colliding with the copy logstore.zig already holds. const std = @import("std"); const logstore = @import("zit").logstore; const faulty = @import("sim/faulty_file.zig"); const delta = @import("zit").delta; /// A fresh in-memory backend with the healthy-disk default config — the /// precondition every round-trip below depends on. fn healthyFile(gpa: std.mem.Allocator, rand: std.Random) faulty.FaultyFile { const file = faulty.FaultyFile.init(gpa, rand); std.debug.assert(std.meta.eql(file.cfg, faulty.FaultCfg{})); return file; } /// A random `min_hash_len..max_hash_len` byte hash into `buf`. Hashes are /// opaque here (never verified against content), so arbitrary bytes are fine. fn randomHash(rand: std.Random, buf: *[logstore.max_hash_len]u8) []const u8 { const span = logstore.max_hash_len - logstore.min_hash_len + 1; const len = logstore.min_hash_len + rand.uintLessThan(usize, span); for (buf[0..len]) |*b| b.* = rand.int(u8); return buf[0..len]; } /// `open` never crashes on garbage appended after a valid header: it recovers a /// store or fails with one of its declared errors (storage-track-plan.md §C /// torn-tail recovery). pub fn openGarbage(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void { var prng = std.Random.DefaultPrng.init(seed); const rand = prng.random(); var round: usize = 0; while (round < rounds) : (round += 1) { var backend = healthyFile(gpa, rand); defer backend.deinit(); const file = backend.storageFile(); var seeded = try logstore.LogStore.open(gpa, file, 0); seeded.deinit(gpa); var garbage: [256]u8 = undefined; const n = rand.uintLessThan(usize, garbage.len + 1); rand.bytes(garbage[0..n]); try file.append(garbage[0..n]); if (logstore.LogStore.open(gpa, file, 0)) |store| { var opened = store; opened.deinit(gpa); } else |err| switch (err) { error.OutOfMemory, error.Storage, error.Corrupt => {}, } } } /// §C/sync §4.1 — putZlib then content round-trips the uncompressed bytes: the /// zlib container the store writes inflates back to exactly what went in. pub fn zlibRoundTrip(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void { var prng = std.Random.DefaultPrng.init(seed); const rand = prng.random(); const max_raw: usize = 128; var hash_buf: [logstore.max_hash_len]u8 = undefined; var raw_buf: [max_raw]u8 = undefined; var round: usize = 0; while (round < rounds) : (round += 1) { var backend = healthyFile(gpa, rand); defer backend.deinit(); var store = try logstore.LogStore.open(gpa, backend.storageFile(), 0); defer store.deinit(gpa); const hash = randomHash(rand, &hash_buf); const rlen = rand.uintLessThan(usize, max_raw + 1); for (raw_buf[0..rlen]) |*b| b.* = rand.int(u8); const raw = raw_buf[0..rlen]; const rid = try store.putZlib(hash, raw); const got = (try store.content(gpa, rid)).?; defer gpa.free(got); try std.testing.expectEqualSlices(u8, raw, got); } } /// §C/delta §4 — putDelta then content applies the delta and round-trips the /// target: a delta frame reconstructs its target by walking to the base frame /// and applying via delta.apply. pub fn deltaRoundTrip(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void { var prng = std.Random.DefaultPrng.init(seed); const rand = prng.random(); const max_len: usize = 128; var base_buf: [max_len]u8 = undefined; var target_buf: [max_len]u8 = undefined; var base_hash: [20]u8 = undefined; var target_hash: [20]u8 = undefined; var round: usize = 0; while (round < rounds) : (round += 1) { var backend = healthyFile(gpa, rand); defer backend.deinit(); var store = try logstore.LogStore.open(gpa, backend.storageFile(), 0); defer store.deinit(gpa); const blen = 1 + rand.uintLessThan(usize, max_len); for (base_buf[0..blen]) |*b| b.* = rand.int(u8); const base = base_buf[0..blen]; const tlen = 1 + rand.uintLessThan(usize, max_len); for (target_buf[0..tlen]) |*b| b.* = rand.int(u8); const target = target_buf[0..tlen]; // Distinct hashes: put keys on the hash, so a base/target collision // would alias the two frames and defeat the round-trip. for (&base_hash) |*b| b.* = rand.int(u8); for (&target_hash) |*b| b.* = rand.int(u8); base_hash[0] = 0; target_hash[0] = 1; const d = try delta.create(gpa, base, target); defer gpa.free(d); const base_rid = try store.put(&base_hash, base); const target_rid = try store.putDelta(&target_hash, base_rid, d); const got = (try store.content(gpa, target_rid)).?; defer gpa.free(got); try std.testing.expectEqualSlices(u8, target, got); } } /// §5 — pickBase/putArtifact over random content and random candidate sets /// preserves the invariant "storage is a cache of the log": whatever base /// policy chooses, `content` returns the exact bytes that went in. And any /// delta pickBase accepts both reconstructs its target and clears the 75% /// accept bar, against an eligible (non-phantom, under-cap) base. pub fn pickBaseRoundTrip(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void { var prng = std.Random.DefaultPrng.init(seed); const rand = prng.random(); const max_len: usize = 160; var buf: [max_len]u8 = undefined; var hash_buf: [logstore.max_hash_len]u8 = undefined; var thash_buf: [logstore.max_hash_len]u8 = undefined; var round: usize = 0; while (round < rounds) : (round += 1) { var backend = healthyFile(gpa, rand); defer backend.deinit(); var store = try logstore.LogStore.open(gpa, backend.storageFile(), 0); defer store.deinit(gpa); // Seed a handful of content frames — some sharing a long prefix so // profitable deltas exist, some pure noise — each put through the full // best-of-N write path with a random (possibly ineligible) base hint. // Every stored frame must round-trip regardless of the chosen encoding. const seed_frames = 1 + rand.uintLessThan(usize, 8); var rids: [8]logstore.Rid = undefined; var nr: usize = 0; while (nr < seed_frames) : (nr += 1) { const len = 1 + rand.uintLessThan(usize, max_len); for (buf[0..len]) |*b| b.* = rand.int(u8); if (nr > 0 and rand.boolean()) @memset(buf[0..@min(len, 48)], 0x5A); const hash = randomHash(rand, &hash_buf); // Content addressing: distinct content ⇒ distinct hash. `randomHash` // can emit a 1-byte hash that collides across seed frames, which // would (correctly) hit `putArtifact`'s idempotent short-circuit and // alias two different contents to one rid — a harness artifact, not a // store bug (real hashes never collide). Stamp the frame index into // byte 0 (`seed_frames <= 8`, and `hash.len >= 1`) so every seed // frame is a distinct key, mirroring the deltaRoundTrip guard above. hash_buf[0] = @intCast(nr); const hint: ?logstore.Rid = if (nr > 0 and rand.boolean()) rids[rand.uintLessThan(usize, nr)] else null; const rid = try store.putArtifact(hash, buf[0..len], hint); const got = (try store.content(gpa, rid)).?; defer gpa.free(got); try std.testing.expectEqualSlices(u8, buf[0..len], got); rids[nr] = rid; } // Now probe pickBase directly with a random candidate multiset (may // over-offer past max_base_candidates, repeat rids, or be empty). var cands: [12]logstore.Rid = undefined; const cn = rand.uintLessThan(usize, @min(nr, cands.len) + 1); for (cands[0..cn]) |*c| c.* = rids[rand.uintLessThan(usize, nr)]; const tlen = 1 + rand.uintLessThan(usize, max_len); for (buf[0..tlen]) |*b| b.* = rand.int(u8); if (rand.boolean()) @memset(buf[0..@min(tlen, 48)], 0x5A); const target = buf[0..tlen]; const thash = randomHash(rand, &thash_buf); if (try store.pickBase(gpa, thash, target, cands[0..cn])) |pick| { defer gpa.free(pick.delta); // Accepted ⇒ strictly under 75% of the uncompressed artifact. try std.testing.expect(@as(u64, pick.delta.len) * 4 < @as(u64, target.len) * 3); // Chosen base is eligible: content-bearing and under the write cap. try std.testing.expect(!store.isPhantom(pick.base)); try std.testing.expect(store.depthOf(pick.base) < logstore.delta_chain_write_max); // …and the delta reconstructs the target byte-for-byte. const base_bytes = (try store.content(gpa, pick.base)).?; defer gpa.free(base_bytes); const rebuilt = try delta.apply(gpa, base_bytes, pick.delta); defer gpa.free(rebuilt); try std.testing.expectEqualSlices(u8, target, rebuilt); } } } /// §E — putConfig then readConfig round-trips hash_policy, branch, and project /// id. readConfig drives the file-private parseConfig; config strings are /// length-prefixed, so arbitrary bytes survive the round-trip. pub fn parseConfig(gpa: std.mem.Allocator, seed: u64, rounds: usize) !void { var prng = std.Random.DefaultPrng.init(seed); const rand = prng.random(); const max_str: usize = 32; // <= logstore.max_config_str var branch_buf: [max_str]u8 = undefined; var pid_buf: [max_str]u8 = undefined; var round: usize = 0; while (round < rounds) : (round += 1) { var backend = healthyFile(gpa, rand); defer backend.deinit(); var store = try logstore.LogStore.open(gpa, backend.storageFile(), 0); defer store.deinit(gpa); const blen = rand.uintLessThan(usize, max_str + 1); for (branch_buf[0..blen]) |*b| b.* = rand.int(u8); const plen = rand.uintLessThan(usize, max_str + 1); for (pid_buf[0..plen]) |*b| b.* = rand.int(u8); const cfg: logstore.Config = .{ .hash_policy = rand.int(u8), .default_branch = branch_buf[0..blen], .project_id = pid_buf[0..plen], }; try store.putConfig(cfg); var oc = (try store.readConfig(gpa)).?; defer oc.deinit(gpa); try std.testing.expectEqual(cfg.hash_policy, oc.config.hash_policy); try std.testing.expectEqualSlices(u8, cfg.default_branch, oc.config.default_branch); try std.testing.expectEqualSlices(u8, cfg.project_id, oc.config.project_id); } } // Fixed-seed smoke tests: each delegates to its pub fn at a small round count so // the invariant runs under `zig build test`; deep runs pass `--rounds`. test "logstore fuzz: open never crashes on trailing garbage (smoke)" { try openGarbage(std.testing.allocator, 0xC0_FFEE, 32); } test "logstore §C/sync §4.1 fuzz: putZlib content round-trip (smoke)" { try zlibRoundTrip(std.testing.allocator, 0x21B_2117, 32); } test "logstore §C/delta §4 fuzz: putDelta content round-trip (smoke)" { try deltaRoundTrip(std.testing.allocator, 0xDE17A_04, 32); } test "logstore §5 fuzz: pickBase/putArtifact content round-trip + accept invariants (smoke)" { try pickBaseRoundTrip(std.testing.allocator, 0xB43E_5EED, 64); } test "logstore §E fuzz: putConfig/readConfig round-trip (smoke)" { try parseConfig(std.testing.allocator, 0xC0_F1_60, 32); }