//! Unit tests for the framed, content-addressed log (storage-track-plan.md §C/§E).
//!
//! Separated from the implementation per the kind-separation convention
//! (mirrors the sibling `*_fuzz.zig` split). Every test here exercises only
//! `logstore`'s public surface; tests reaching file-private decls stay inline
//! in `logstore.zig`.
const std = @import("std");
const logstore = @import("logstore.zig");
const storage = @import("storage.zig");
// delta is part of the public `zit` module; reach it the same way logstore.zig
// does (a relative `@import("delta.zig")` would pull delta.zig into this
// compilation root as well, colliding with the copy in the `zit` module).
const delta = @import("zit").delta;
fn openTmp(tmp: anytype, io: std.Io, name: []const u8) !storage.FileStorage {
const raw = try tmp.dir.createFile(io, name, .{ .read = true });
return storage.FileStorage.init(io, raw, 0);
}
fn writeRawHeader(file: storage.StorageFile, magic_bytes: []const u8, ver: u16, hk: u8, bad_crc: bool) !void {
std.debug.assert(magic_bytes.len == 4);
var buf: [logstore.header_len]u8 = [_]u8{0} ** logstore.header_len;
@memcpy(buf[0..4], magic_bytes);
std.mem.writeInt(u16, buf[4..6], ver, .little);
buf[6] = hk;
var crc = std.hash.Crc32.init();
crc.update(buf[0..60]);
var crc_val = crc.final();
if (bad_crc) crc_val ^= 1;
std.mem.writeInt(u32, buf[60..64], crc_val, .little);
try file.append(&buf);
}
test "logstore §C: open rejects a bad magic" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
try writeRawHeader(file, "XXXX", logstore.version, 0, false);
try std.testing.expectError(error.Corrupt, logstore.LogStore.open(std.testing.allocator, file, 0));
}
test "logstore §C: open re-initializes a torn file header instead of rejecting it" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
try file.append(&[_]u8{ 'z', 'i', 't' }); // 3 of 64 header bytes: nowhere near complete
var store = try logstore.LogStore.open(std.testing.allocator, file, 3);
defer store.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(u64, logstore.header_len), store.size());
try std.testing.expectEqual(@as(u8, 3), store.hash_kind);
}
test "logstore §C: put then content round-trips a raw frame" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
const hash = "h" ** 32;
const rid = try store.put(hash, "hello world");
try std.testing.expectEqualStrings(hash, store.hashOf(rid));
const got = (try store.content(std.testing.allocator, rid)).?;
defer std.testing.allocator.free(got);
try std.testing.expectEqualStrings("hello world", got);
}
test "logstore §C: a written frame occupies an 8-byte-aligned span" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
_ = try store.put("abcde", "xyz"); // unpadded = 26+5+3 = 34, align8 -> 40
try std.testing.expectEqual(@as(u64, 0), store.size() % 8);
try std.testing.expectEqual(logstore.header_len + 40, store.size());
}
test "logstore §C: content hash is the uncompressed address; hashOf returns it" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
const h1 = "1" ** 16;
const h2 = "2" ** 20;
const r1 = try store.put(h1, "aaa");
const r2 = try store.put(h2, "bbbbbb");
try std.testing.expectEqualStrings(h1, store.hashOf(r1));
try std.testing.expectEqualStrings(h2, store.hashOf(r2));
const c1 = (try store.content(std.testing.allocator, r1)).?;
defer std.testing.allocator.free(c1);
try std.testing.expectEqualStrings("aaa", c1);
}
test "logstore §C: put is idempotent for an equal hash and appends nothing" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
const hash = "dup" ** 8;
const r1 = try store.put(hash, "payload-a");
const size_after_first = store.size();
const r2 = try store.put(hash, "payload-a");
try std.testing.expectEqual(r1, r2);
try std.testing.expectEqual(size_after_first, store.size());
}
test "logstore §C: intern yields a phantom; content is null and no frame is written" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
const hash = "phantom-hash-example-value";
const size_before = store.size();
const rid = try store.intern(hash);
try std.testing.expectEqual(size_before, store.size());
const got = try store.content(std.testing.allocator, rid);
try std.testing.expectEqual(@as(?[]u8, null), got);
}
test "logstore §C: put upgrades an interned phantom in place, same rid, content now present" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
// intern fixes the rid with no frame; content is null while phantom.
const h = "phantom-upgrade-hash-value-x";
const rid = try store.intern(h);
try std.testing.expectEqual(@as(?[]u8, null), try store.content(std.testing.allocator, rid));
const size0 = store.size();
// put on the same hash upgrades the phantom in place: same rid, and it
// appends the first frame (a phantom had none), so the log grows.
const rid2 = try store.put(h, "content-arrived");
try std.testing.expectEqual(rid, rid2);
try std.testing.expect(store.size() > size0);
{
const got = (try store.content(std.testing.allocator, rid)).?;
defer std.testing.allocator.free(got);
try std.testing.expectEqualStrings("content-arrived", got);
}
try std.testing.expectEqualStrings(h, store.hashOf(rid));
// The upgraded frame is real on disk, not just an in-memory flag: commit,
// reopen, and the hash resolves to content-bearing bytes (§B inv1 for a
// put that began life as a phantom).
try store.commit();
store.deinit(std.testing.allocator);
var reopened = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer reopened.deinit(std.testing.allocator);
const rrid = reopened.lookup(h) orelse return error.TestUnexpectedResult;
const got2 = (try reopened.content(std.testing.allocator, rrid)).?;
defer std.testing.allocator.free(got2);
try std.testing.expectEqualStrings("content-arrived", got2);
try std.testing.expectEqualStrings(h, reopened.hashOf(rrid));
}
test "logstore §C(inv1): every committed put is readable and hash-exact after reopen" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
const Item = struct { hash: []const u8, bytes: []const u8 };
const items = [_]Item{
.{ .hash = "h-one" ** 4, .bytes = "one" },
.{ .hash = "h-two" ** 4, .bytes = "two-payload" },
.{ .hash = "h-three" ** 3, .bytes = "three!" },
};
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
for (items) |it| _ = try store.put(it.hash, it.bytes);
try store.commit();
store.deinit(std.testing.allocator);
var reopened = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer reopened.deinit(std.testing.allocator);
for (items) |it| {
const rid = reopened.lookup(it.hash) orelse return error.TestUnexpectedResult;
try std.testing.expectEqualStrings(it.hash, reopened.hashOf(rid));
const got = (try reopened.content(std.testing.allocator, rid)).?;
defer std.testing.allocator.free(got);
try std.testing.expectEqualStrings(it.bytes, got);
}
}
test "logstore §C(inv3): the index rebuilt from the log equals the written model" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
const hashes = [_][]const u8{ "r1" ** 8, "r2" ** 8, "r3" ** 8, "r4" ** 8 };
const bodies = [_][]const u8{ "b1", "bb2", "bbb3", "bbbb4" };
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
for (hashes, bodies) |h, b| _ = try store.put(h, b);
try store.commit();
store.deinit(std.testing.allocator);
var reopened = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer reopened.deinit(std.testing.allocator);
try std.testing.expectEqual(hashes.len, reopened.entries.items.len);
for (hashes, bodies) |h, b| {
const rid = reopened.lookup(h).?;
const got = (try reopened.content(std.testing.allocator, rid)).?;
defer std.testing.allocator.free(got);
try std.testing.expectEqualStrings(b, got);
}
}
test "logstore §C: phantom hash is absent after reopen" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
const hash = "phantom-only-hash-value";
_ = try store.intern(hash);
try store.commit();
store.deinit(std.testing.allocator);
var reopened = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer reopened.deinit(std.testing.allocator);
try std.testing.expect(reopened.lookup(hash) == null);
}
fn openNoLeak(gpa: std.mem.Allocator, file: storage.StorageFile) !void {
var store = try logstore.LogStore.open(gpa, file, 0);
store.deinit(gpa);
}
test "logstore §C: open propagates allocation failure" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var seed = try logstore.LogStore.open(std.testing.allocator, file, 0);
_ = try seed.put("seed-hash-one-value", "seed payload one");
_ = try seed.put("seed-hash-two-value", "seed payload two, a bit longer");
try seed.commit();
seed.deinit(std.testing.allocator);
try std.testing.checkAllAllocationFailures(std.testing.allocator, openNoLeak, .{file});
}
fn putNoLeak(gpa: std.mem.Allocator, file: storage.StorageFile) !void {
var store = try logstore.LogStore.open(gpa, file, 0);
defer store.deinit(gpa);
const size = file.size() catch 0;
var hash_buf: [8]u8 = undefined;
std.mem.writeInt(u64, &hash_buf, size, .little);
_ = try store.put(&hash_buf, "payload bytes exercised under simulated allocation failure");
}
test "logstore §C: put propagates allocation failure" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var seed = try logstore.LogStore.open(std.testing.allocator, file, 0);
seed.deinit(std.testing.allocator); // header only: put() always takes the new-entry path
try std.testing.checkAllAllocationFailures(std.testing.allocator, putNoLeak, .{file});
}
fn putPhantomUpgradeNoLeak(gpa: std.mem.Allocator, file: storage.StorageFile) !void {
var store = try logstore.LogStore.open(gpa, file, 0);
defer store.deinit(gpa);
// Hash off the current file size so every run targets a fresh hash: intern
// makes a live phantom this run, then put drives the phantom-upgrade branch
// (a fixed hash would already be content-bearing on disk after the counting
// run's append, sending put down the idempotent path instead).
const size = file.size() catch 0;
var hash_buf: [8]u8 = undefined;
std.mem.writeInt(u64, &hash_buf, size, .little);
_ = try store.intern(&hash_buf);
_ = try store.put(&hash_buf, "payload upgrading a phantom under simulated allocation failure");
}
test "logstore §C: put upgrading a phantom propagates allocation failure" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var seed = try logstore.LogStore.open(std.testing.allocator, file, 0);
seed.deinit(std.testing.allocator); // header only
// On OOM in the upgrade's frame append the entry stays phantom (its fields
// flip only after a successful append), so there is no leak and no
// half-upgraded state at any fail point.
try std.testing.checkAllAllocationFailures(std.testing.allocator, putPhantomUpgradeNoLeak, .{file});
}
fn contentNoLeak(gpa: std.mem.Allocator, file: storage.StorageFile) !void {
var store = try logstore.LogStore.open(gpa, file, 0);
defer store.deinit(gpa);
const bytes = try store.content(gpa, 1);
if (bytes) |b| gpa.free(b);
}
test "logstore §C: content propagates allocation failure" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var seed = try logstore.LogStore.open(std.testing.allocator, file, 0);
_ = try seed.put("content-oom-test-hash", "payload bytes for the content allocation-failure test");
try seed.commit();
seed.deinit(std.testing.allocator);
try std.testing.checkAllAllocationFailures(std.testing.allocator, contentNoLeak, .{file});
}
test "logstore §C/sync §4.1: putZlib then content round-trips the uncompressed bytes" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
const hash = "zlib-round-trip-hash-value-0001";
const raw = "compress me: aaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbb aaaaaaaa";
const rid = try store.putZlib(hash, raw);
const got = (try store.content(std.testing.allocator, rid)).?;
defer std.testing.allocator.free(got);
try std.testing.expectEqualStrings(raw, got);
}
test "logstore §C/sync §4.1: a zlib frame payload is 4-byte-BE-uncompressed-length + RFC1950 stream" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
const hash = "z" ** 20;
const raw = "the payload whose uncompressed length the container declares";
_ = try store.putZlib(hash, raw);
// First frame sits at header_len; its payload starts after the 22+hash_len
// fixed header window (encodeFrame layout).
const poff: u64 = logstore.header_len + 22 + hash.len;
var head: [5]u8 = undefined;
const n = try file.pread(poff, &head);
try std.testing.expectEqual(@as(usize, 5), n);
// §4.1 L1194: 4-byte big-endian uncompressed length.
try std.testing.expectEqual(@as(u32, raw.len), std.mem.readInt(u32, head[0..4], .big));
// RFC1950 §2.2: the CMF byte's low nibble (CM) is 8 for a DEFLATE stream.
try std.testing.expectEqual(@as(u8, 8), head[4] & 0x0f);
}
test "logstore §C: a zlib frame is addressed by its uncompressed hash" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
const hash = "uncompressed-address-hash-00001";
const rid = try store.putZlib(hash, "content that gets compressed on the way to disk");
// The hash names the uncompressed bytes; storage compression must not
// change the address (§4.1 L1206-1208).
try std.testing.expectEqualStrings(hash, store.hashOf(rid));
try std.testing.expectEqual(logstore.FrameKind.zlib, store.kindOf(rid));
}
test "logstore §C(inv1): a zlib frame reopens and still inflates" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
const hash = "zlib-reopen-hash-value-000000001";
const raw = "reopen and inflate me, please, exactly as written before the crash";
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
_ = try store.putZlib(hash, raw);
try store.commit();
store.deinit(std.testing.allocator);
var reopened = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer reopened.deinit(std.testing.allocator);
const rid = reopened.lookup(hash) orelse return error.TestUnexpectedResult;
try std.testing.expectEqual(logstore.FrameKind.zlib, reopened.kindOf(rid));
const got = (try reopened.content(std.testing.allocator, rid)).?;
defer std.testing.allocator.free(got);
try std.testing.expectEqualStrings(raw, got);
}
test "logstore §C: putZlib is idempotent for an equal hash" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
const hash = "zlib-idempotent-hash-value-00001";
const r1 = try store.putZlib(hash, "payload-z");
const size_after = store.size();
const r2 = try store.putZlib(hash, "payload-z");
try std.testing.expectEqual(r1, r2);
try std.testing.expectEqual(size_after, store.size());
}
test "logstore §C: putZlib upgrades a phantom in place" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
const hash = "zlib-phantom-upgrade-hash-000001";
const rid = try store.intern(hash);
try std.testing.expectEqual(@as(?[]u8, null), try store.content(std.testing.allocator, rid));
const rid2 = try store.putZlib(hash, "content arriving for a zlib phantom");
try std.testing.expectEqual(rid, rid2);
const got = (try store.content(std.testing.allocator, rid)).?;
defer std.testing.allocator.free(got);
try std.testing.expectEqualStrings("content arriving for a zlib phantom", got);
}
test "logstore §C/delta §4: putDelta then content applies the delta and round-trips the target" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
const base = "the quick brown fox jumps over the lazy dog, and then again once more";
const target = "the quick brown fox jumps over the lazy dog, and then twice more, at last";
const d = try delta.create(std.testing.allocator, base, target);
defer std.testing.allocator.free(d);
const base_rid = try store.put("delta-base-hash-value-0000000001", base);
const delta_rid = try store.putDelta("delta-target-hash-value-00000001", base_rid, d);
try std.testing.expectEqual(logstore.FrameKind.delta, store.kindOf(delta_rid));
const got = (try store.content(std.testing.allocator, delta_rid)).?;
defer std.testing.allocator.free(got);
try std.testing.expectEqualStrings(target, got);
}
test "logstore §C: a delta frame stores delta_of = the base frame byte offset" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
const base = "base bytes long enough to admit a copy segment in the delta encoder";
const target = "base bytes long enough to admit a copy segment in the delta encoder too";
const d = try delta.create(std.testing.allocator, base, target);
defer std.testing.allocator.free(d);
const base_rid = try store.put("delta-of-base-hash-value-0000001", base); // first frame at header_len
const delta_off = store.size();
const delta_rid = try store.putDelta("delta-of-target-hash-value-00001", base_rid, d);
// Read delta_of straight from the frame header: it must be the base frame's
// byte offset, i.e. header_len for the first frame.
const hl = store.hashOf(delta_rid).len;
var field: [8]u8 = undefined;
_ = try file.pread(delta_off + 10 + hl, &field);
try std.testing.expectEqual(@as(u64, logstore.header_len), std.mem.readInt(u64, &field, .little));
}
test "logstore §C: a two-link delta chain (delta-of-delta) resolves in content()" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
const a = "artifact A: a reasonably long base so the delta encoder emits copies";
const b = "artifact B: a reasonably long base so the delta encoder emits copies, v2";
const c = "artifact C: a reasonably long base so the delta encoder emits copies, v3!!";
const rid_a = try store.put("chain-hash-a-value-000000000001", a);
const d_ab = try delta.create(std.testing.allocator, a, b);
defer std.testing.allocator.free(d_ab);
const rid_b = try store.putDelta("chain-hash-b-value-000000000001", rid_a, d_ab);
const d_bc = try delta.create(std.testing.allocator, b, c);
defer std.testing.allocator.free(d_bc);
const rid_c = try store.putDelta("chain-hash-c-value-000000000001", rid_b, d_bc);
const got = (try store.content(std.testing.allocator, rid_c)).?;
defer std.testing.allocator.free(got);
try std.testing.expectEqualStrings(c, got);
}
test "logstore §C(inv1): a delta frame reopens and content still resolves" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
const base = "reopen base: bytes shared with the target so a copy is worthwhile here";
const target = "reopen base: bytes shared with the target so a copy is worthwhile now!!";
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
const base_rid = try store.put("delta-reopen-base-hash-000000001", base);
const d = try delta.create(std.testing.allocator, base, target);
defer std.testing.allocator.free(d);
_ = try store.putDelta("delta-reopen-target-hash-0000001", base_rid, d);
try store.commit();
store.deinit(std.testing.allocator);
var reopened = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer reopened.deinit(std.testing.allocator);
const rid = reopened.lookup("delta-reopen-target-hash-0000001") orelse return error.TestUnexpectedResult;
const got = (try reopened.content(std.testing.allocator, rid)).?;
defer std.testing.allocator.free(got);
try std.testing.expectEqualStrings(target, got);
}
test "logstore §C/sync §7.2: storedDelta serves the stored container bytes verbatim" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
const base = "verbatim base bytes shared with the target so a copy segment appears here";
const target = "verbatim base bytes shared with the target so a copy segment appears now!";
const base_hash = "verbatim-base-hash-value-0000001";
const base_rid = try store.put(base_hash, base);
const d = try delta.create(std.testing.allocator, base, target);
defer std.testing.allocator.free(d);
const delta_off = store.size();
const delta_rid = try store.putDelta("verbatim-target-hash-value-00001", base_rid, d);
const sd = try store.storedDelta(std.testing.allocator, delta_rid);
defer std.testing.allocator.free(sd.wire);
// §7.2: the sender writes the stored bytes; it does not re-compress. The
// served wire must equal the on-disk payload byte-for-byte, and name the
// delta basis (base) artifact.
const plen = store.payloadLenOf(delta_rid);
const on_disk = try std.testing.allocator.alloc(u8, plen);
defer std.testing.allocator.free(on_disk);
_ = try file.pread(delta_off + 22 + base_hash.len, on_disk);
try std.testing.expectEqualSlices(u8, on_disk, sd.wire);
try std.testing.expectEqualStrings(base_hash, sd.baseHash());
}
test "logstore §C: kindOf reports raw/zlib/delta" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
const base = "kindOf base bytes with enough shared run for a copy in the delta encoder";
const raw_rid = try store.put("kindof-raw-hash-value-0000000001", base);
const zlib_rid = try store.putZlib("kindof-zlib-hash-value-000000001", "some zlib payload");
const d = try delta.create(std.testing.allocator, base, "kindOf base bytes with enough shared run for a copy in the delta encoder!");
defer std.testing.allocator.free(d);
const delta_rid = try store.putDelta("kindof-delta-hash-value-00000001", raw_rid, d);
try std.testing.expectEqual(logstore.FrameKind.raw, store.kindOf(raw_rid));
try std.testing.expectEqual(logstore.FrameKind.zlib, store.kindOf(zlib_rid));
try std.testing.expectEqual(logstore.FrameKind.delta, store.kindOf(delta_rid));
}
test "logstore §C: a delta chain deeper than max_delta_chain is error.Corrupt, not unbounded recursion" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
// Build a chain of exactly max_delta_chain deltas over a raw base: it must
// resolve. One more link must be rejected by the depth bound (no unbounded
// recursion), proving the bound is enforced from the read side.
var current: []u8 = try std.testing.allocator.dupe(u8, "chain base bytes, long enough to permit copies across every link here");
defer std.testing.allocator.free(current);
var prev_rid = try store.put("depth-base-hash-value-0000000000", current);
var last_ok_rid: logstore.Rid = prev_rid;
var i: usize = 0;
while (i < logstore.max_delta_chain + 1) : (i += 1) {
var next_buf: [96]u8 = undefined;
const next = try std.fmt.bufPrint(&next_buf, "chain base bytes, long enough to permit copies across every link {d:0>3}", .{i});
const d = try delta.create(std.testing.allocator, current, next);
defer std.testing.allocator.free(d);
var hbuf: [32]u8 = undefined;
std.mem.writeInt(u64, hbuf[0..8], i, .little);
@memcpy(hbuf[8..], "depth-chain-hash-padding-value--"[0..24]);
const rid = try store.putDelta(&hbuf, prev_rid, d);
std.testing.allocator.free(current);
current = try std.testing.allocator.dupe(u8, next);
prev_rid = rid;
if (i + 1 == logstore.max_delta_chain) last_ok_rid = rid; // top of a length-max_delta_chain chain
}
// A chain of exactly max_delta_chain deltas resolves.
const ok = (try store.content(std.testing.allocator, last_ok_rid)).?;
defer std.testing.allocator.free(ok);
// One link deeper (max_delta_chain + 1) trips the bound.
try std.testing.expectError(error.Corrupt, store.content(std.testing.allocator, prev_rid));
}
// OOM coverage for every public allocating fn added this slice.
fn putZlibNoLeak(gpa: std.mem.Allocator, file: storage.StorageFile) !void {
var store = try logstore.LogStore.open(gpa, file, 0);
defer store.deinit(gpa);
const size = file.size() catch 0;
var hash_buf: [8]u8 = undefined;
std.mem.writeInt(u64, &hash_buf, size, .little);
_ = try store.putZlib(&hash_buf, "payload compressed under simulated allocation failure");
}
test "logstore §C: putZlib propagates allocation failure" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var seed = try logstore.LogStore.open(std.testing.allocator, file, 0);
seed.deinit(std.testing.allocator);
try std.testing.checkAllAllocationFailures(std.testing.allocator, putZlibNoLeak, .{file});
}
fn putDeltaNoLeak(gpa: std.mem.Allocator, file: storage.StorageFile) !void {
var store = try logstore.LogStore.open(gpa, file, 0);
defer store.deinit(gpa);
const base_rid = store.lookup("oom-delta-base-hash-value-000001") orelse return error.TestUnexpectedResult;
const size = file.size() catch 0;
var hash_buf: [8]u8 = undefined;
std.mem.writeInt(u64, &hash_buf, size, .little);
_ = try store.putDelta(&hash_buf, base_rid, "raw delta bytes under simulated allocation failure");
}
test "logstore §C: putDelta propagates allocation failure" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var seed = try logstore.LogStore.open(std.testing.allocator, file, 0);
_ = try seed.put("oom-delta-base-hash-value-000001", "a base frame the delta references");
try seed.commit();
seed.deinit(std.testing.allocator);
try std.testing.checkAllAllocationFailures(std.testing.allocator, putDeltaNoLeak, .{file});
}
fn contentZlibNoLeak(gpa: std.mem.Allocator, file: storage.StorageFile) !void {
var store = try logstore.LogStore.open(gpa, file, 0);
defer store.deinit(gpa);
const rid = store.lookup("oom-zlib-content-hash-value-0001") orelse return error.TestUnexpectedResult;
const bytes = try store.content(gpa, rid);
if (bytes) |b| gpa.free(b);
}
test "logstore §C: content on a zlib frame propagates allocation failure" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var seed = try logstore.LogStore.open(std.testing.allocator, file, 0);
_ = try seed.putZlib("oom-zlib-content-hash-value-0001", "payload for the zlib content allocation-failure probe");
try seed.commit();
seed.deinit(std.testing.allocator);
try std.testing.checkAllAllocationFailures(std.testing.allocator, contentZlibNoLeak, .{file});
}
fn contentDeltaNoLeak(gpa: std.mem.Allocator, file: storage.StorageFile) !void {
var store = try logstore.LogStore.open(gpa, file, 0);
defer store.deinit(gpa);
const rid = store.lookup("oom-delta-content-target-hash-01") orelse return error.TestUnexpectedResult;
const bytes = try store.content(gpa, rid);
if (bytes) |b| gpa.free(b);
}
test "logstore §C: content on a delta chain propagates allocation failure" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
const base = "oom delta base bytes, long enough that the encoder emits a copy segment";
const target = "oom delta base bytes, long enough that the encoder emits a copy segment!!";
var seed = try logstore.LogStore.open(std.testing.allocator, file, 0);
const base_rid = try seed.put("oom-delta-content-base-hash-0001", base);
const d = try delta.create(std.testing.allocator, base, target);
defer std.testing.allocator.free(d);
_ = try seed.putDelta("oom-delta-content-target-hash-01", base_rid, d);
try seed.commit();
seed.deinit(std.testing.allocator);
try std.testing.checkAllAllocationFailures(std.testing.allocator, contentDeltaNoLeak, .{file});
}
fn storedDeltaNoLeak(gpa: std.mem.Allocator, file: storage.StorageFile) !void {
var store = try logstore.LogStore.open(gpa, file, 0);
defer store.deinit(gpa);
const rid = store.lookup("oom-stored-delta-target-hash-001") orelse return error.TestUnexpectedResult;
const sd = try store.storedDelta(gpa, rid);
gpa.free(sd.wire);
}
test "logstore §C: storedDelta propagates allocation failure" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
const base = "stored-delta base bytes, long enough that the encoder emits a copy here";
const target = "stored-delta base bytes, long enough that the encoder emits a copy here!!";
var seed = try logstore.LogStore.open(std.testing.allocator, file, 0);
const base_rid = try seed.put("oom-stored-delta-base-hash-00001", base);
const d = try delta.create(std.testing.allocator, base, target);
defer std.testing.allocator.free(d);
_ = try seed.putDelta("oom-stored-delta-target-hash-001", base_rid, d);
try seed.commit();
seed.deinit(std.testing.allocator);
try std.testing.checkAllAllocationFailures(std.testing.allocator, storedDeltaNoLeak, .{file});
}
// ---------------------------------------------------------------------------
// Config frames (storage-track-plan.md §E: repo-local config in the file).
// ---------------------------------------------------------------------------
test "logstore §E: putConfig then readConfig round-trips branch, project id, and opaque hash policy" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
// hash_policy 7 is opaque: it round-trips byte-for-byte, never interpreted.
try store.putConfig(.{ .hash_policy = 7, .default_branch = "trunk", .project_id = "proj-abc" });
var oc = (try store.readConfig(std.testing.allocator)).?;
defer oc.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(u8, 7), oc.config.hash_policy);
try std.testing.expectEqualStrings("trunk", oc.config.default_branch);
try std.testing.expectEqualStrings("proj-abc", oc.config.project_id);
}
test "logstore §E: the last config frame wins, and survives commit+reopen" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
try store.putConfig(.{ .hash_policy = 1, .default_branch = "trunk", .project_id = "p1" });
try store.putConfig(.{ .hash_policy = 2, .default_branch = "main", .project_id = "p2" });
{
var oc = (try store.readConfig(std.testing.allocator)).?;
defer oc.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(u8, 2), oc.config.hash_policy);
try std.testing.expectEqualStrings("main", oc.config.default_branch);
}
try store.commit();
store.deinit(std.testing.allocator);
var reopened = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer reopened.deinit(std.testing.allocator);
var oc = (try reopened.readConfig(std.testing.allocator)).?;
defer oc.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(u8, 2), oc.config.hash_policy);
try std.testing.expectEqualStrings("main", oc.config.default_branch);
try std.testing.expectEqualStrings("p2", oc.config.project_id);
}
test "logstore §E: a fresh log has no config; readConfig returns null" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var store = try logstore.LogStore.open(std.testing.allocator, file, 0);
defer store.deinit(std.testing.allocator);
try std.testing.expect((try store.readConfig(std.testing.allocator)) == null);
}
fn configNoLeak(gpa: std.mem.Allocator, file: storage.StorageFile) !void {
var store = try logstore.LogStore.open(gpa, file, 0);
defer store.deinit(gpa);
try store.putConfig(.{ .hash_policy = 3, .default_branch = "branch-name", .project_id = "project-id" });
// config_offset flips only after a successful append, so an OOM mid-put
// leaves no config to read and no half-state; a successful put must read
// back cleanly with no leak.
var oc = (try store.readConfig(gpa)) orelse return error.TestUnexpectedResult;
oc.deinit(gpa);
}
test "logstore §E: putConfig and readConfig propagate allocation failure" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var fs = try openTmp(&tmp, io, "log.bin");
defer fs.file.close(io);
const file = fs.storageFile();
var seed = try logstore.LogStore.open(std.testing.allocator, file, 0);
seed.deinit(std.testing.allocator); // header only
try std.testing.checkAllAllocationFailures(std.testing.allocator, configNoLeak, .{file});
}
test "logstore §E: cloneInto copies + derives — the clone is an equal repository (same artifact set, same config, byte-identical log)" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
const alloc = std.testing.allocator;
var src_fs = try openTmp(&tmp, io, "ship.zit");
defer src_fs.file.close(io);
const src_file = src_fs.storageFile();
var dst_fs = try openTmp(&tmp, io, "clone.zit");
defer dst_fs.file.close(io);
const dst_file = dst_fs.storageFile();
const a1 = "artifact one: a base blob long enough to feed the delta encoder a copy segment";
const a2 = "artifact two: separate content routed through the fossil zlib container path here";
const a3 = "artifact one: a base blob long enough to feed the delta encoder a copy segment, extended tail";
const cfg: logstore.Config = .{ .hash_policy = 6, .default_branch = "trunk", .project_id = "ship-proj" };
var r1: logstore.Rid = undefined;
var r2: logstore.Rid = undefined;
var r3: logstore.Rid = undefined;
{
var store = try logstore.LogStore.open(alloc, src_file, 9);
defer store.deinit(alloc);
r1 = try store.put("clone-A1", a1);
r2 = try store.putZlib("clone-A2", a2);
const d = try delta.create(alloc, a1, a3);
defer alloc.free(d);
r3 = try store.putDelta("clone-A3", r1, d);
try store.putConfig(cfg);
try store.commit();
}
var clone = try logstore.LogStore.cloneInto(alloc, src_file, dst_file);
defer clone.deinit(alloc);
// Byte-identical log: only the authoritative artifact set ships (no
// derived tables), so the copy is exact and the header round-trips.
try std.testing.expectEqual(try src_file.size(), clone.size());
try std.testing.expectEqual(@as(u8, 9), clone.hash_kind);
// Same artifact set: every content hash resolves to the same rid and the
// same reconstructed bytes — the delta resolves against its copied base.
const cases = [_]struct { hash: []const u8, rid: logstore.Rid, want: []const u8 }{
.{ .hash = "clone-A1", .rid = r1, .want = a1 },
.{ .hash = "clone-A2", .rid = r2, .want = a2 },
.{ .hash = "clone-A3", .rid = r3, .want = a3 },
};
for (cases) |c| {
try std.testing.expectEqual(c.rid, clone.lookup(c.hash).?);
const got = (try clone.content(alloc, clone.lookup(c.hash).?)).?;
defer alloc.free(got);
try std.testing.expectEqualStrings(c.want, got);
}
try std.testing.expectEqual(@as(usize, 3), clone.entries.items.len);
// Config recovered through the fresh open — the file is self-contained.
var oc = (try clone.readConfig(alloc)).?;
defer oc.deinit(alloc);
try std.testing.expectEqual(cfg.hash_policy, oc.config.hash_policy);
try std.testing.expectEqualStrings(cfg.default_branch, oc.config.default_branch);
try std.testing.expectEqualStrings(cfg.project_id, oc.config.project_id);
}
test "logstore §E: cloning a header-only log yields an equal empty repository" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
const alloc = std.testing.allocator;
var src_fs = try openTmp(&tmp, io, "empty-ship.zit");
defer src_fs.file.close(io);
const src_file = src_fs.storageFile();
var dst_fs = try openTmp(&tmp, io, "empty-clone.zit");
defer dst_fs.file.close(io);
const dst_file = dst_fs.storageFile();
// Source carries a header and no content frames.
var seed = try logstore.LogStore.open(alloc, src_file, 0);
seed.deinit(alloc);
var clone = try logstore.LogStore.cloneInto(alloc, src_file, dst_file);
defer clone.deinit(alloc);
try std.testing.expectEqual(@as(u64, logstore.header_len), clone.size());
try std.testing.expectEqual(@as(usize, 0), clone.entries.items.len);
try std.testing.expect((try clone.readConfig(alloc)) == null);
}
test "logstore §E: a damaged clone degrades gracefully via torn-tail truncation" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
const alloc = std.testing.allocator;
var src_fs = try openTmp(&tmp, io, "torn-ship.zit");
defer src_fs.file.close(io);
const src_file = src_fs.storageFile();
var dst_fs = try openTmp(&tmp, io, "torn-clone.zit");
defer dst_fs.file.close(io);
const dst_file = dst_fs.storageFile();
var cut_base: u64 = undefined;
{
var store = try logstore.LogStore.open(alloc, src_file, 0);
defer store.deinit(alloc);
_ = try store.put("clone-B4-a", "alpha payload for the damaged-clone recovery test");
try store.putConfig(.{ .hash_policy = 3, .default_branch = "trunk", .project_id = "keep" });
try store.commit();
cut_base = store.size(); // boundary after A + config, before B
_ = try store.put("clone-B4-b", "beta payload that the torn tail truncates away on derive");
try store.commit();
}
// A damaged copy: only a byte-prefix ending 5 bytes into B's frame. The
// derive step (open) is the same one cloneInto uses, so it drops exactly
// the torn tail and recovers A + config (§E L119).
var buf: [4096]u8 = undefined;
const prefix: usize = @intCast(cut_base + 5);
std.debug.assert(prefix <= buf.len);
const n = try src_file.pread(0, buf[0..prefix]);
try std.testing.expectEqual(prefix, n);
try dst_file.append(buf[0..n]);
var clone = try logstore.LogStore.open(alloc, dst_file, 0);
defer clone.deinit(alloc);
try std.testing.expectEqual(cut_base, clone.size());
try std.testing.expect(clone.lookup("clone-B4-a") != null);
try std.testing.expect(clone.lookup("clone-B4-b") == null);
var oc = (try clone.readConfig(alloc)).?;
defer oc.deinit(alloc);
try std.testing.expectEqualStrings("trunk", oc.config.default_branch);
try std.testing.expectEqualStrings("keep", oc.config.project_id);
}
fn cloneNoLeak(gpa: std.mem.Allocator, source: storage.StorageFile, tmp: *std.testing.TmpDir, io: std.Io) !void {
// A fresh (truncating) dest each pass, so a retried invocation starts from
// an empty capability — cloneInto's documented precondition.
var dst_fs = try openTmp(tmp, io, "clone-noleak.zit");
defer dst_fs.file.close(io);
var store = try logstore.LogStore.cloneInto(gpa, source, dst_fs.storageFile());
defer store.deinit(gpa);
const c = (try store.content(gpa, store.lookup("clone-A1").?)).?;
gpa.free(c);
}
test "logstore §E: cloneInto propagates allocation failure" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
const io = std.testing.io;
var src_fs = try openTmp(&tmp, io, "noleak-ship.zit");
defer src_fs.file.close(io);
const src_file = src_fs.storageFile();
{
var store = try logstore.LogStore.open(std.testing.allocator, src_file, 0);
defer store.deinit(std.testing.allocator);
_ = try store.put("clone-A1", "the base blob for the clone allocation-failure sweep");
try store.putConfig(.{ .hash_policy = 2, .default_branch = "trunk", .project_id = "pid" });
try store.commit();
}
try std.testing.checkAllAllocationFailures(std.testing.allocator, cloneNoLeak, .{ src_file, &tmp, io });
}