//! zit — the persisted derivation cache (CLI-layer, NOT part of the embeddable
//! `zit` library).
//!
//! A `.zit-cache` sidecar beside `.zit` is a pure MEMO of the derived repo-view:
//! repo-state §1 pins derived state as a function of the immutable log, §7.1
//! blesses precomputing the leaf table, and §7.3 keeps full rebuild as the
//! fallback — this file stores the crosslink INPUT records (in persisted order)
//! at a named log prefix so a read-only verb replays them (`RepoView`-side)
//! instead of re-reading and re-CRC'ing the whole log. Deleting the file must
//! never change any answer: it is disposable by construction.
//!
//! The cache is keyed on the append-only log's identity (`logstore.LogKey`:
//! end offset, frame count, last-frame header CRC). A key match serves the
//! cache; any mismatch, a wrong magic/version, or a failed self-checksum is a
//! MISS — the caller rebuilds from the log and rewrites the cache atomically.
//! A stale, truncated, or corrupt cache is therefore never trusted.
//!
//! This module is a dumb container: it validates magic/version/key/checksum and
//! hands the opaque record body to `RepoView`, which alone knows the record
//! format. All effects go through the injected `std.Io` seam (design-seams rule).
const std = @import("std");
const repoview = @import("repoview.zig");
const logstore = @import("zit_logstore");
const storage = @import("zit_storage");
/// The sidecar's name, beside `.zit` in the checkout root. Fossil-ignored
/// (`.fossil-settings/ignore-glob`) and never committed — a disposable,
/// regenerable build artifact in the `.zit`/`.zit-checkout` family.
pub const cache_filename: []const u8 = ".zit-cache";
/// File magic; distinguishes a `.zit-cache` from any other file that lands at
/// the name. Eight bytes so the fixed header stays 8-aligned.
const magic = "ZITCACHE";
/// On-disk format version. Any mismatch is a MISS: the cache is disposable, so
/// the tables it serializes stay free to evolve without a migration path.
const format_version: u16 = 1;
/// Bytes before the variable-length `main_branch`:
/// `magic(8) | format_version(2) | end(8) | frame_count(8) |
/// last_frame_offset(8) | last_header_crc(4) | main_branch_len(2)` = 40.
const fixed_prefix_len: usize = 40;
/// Trailing self-checksum width (CRC32 over every earlier byte).
const checksum_len: usize = 4;
/// Refuse to read or write a cache larger than this. A `.zit-cache` body is the
/// structural records only (tens of MB even at 130k artifacts), so this cap is
/// generous headroom that still bounds a lying on-disk size before any
/// allocation (TigerStyle: never trust a claimed length).
const max_cache_bytes: usize = 1 << 31;
/// Try to serve `view` from the `.zit-cache` in `dir`, validating it against
/// the live `log_file` (the open `.zit`). Returns true only on a fully
/// validated HIT: correct magic and version, an intact self-checksum, a stored
/// key whose tail still matches the live log (`logstore.validateTail` — O(1), no
/// full scan), and a record body `RepoView` accepts. On a hit `view` is filled
/// and self-contained (the caller `deinit`s it). Returns false on ANY miss — no
/// file, wrong magic/version, size out of bounds, checksum failure, a stale key,
/// or a malformed body — and never errors: a cache is advisory, so a missing or
/// unreadable one just means "rebuild from the log". On a false return `view` is
/// untouched or already torn down; the caller must not `deinit` it.
pub fn tryLoad(
gpa: std.mem.Allocator,
io: std.Io,
dir: std.Io.Dir,
log_file: storage.StorageFile,
view: *repoview.RepoView,
) bool {
var file = dir.openFile(io, cache_filename, .{}) catch return false; // absent/unreadable ⇒ miss
defer file.close(io);
const st = file.stat(io) catch return false;
const size: usize = std.math.cast(usize, st.size) orelse return false;
// A valid cache is at least the fixed header + checksum; reject anything
// shorter (truncated) or implausibly large before allocating.
if (size < fixed_prefix_len + checksum_len or size > max_cache_bytes) return false;
const buf = gpa.alloc(u8, size) catch return false;
defer gpa.free(buf);
const n = file.readPositionalAll(io, buf, 0) catch return false;
if (n != size) return false;
if (!std.mem.eql(u8, buf[0..8], magic)) return false;
if (std.mem.readInt(u16, buf[8..10], .little) != format_version) return false;
// Self-checksum over everything but the trailing CRC — a torn or bitflipped
// cache fails here and is treated as a miss, never a trusted partial view.
const stored_crc = std.mem.readInt(u32, buf[size - checksum_len ..][0..4], .little);
var crc = std.hash.Crc32.init();
crc.update(buf[0 .. size - checksum_len]);
if (crc.final() != stored_crc) return false;
// The stored key must still describe the live log's tail — the whole
// invalidation story. Any append, truncation, or a rewritten tail changes
// it, so a stale cache cannot validate. O(1): one stat + one header read.
const stored_key: logstore.LogStore.LogKey = .{
.end = std.mem.readInt(u64, buf[10..18], .little),
.frame_count = std.mem.readInt(u64, buf[18..26], .little),
.last_frame_offset = std.mem.readInt(u64, buf[26..34], .little),
.last_header_crc = std.mem.readInt(u32, buf[34..38], .little),
};
if (!logstore.LogStore.validateTail(log_file, stored_key)) return false;
const mbl: usize = std.mem.readInt(u16, buf[38..40], .little);
// Validate the declared main-branch length against the actual file span
// before slicing (never trust a claimed-huge length).
if (fixed_prefix_len + mbl + checksum_len > size) return false;
if (mbl == 0) return false; // main_branch is never empty (§6.1: unset ⇒ "trunk")
const main_branch = buf[fixed_prefix_len .. fixed_prefix_len + mbl];
const body = buf[fixed_prefix_len + mbl .. size - checksum_len];
// Replay the records without touching the log. On any framing error the
// view tears itself down and we fall back to a rebuild.
view.buildFromRecordBody(gpa, body, main_branch) catch return false;
return true;
}
/// Write `body` (a `RepoView.loadCapturing` record body) plus `main_branch` and
/// `key` to the `.zit-cache` in `dir`, atomically (temp file + fsync + rename),
/// so a reader ever sees either the prior valid cache or this one — never a torn
/// file. Best-effort: a read-only directory, a full disk, or any I/O failure is
/// swallowed (the verb has already answered from the just-built view); the cache
/// is an optimization, not a correctness dependency. The temp file is deleted on
/// any early return.
pub fn store(
gpa: std.mem.Allocator,
io: std.Io,
dir: std.Io.Dir,
key: logstore.LogStore.LogKey,
main_branch: []const u8,
body: []const u8,
) void {
if (main_branch.len == 0 or main_branch.len > std.math.maxInt(u16)) return; // §6.1 invariant
if (body.len > max_cache_bytes) return; // stay inside the read-side cap
var out: std.ArrayList(u8) = .empty;
defer out.deinit(gpa);
out.appendSlice(gpa, magic) catch return;
appendInt(gpa, &out, u16, format_version) catch return;
appendInt(gpa, &out, u64, key.end) catch return;
appendInt(gpa, &out, u64, key.frame_count) catch return;
appendInt(gpa, &out, u64, key.last_frame_offset) catch return;
appendInt(gpa, &out, u32, key.last_header_crc) catch return;
appendInt(gpa, &out, u16, @intCast(main_branch.len)) catch return;
out.appendSlice(gpa, main_branch) catch return;
out.appendSlice(gpa, body) catch return;
var crc = std.hash.Crc32.init();
crc.update(out.items);
appendInt(gpa, &out, u32, crc.final()) catch return;
var af = dir.createFileAtomic(io, cache_filename, .{ .replace = true }) catch return;
defer af.deinit(io);
af.file.writePositionalAll(io, out.items, 0) catch return;
af.file.sync(io) catch return; // durable before the rename makes it visible
af.replace(io) catch return; // atomic rename into place
}
fn appendInt(gpa: std.mem.Allocator, s: *std.ArrayList(u8), comptime T: type, v: T) error{OutOfMemory}!void {
var buf: [@sizeOf(T)]u8 = undefined;
std.mem.writeInt(T, &buf, v, .little);
try s.appendSlice(gpa, &buf);
}
// ===========================================================================
// Tests — the load-bearing invariant is HIT ≡ recompute (byte-identical) and
// "a stale/corrupt/wrong-key cache is never served". Fixtures build real logs
// through `LogStore` and derive with the real `RepoView`; expected values come
// from the repo-state RFC (§1 memo, §7.1 precomputed table, §7.3 rebuild).
// ===========================================================================
const zit = @import("zit");
const artifact = zit.artifact;
const repostate = zit.repostate;
const testing = std.testing;
/// A tmp-backed writable log plus the checkout dir the cache lives in.
const Fixture = struct {
tmp: std.testing.TmpDir,
fs: storage.FileStorage,
log: logstore.LogStore,
fn init(self: *Fixture, io: std.Io) !void {
self.tmp = testing.tmpDir(.{});
const raw = try self.tmp.dir.createFile(io, ".zit", .{ .read = true });
self.fs = storage.FileStorage.init(io, raw, 0);
self.log = try logstore.LogStore.open(testing.allocator, self.fs.storageFile(), 0);
}
fn deinit(self: *Fixture, io: std.Io) void {
self.log.deinit(testing.allocator);
self.fs.file.close(io);
self.tmp.cleanup();
}
/// A freshly-scanned read-only view of the log — the shape production always
/// derives from (`openReadOnly` populates the frame-count/tail bookkeeping
/// `logKey` reads). The writable `self.log` used for seeding does not
/// maintain that bookkeeping, so cache-key work goes through this. Caller
/// `deinit`s the returned store.
fn openScan(self: *Fixture) !logstore.LogStore {
return logstore.LogStore.openReadOnly(testing.allocator, self.fs.storageFile());
}
/// Store a manifest/control built by `m` under its own sha3-256 digest.
fn put(self: *Fixture, m: *const artifact.Manifest) !void {
const bytes = try artifact.create(m, testing.allocator);
defer testing.allocator.free(bytes);
var digest: [32]u8 = undefined;
std.crypto.hash.sha3.Sha3_256.hash(bytes, &digest, .{});
_ = try self.log.put(&digest, bytes);
}
};
/// The 64-hex name of a serialized manifest (so a later child can name it as a
/// parent). Caller frees.
fn nameOf(m: *const artifact.Manifest) ![]u8 {
const bytes = try artifact.create(m, testing.allocator);
defer testing.allocator.free(bytes);
var digest: [32]u8 = undefined;
std.crypto.hash.sha3.Sha3_256.hash(bytes, &digest, .{});
var hex: [64]u8 = undefined;
const lut = "0123456789abcdef";
for (digest, 0..) |b, i| {
hex[i * 2] = lut[b >> 4];
hex[i * 2 + 1] = lut[b & 0x0f];
}
return testing.allocator.dupe(u8, &hex);
}
fn checkinManifest(ts: []const u8, parent: ?[]const u8, tags: []const artifact.TCard, pbuf: []artifact.PCard) artifact.Manifest {
var p_slice: []const artifact.PCard = &.{};
if (parent) |par| {
pbuf[0] = .{ .parents = par, .count = 1 };
p_slice = pbuf[0..1];
}
return .{
.arena_state = std.heap.ArenaAllocator.init(testing.allocator),
.kind = .manifest,
.a = null,
.b = null,
.c = .{ .comment = "c" },
.d = .{ .timestamp = ts },
.e = null,
.g = null,
.h = null,
.i = null,
.k = null,
.l = null,
.n = null,
.r = null,
.u = .{ .login = "u" },
.z = null,
.w = null,
.f = &.{},
.j = &.{},
.m = &.{},
.p = p_slice,
.q = &.{},
.t = tags,
};
}
fn controlManifest(ts: []const u8, tags: []const artifact.TCard) artifact.Manifest {
return .{
.arena_state = std.heap.ArenaAllocator.init(testing.allocator),
.kind = .control,
.a = null,
.b = null,
.c = null,
.d = .{ .timestamp = ts },
.e = null,
.g = null,
.h = null,
.i = null,
.k = null,
.l = null,
.n = null,
.r = null,
.u = .{ .login = "u" },
.z = null,
.w = null,
.f = &.{},
.j = &.{},
.m = &.{},
.p = &.{},
.q = &.{},
.t = tags,
};
}
/// Build a small but structurally rich repo: a trunk root, a same-branch child,
/// a second root that forks trunk, and a control that closes a leaf — exercising
/// leaves, branches, propagation, and a control artifact.
fn seedRepo(fx: *Fixture) !void {
var pbuf: [1]artifact.PCard = undefined;
const branch_root = [_]artifact.TCard{.{ .sign = .propagate, .name = "branch", .target = null, .value = "trunk" }};
var root_m = checkinManifest("2020-01-01T00:00:00", null, &branch_root, &pbuf);
defer root_m.deinit();
const root = try nameOf(&root_m);
defer testing.allocator.free(root);
try fx.put(&root_m);
var child_m = checkinManifest("2020-01-02T00:00:00", root, &.{}, &pbuf);
defer child_m.deinit();
const child = try nameOf(&child_m);
defer testing.allocator.free(child);
try fx.put(&child_m);
var root2_m = checkinManifest("2020-01-03T00:00:00", null, &branch_root, &pbuf);
defer root2_m.deinit();
const root2 = try nameOf(&root2_m);
defer testing.allocator.free(root2);
try fx.put(&root2_m);
const close = [_]artifact.TCard{.{ .sign = .add, .name = "closed", .target = root2, .value = null }};
var ctl_m = controlManifest("2020-01-04T00:00:00", &close);
defer ctl_m.deinit();
try fx.put(&ctl_m);
}
/// Assert two views are observably identical across every derived query the
/// verbs read. Rids match between the views because the same records intern in
/// the same order, so a per-rid comparison is valid.
fn expectViewsEqual(a: *repoview.RepoView, b: *repoview.RepoView) !void {
try testing.expectEqualStrings(a.main_branch, b.main_branch);
try testing.expectEqual(a.store.store().count(), b.store.store().count());
// Leaf table: same membership AND same iteration order (§7.5 SHOULD order).
var la = a.state.leaves();
var lb = b.state.leaves();
while (la.next()) |ra| {
const rb = lb.next() orelse return error.LeafCountMismatch;
try testing.expectEqual(ra, rb);
}
try testing.expect(lb.next() == null);
const count = a.store.store().count();
var rid: repostate.Rid = 1;
while (rid <= count) : (rid += 1) {
try testing.expectEqualStrings(a.hashOf(rid), b.hashOf(rid));
try testing.expectEqual(a.state.mtimeOf(rid), b.state.mtimeOf(rid));
try testing.expectEqual(a.state.isLeaf(rid), b.state.isLeaf(rid));
try testing.expectEqual(a.state.isOpenLeaf(rid), b.state.isOpenLeaf(rid));
try testing.expectEqual(a.state.isAmbiguousLeaf(rid), b.state.isAmbiguousLeaf(rid));
// branchOf requires an event row (§6.1): skip eventless phantoms and
// control carriers, whose absence-of-event was already asserted above.
if (a.state.mtimeOf(rid) != null)
try testing.expectEqualStrings(a.state.branchOf(rid, a.main_branch), b.state.branchOf(rid, b.main_branch));
const ta = try a.state.tagsOf(testing.allocator, rid);
defer testing.allocator.free(ta);
const tb = try b.state.tagsOf(testing.allocator, rid);
defer testing.allocator.free(tb);
try testing.expectEqual(ta.len, tb.len);
for (ta, tb) |ea, eb| {
try testing.expectEqualStrings(ea.name, eb.name);
try testing.expectEqual(ea.tagtype, eb.tagtype);
}
}
const ba = try a.state.branches(testing.allocator);
defer testing.allocator.free(ba);
const bb = try b.state.branches(testing.allocator);
defer testing.allocator.free(bb);
try testing.expectEqual(ba.len, bb.len);
for (ba, bb) |ea, eb| {
try testing.expectEqualStrings(ea.name, eb.name);
try testing.expectEqual(ea.tip, eb.tip);
try testing.expectEqual(ea.closed, eb.closed);
}
const na = try a.state.tagNames(testing.allocator);
defer testing.allocator.free(na);
const nb = try b.state.tagNames(testing.allocator);
defer testing.allocator.free(nb);
try testing.expectEqual(na.len, nb.len);
for (na, nb) |ea, eb| try testing.expectEqualStrings(ea, eb);
}
/// Capture the record body + key + main_branch for `fx`'s current log (through a
/// freshly-scanned read-only store, as production does), then build a
/// from-scratch reference view. Caller `deinit`s `ref` and frees `body`.
fn captureAndReference(fx: *Fixture, ref: *repoview.RepoView, body: *std.ArrayList(u8)) !logstore.LogStore.LogKey {
var ro = try fx.openScan();
defer ro.deinit(testing.allocator);
const key = try ro.logKey();
var capture: repoview.RepoView = undefined;
try capture.loadCapturing(testing.allocator, &ro, body);
// The reference is a plain from-scratch derivation over the same log.
try ref.load(testing.allocator, &ro);
capture.deinit(testing.allocator);
return key;
}
test "cache §1/§7.1: a hit reconstructs a byte-identical view for a representative repo" {
const io = testing.io;
var fx: Fixture = undefined;
try fx.init(io);
defer fx.deinit(io);
try seedRepo(&fx);
var body: std.ArrayList(u8) = .empty;
defer body.deinit(testing.allocator);
var ref: repoview.RepoView = undefined;
const key = try captureAndReference(&fx, &ref, &body);
defer ref.deinit(testing.allocator);
store(testing.allocator, io, fx.tmp.dir, key, ref.main_branch, body.items);
var hit: repoview.RepoView = undefined;
try testing.expect(tryLoad(testing.allocator, io, fx.tmp.dir, fx.fs.storageFile(), &hit));
defer hit.deinit(testing.allocator);
try expectViewsEqual(&ref, &hit);
}
test "cache §1: hit equals from-scratch derivation for random check-in op-sequences" {
const io = testing.io;
// The DST derivation oracle in miniature: random DAGs of varied sizes,
// each proved to round-trip through the on-disk cache byte-identically.
var prng = std.Random.DefaultPrng.init(0xca6e_5eed);
const rand = prng.random();
for ([_]usize{ 1, 2, 5, 12, 30, 64 }) |n| {
var fx: Fixture = undefined;
try fx.init(io);
defer fx.deinit(io);
var names: std.ArrayList([]u8) = .empty;
defer {
for (names.items) |nm| testing.allocator.free(nm);
names.deinit(testing.allocator);
}
var pbuf: [1]artifact.PCard = undefined;
var day: usize = 0;
while (day < n) : (day += 1) {
var tsbuf: [32]u8 = undefined;
const ts = try std.fmt.bufPrint(&tsbuf, "2021-01-01T00:00:{d:0>2}", .{day % 60});
// Roughly half the check-ins descend from a random earlier one; the
// rest are fresh roots — a mix of chains, forks, and independent tips.
const parent: ?[]const u8 = if (names.items.len != 0 and rand.boolean())
names.items[rand.intRangeLessThan(usize, 0, names.items.len)]
else
null;
const tags: []const artifact.TCard = if (parent == null)
&[_]artifact.TCard{.{ .sign = .propagate, .name = "branch", .target = null, .value = "trunk" }}
else
&.{};
var m = checkinManifest(ts, parent, tags, &pbuf);
defer m.deinit();
try names.append(testing.allocator, try nameOf(&m));
try fx.put(&m);
}
var body: std.ArrayList(u8) = .empty;
defer body.deinit(testing.allocator);
var ref: repoview.RepoView = undefined;
const key = try captureAndReference(&fx, &ref, &body);
defer ref.deinit(testing.allocator);
store(testing.allocator, io, fx.tmp.dir, key, ref.main_branch, body.items);
var hit: repoview.RepoView = undefined;
try testing.expect(tryLoad(testing.allocator, io, fx.tmp.dir, fx.fs.storageFile(), &hit));
defer hit.deinit(testing.allocator);
try expectViewsEqual(&ref, &hit);
}
}
test "cache §7.3: a grown-log key mismatch is a miss (caller rebuilds, then rewrites)" {
const io = testing.io;
var fx: Fixture = undefined;
try fx.init(io);
defer fx.deinit(io);
try seedRepo(&fx);
var body: std.ArrayList(u8) = .empty;
defer body.deinit(testing.allocator);
var ref: repoview.RepoView = undefined;
const key = try captureAndReference(&fx, &ref, &body);
ref.deinit(testing.allocator);
store(testing.allocator, io, fx.tmp.dir, key, "trunk", body.items);
// Grow the log by one control artifact: the live tail advances, so the
// cache written at the old key must now MISS (validateTail sees the change).
var ctl = controlManifest("2020-02-01T00:00:00", &[_]artifact.TCard{.{ .sign = .add, .name = "wip", .target = null, .value = null }});
defer ctl.deinit();
try fx.put(&ctl);
var view: repoview.RepoView = undefined;
try testing.expect(!tryLoad(testing.allocator, io, fx.tmp.dir, fx.fs.storageFile(), &view));
// The rebuild+rewrite path yields a hit again at the new key.
var body2: std.ArrayList(u8) = .empty;
defer body2.deinit(testing.allocator);
var ref2: repoview.RepoView = undefined;
const grown_key = try captureAndReference(&fx, &ref2, &body2);
defer ref2.deinit(testing.allocator);
try testing.expect(grown_key.end != key.end); // the key genuinely advanced
store(testing.allocator, io, fx.tmp.dir, grown_key, ref2.main_branch, body2.items);
var hit2: repoview.RepoView = undefined;
try testing.expect(tryLoad(testing.allocator, io, fx.tmp.dir, fx.fs.storageFile(), &hit2));
defer hit2.deinit(testing.allocator);
try expectViewsEqual(&ref2, &hit2);
}
test "cache: a corrupt, truncated, wrong-magic, or wrong-version cache is a miss, never trusted" {
const io = testing.io;
var fx: Fixture = undefined;
try fx.init(io);
defer fx.deinit(io);
try seedRepo(&fx);
var body: std.ArrayList(u8) = .empty;
defer body.deinit(testing.allocator);
var ref: repoview.RepoView = undefined;
const key = try captureAndReference(&fx, &ref, &body);
ref.deinit(testing.allocator);
store(testing.allocator, io, fx.tmp.dir, key, "trunk", body.items);
// Read the freshly written cache back so we can mutate copies of it.
var f = try fx.tmp.dir.openFile(io, cache_filename, .{});
const sz = (try f.stat(io)).size;
const original = try testing.allocator.alloc(u8, @intCast(sz));
defer testing.allocator.free(original);
_ = try f.readPositionalAll(io, original, 0);
f.close(io);
const rewrite = struct {
fn go(dir: std.Io.Dir, aio: std.Io, bytes: []const u8) !void {
var af = try dir.createFileAtomic(aio, cache_filename, .{ .replace = true });
defer af.deinit(aio);
try af.file.writePositionalAll(aio, bytes, 0);
try af.replace(aio);
}
}.go;
// (a) A flipped self-checksum byte ⇒ miss.
{
const bad = try testing.allocator.dupe(u8, original);
defer testing.allocator.free(bad);
bad[bad.len - 1] ^= 0xff;
try rewrite(fx.tmp.dir, io, bad);
var v: repoview.RepoView = undefined;
try testing.expect(!tryLoad(testing.allocator, io, fx.tmp.dir, fx.fs.storageFile(), &v));
}
// (b) A flipped body byte (checksum now fails) ⇒ miss.
{
const bad = try testing.allocator.dupe(u8, original);
defer testing.allocator.free(bad);
bad[bad.len / 2] ^= 0x01;
try rewrite(fx.tmp.dir, io, bad);
var v: repoview.RepoView = undefined;
try testing.expect(!tryLoad(testing.allocator, io, fx.tmp.dir, fx.fs.storageFile(), &v));
}
// (c) Truncated to half its length ⇒ miss (short read / bad checksum).
{
try rewrite(fx.tmp.dir, io, original[0 .. original.len / 2]);
var v: repoview.RepoView = undefined;
try testing.expect(!tryLoad(testing.allocator, io, fx.tmp.dir, fx.fs.storageFile(), &v));
}
// (d) Wrong magic ⇒ miss.
{
const bad = try testing.allocator.dupe(u8, original);
defer testing.allocator.free(bad);
bad[0] = 'X';
try rewrite(fx.tmp.dir, io, bad);
var v: repoview.RepoView = undefined;
try testing.expect(!tryLoad(testing.allocator, io, fx.tmp.dir, fx.fs.storageFile(), &v));
}
// (e) Wrong version ⇒ miss.
{
const bad = try testing.allocator.dupe(u8, original);
defer testing.allocator.free(bad);
std.mem.writeInt(u16, bad[8..10], format_version + 1, .little);
try rewrite(fx.tmp.dir, io, bad);
var v: repoview.RepoView = undefined;
try testing.expect(!tryLoad(testing.allocator, io, fx.tmp.dir, fx.fs.storageFile(), &v));
}
}
test "cache: an atomic rewrite over a corrupt cache lands a valid, servable one" {
const io = testing.io;
var fx: Fixture = undefined;
try fx.init(io);
defer fx.deinit(io);
try seedRepo(&fx);
// Plant a garbage `.zit-cache` first (a prior torn/foreign file).
{
var af = try fx.tmp.dir.createFileAtomic(io, cache_filename, .{ .replace = true });
defer af.deinit(io);
try af.file.writePositionalAll(io, "garbage not a cache", 0);
try af.replace(io);
}
var body: std.ArrayList(u8) = .empty;
defer body.deinit(testing.allocator);
var ref: repoview.RepoView = undefined;
const key = try captureAndReference(&fx, &ref, &body);
defer ref.deinit(testing.allocator);
// The garbage file is a clean miss...
{
var v: repoview.RepoView = undefined;
try testing.expect(!tryLoad(testing.allocator, io, fx.tmp.dir, fx.fs.storageFile(), &v));
}
// ...and the atomic store replaces it in place with a servable cache.
store(testing.allocator, io, fx.tmp.dir, key, ref.main_branch, body.items);
var hit: repoview.RepoView = undefined;
try testing.expect(tryLoad(testing.allocator, io, fx.tmp.dir, fx.fs.storageFile(), &hit));
defer hit.deinit(testing.allocator);
try expectViewsEqual(&ref, &hit);
}
test "cache: tryLoad on an absent cache is a miss, not an error" {
const io = testing.io;
var fx: Fixture = undefined;
try fx.init(io);
defer fx.deinit(io);
try seedRepo(&fx);
var v: repoview.RepoView = undefined;
try testing.expect(!tryLoad(testing.allocator, io, fx.tmp.dir, fx.fs.storageFile(), &v));
}