//! MirrorState container tests, on the repocache model: a validated HIT
//! round-trips exactly; a stale/corrupt/truncated/wrong-anything file is a
//! DISTINCT load error, never a partial load; the atomic publish never
//! leaves a torn file authoritative.
const std = @import("std");
const state = @import("git_export_state.zig");
const logstore = @import("../logstore.zig");
const testing = std.testing;
const key_a: logstore.LogStore.LogKey = .{
.end = 4096,
.frame_count = 7,
.last_frame_offset = 3968,
.last_header_crc = 0xDEAD_BEEF,
};
const hash_a = "a" ** 64;
const hash_b = "b" ** 40; // SHA-1-sized names round-trip too
const oid_a = "0123456789abcdef0123456789abcdef01234567";
fn sampleRecords() [2]state.Record {
return .{
.{ .hash = hash_a, .mark = 1, .oid = oid_a, .kind = .commit },
.{ .hash = hash_b, .mark = 2, .oid = oid_a, .kind = .tag },
};
}
fn sampleRefs() [1]state.Ref {
return .{.{ .name = "refs/heads/trunk", .oid = oid_a }};
}
test "export mirror state: a validated hit round-trips key, flag, records, refs, and the hash index" {
const io = testing.io;
const gpa = testing.allocator;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
const records = sampleRecords();
const refs = sampleRefs();
try testing.expect(state.store(gpa, io, tmp.dir, key_a, true, &records, &refs));
var loaded = try state.load(gpa, io, tmp.dir);
defer loaded.deinit();
try testing.expectEqual(key_a.end, loaded.source_key.end);
try testing.expectEqual(key_a.frame_count, loaded.source_key.frame_count);
try testing.expectEqual(key_a.last_frame_offset, loaded.source_key.last_frame_offset);
try testing.expectEqual(key_a.last_header_crc, loaded.source_key.last_header_crc);
try testing.expect(loaded.provisional_fixture);
try testing.expectEqual(@as(usize, 2), loaded.records.len);
try testing.expectEqualStrings(hash_a, loaded.records[0].hash);
try testing.expectEqual(@as(u64, 1), loaded.records[0].mark);
try testing.expectEqualStrings(oid_a, loaded.records[0].oid);
try testing.expectEqual(state.RecordKind.commit, loaded.records[0].kind);
try testing.expectEqual(state.RecordKind.tag, loaded.records[1].kind);
try testing.expectEqual(@as(usize, 1), loaded.refs.len);
try testing.expectEqualStrings("refs/heads/trunk", loaded.refs[0].name);
const hit = loaded.containsHash(hash_b) orelse return error.TestUnexpectedResult;
try testing.expectEqual(@as(u64, 2), hit.mark);
try testing.expect(loaded.containsHash("c" ** 64) == null);
}
test "export mirror state: absence is Absent — the ordinary first run, distinct from damage" {
const io = testing.io;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
try testing.expectError(error.Absent, state.load(testing.allocator, io, tmp.dir));
}
fn storedBytes(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![]u8 {
return dir.readFileAlloc(io, state.state_filename, gpa, .limited(1 << 20));
}
fn rewrite(io: std.Io, dir: std.Io.Dir, bytes: []const u8) !void {
var f = try dir.createFile(io, state.state_filename, .{ .truncate = true });
defer f.close(io);
try f.writePositionalAll(io, bytes, 0);
}
test "export mirror state: every framing violation is a distinct error, never a partial load" {
const io = testing.io;
const gpa = testing.allocator;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
const records = sampleRecords();
const refs = sampleRefs();
try testing.expect(state.store(gpa, io, tmp.dir, key_a, false, &records, &refs));
const good = try storedBytes(gpa, io, tmp.dir);
defer gpa.free(good);
// Wrong magic.
{
const bad = try gpa.dupe(u8, good);
defer gpa.free(bad);
bad[0] = 'X';
try rewrite(io, tmp.dir, bad);
try testing.expectError(error.WrongMagic, state.load(gpa, io, tmp.dir));
}
// Wrong version.
{
const bad = try gpa.dupe(u8, good);
defer gpa.free(bad);
bad[8] = 0xEE;
try rewrite(io, tmp.dir, bad);
try testing.expectError(error.WrongVersion, state.load(gpa, io, tmp.dir));
}
// A flipped byte ANYWHERE past the version is a checksum mismatch (the
// magic/version prefix reports its own named error first).
{
var offset: usize = 10;
while (offset < good.len) : (offset += 7) {
const bad = try gpa.dupe(u8, good);
defer gpa.free(bad);
bad[offset] ^= 0xFF;
try rewrite(io, tmp.dir, bad);
const result = state.load(gpa, io, tmp.dir);
try testing.expectError(error.ChecksumMismatch, result);
}
}
// Truncation at every 8-byte boundary: named truncation-class errors,
// never success (below the 10-byte prefix the magic/version reads fail).
{
var cut: usize = 0;
while (cut < good.len) : (cut += 8) {
try rewrite(io, tmp.dir, good[0..cut]);
const result = state.load(gpa, io, tmp.dir);
if (result) |_| return error.TestUnexpectedResult else |err| switch (err) {
error.Truncated, error.ChecksumMismatch, error.WrongMagic, error.WrongVersion => {},
else => return err,
}
}
}
// A record count lying about the body: crc-honest rewrite still refuses.
{
const bad = try gpa.dupe(u8, good);
defer gpa.free(bad);
const count_at = 8 + 2 + 8 + 8 + 8 + 4 + 1;
std.mem.writeInt(u64, bad[count_at..][0..8], 10_000, .little);
var crc = @import("../crc32.zig").Crc32.init();
crc.update(bad[0 .. bad.len - 4]);
std.mem.writeInt(u32, bad[bad.len - 4 ..][0..4], crc.final(), .little);
try rewrite(io, tmp.dir, bad);
try testing.expectError(error.LyingLength, state.load(gpa, io, tmp.dir));
}
// Control: the untouched bytes still load.
try rewrite(io, tmp.dir, good);
var loaded = try state.load(gpa, io, tmp.dir);
loaded.deinit();
}
test "export mirror state: a stray temp or a replaced-and-torn write never unseats a good state" {
const io = testing.io;
const gpa = testing.allocator;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
const records = sampleRecords();
const refs = sampleRefs();
try testing.expect(state.store(gpa, io, tmp.dir, key_a, false, &records, &refs));
// Crash shape 1: a temp file left behind beside the member (the moment
// between temp write and rename) — the published state is untouched.
{
var f = try tmp.dir.createFile(io, state.state_dirname ++ "/state.tmp-crash", .{});
try f.writePositionalAll(io, "torn temp garbage", 0);
f.close(io);
var loaded = try state.load(gpa, io, tmp.dir);
loaded.deinit();
}
// Crash shape 2: a successor store OVER the member is atomic — after a
// successful second store the new content is whole (no torn middle
// state observable through the member name).
{
const more = [_]state.Record{.{ .hash = "d" ** 64, .mark = 9, .oid = oid_a, .kind = .commit }};
try testing.expect(state.store(gpa, io, tmp.dir, key_a, false, &more, &refs));
var loaded = try state.load(gpa, io, tmp.dir);
defer loaded.deinit();
try testing.expectEqual(@as(usize, 1), loaded.records.len);
try testing.expectEqual(@as(u64, 9), loaded.records[0].mark);
}
}
test "export mirror state: prefix extension admits growth, refuses shrink and a swapped tail" {
const grown: logstore.LogStore.LogKey = .{ .end = 8192, .frame_count = 11, .last_frame_offset = 8000, .last_header_crc = 1 };
try testing.expect(state.MirrorState.isPrefixExtensionOf(key_a, grown));
// Identical log: trivially a prefix.
try testing.expect(state.MirrorState.isPrefixExtensionOf(key_a, key_a));
// Shrunk log: refused.
const shrunk: logstore.LogStore.LogKey = .{ .end = 100, .frame_count = 1, .last_frame_offset = 0, .last_header_crc = 1 };
try testing.expect(!state.MirrorState.isPrefixExtensionOf(key_a, shrunk));
// Same length, different tail identity: a generation swap, refused.
var swapped = key_a;
swapped.last_header_crc = 0x1234_5678;
try testing.expect(!state.MirrorState.isPrefixExtensionOf(key_a, swapped));
}