//! Import map container tests, on the export-state model: a validated hit
//! round-trips exactly; every damaged file is a DISTINCT load error, never a
//! partial load; a publish that fails before the rename leaves the previous
//! map authoritative.
const std = @import("std");
const repeat = @import("../repeat.zig").repeat;
const map_mod = @import("git_import_map.zig");
const Crc32 = @import("../crc32.zig").Crc32;
const testing = std.testing;
const oid_root_a = repeat("1", 40);
const oid_root_b = repeat("0", 40);
const oid_c = "0123456789abcdef0123456789abcdef01234567";
const checkin_a = repeat("a", 64);
const checkin_b = repeat("b", 40);
fn sampleCommits() [3]map_mod.CommitRecord {
return .{
.{ .oid = oid_root_a, .checkin = checkin_a, .branch = "trunk", .depth = 0, .height = 0 },
.{ .oid = oid_root_b, .checkin = checkin_b, .branch = "feature", .depth = 0, .height = 1 },
.{ .oid = oid_c, .checkin = repeat("c", 64), .branch = "trunk", .depth = 1, .height = 2 },
};
}
fn sampleRefs() [2]map_mod.RefRecord {
return .{
.{ .name = "refs/heads/main", .oid = oid_c },
.{ .name = "refs/tags/v1", .oid = oid_root_a },
};
}
/// Roots deliberately out of order: the writer sorts them.
const sample_roots = [_][]const u8{ oid_root_a, oid_root_b };
fn sampleContents(commits: []const map_mod.CommitRecord, refs: []const map_mod.RefRecord) map_mod.Contents {
return .{
.object_format = .sha1,
.roots = &sample_roots,
.settings = .{
.submanifest = .{ .tree = 7 },
.attribution = .author,
.refuse_signed = true,
.default_branch = "main",
},
.mirror_classifier_fired = true,
.artifact_count = 12345,
.commits = commits,
.refs = refs,
};
}
fn storedBytes(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir) ![]u8 {
return dir.readFileAlloc(io, map_mod.map_filename, gpa, .limited(1 << 20));
}
fn rewrite(io: std.Io, dir: std.Io.Dir, bytes: []const u8) !void {
var f = try dir.createFile(io, map_mod.map_filename, .{ .truncate = true });
defer f.close(io);
try f.writePositionalAll(io, bytes, 0);
}
/// Rewrite `bytes` with a recomputed checksum, so the load gets past the
/// self-check and the field validation under test is what answers.
fn rewriteHonest(io: std.Io, dir: std.Io.Dir, bytes: []u8) !void {
var crc = Crc32.init();
crc.update(bytes[0 .. bytes.len - 4]);
std.mem.writeInt(u32, bytes[bytes.len - 4 ..][0..4], crc.final(), .little);
try rewrite(io, dir, bytes);
}
// Fixed offsets in the sample file.
const object_format_at = 8 + 2;
const submanifest_at = object_format_at + 1;
const k_at = submanifest_at + 1;
const attribution_at = k_at + 1;
const refuse_signed_at = attribution_at + 1;
const classifier_at = refuse_signed_at + 1;
const artifact_count_at = classifier_at + 1;
const default_branch_at = artifact_count_at + 8;
const root_count_at = default_branch_at + 2 + "main".len;
const first_root_at = root_count_at + 8 + 1;
const second_root_at = first_root_at + 40 + 1;
const commit_count_at = second_root_at + 40;
const first_commit_oid_at = commit_count_at + 8 + 1;
test "import map: a populated map round-trips identity, settings, records, and refs" {
const io = testing.io;
const gpa = testing.allocator;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
const commits = sampleCommits();
const refs = sampleRefs();
try map_mod.write(gpa, io, tmp.dir, sampleContents(&commits, &refs));
var loaded = try map_mod.load(gpa, io, tmp.dir);
defer loaded.deinit();
const c = loaded.contents;
try testing.expectEqual(map_mod.ObjectFormat.sha1, c.object_format);
try testing.expectEqual(@as(usize, 2), c.roots.len);
try testing.expectEqualStrings(oid_root_b, c.roots[0]);
try testing.expectEqualStrings(oid_root_a, c.roots[1]);
try testing.expectEqual(@as(u4, 7), c.settings.submanifest.tree);
try testing.expectEqual(map_mod.Attribution.author, c.settings.attribution);
try testing.expect(c.settings.refuse_signed);
try testing.expectEqualStrings("main", c.settings.default_branch);
try testing.expect(c.mirror_classifier_fired);
try testing.expectEqual(@as(u64, 12345), c.artifact_count);
try testing.expectEqual(@as(usize, 3), c.commits.len);
for (commits, c.commits) |want, got| {
try testing.expectEqualStrings(want.oid, got.oid);
try testing.expectEqualStrings(want.checkin, got.checkin);
try testing.expectEqualStrings(want.branch, got.branch);
try testing.expectEqual(want.depth, got.depth);
try testing.expectEqual(want.height, got.height);
}
try testing.expectEqual(@as(usize, 2), c.refs.len);
try testing.expectEqualStrings("refs/tags/v1", c.refs[1].name);
try testing.expectEqualStrings(oid_root_a, c.refs[1].oid);
// A loaded map writes back byte-identically.
const first = try storedBytes(gpa, io, tmp.dir);
defer gpa.free(first);
try map_mod.write(gpa, io, tmp.dir, loaded.contents);
const second = try storedBytes(gpa, io, tmp.dir);
defer gpa.free(second);
try testing.expectEqualSlices(u8, first, second);
}
test "import map: an empty sha256 flat map round-trips" {
const io = testing.io;
const gpa = testing.allocator;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
try map_mod.write(gpa, io, tmp.dir, .{
.object_format = .sha256,
.roots = &.{},
.settings = .{ .submanifest = .flat, .attribution = .committer, .refuse_signed = false, .default_branch = "" },
.mirror_classifier_fired = false,
.artifact_count = 0,
.commits = &.{},
.refs = &.{},
});
var loaded = try map_mod.load(gpa, io, tmp.dir);
defer loaded.deinit();
try testing.expectEqual(map_mod.ObjectFormat.sha256, loaded.contents.object_format);
try testing.expect(loaded.contents.settings.submanifest == .flat);
try testing.expectEqual(map_mod.Attribution.committer, loaded.contents.settings.attribution);
try testing.expect(!loaded.contents.settings.refuse_signed);
try testing.expect(!loaded.contents.mirror_classifier_fired);
try testing.expectEqual(@as(usize, 0), loaded.contents.roots.len);
try testing.expectEqual(@as(usize, 0), loaded.contents.commits.len);
try testing.expectEqual(@as(usize, 0), loaded.contents.refs.len);
try testing.expect(loaded.lookupOid(oid_c) == null);
try testing.expect(loaded.lookupRef("refs/heads/main") == null);
}
test "import map: absence is Absent, distinct from damage" {
const io = testing.io;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
try testing.expectError(error.Absent, map_mod.load(testing.allocator, io, tmp.dir));
}
test "import map: lookups hit recorded commits and refs and miss everything else" {
const io = testing.io;
const gpa = testing.allocator;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
const commits = sampleCommits();
const refs = sampleRefs();
try map_mod.write(gpa, io, tmp.dir, sampleContents(&commits, &refs));
var loaded = try map_mod.load(gpa, io, tmp.dir);
defer loaded.deinit();
const hit = loaded.lookupOid(oid_c) orelse return error.TestUnexpectedResult;
try testing.expectEqualStrings(repeat("c", 64), hit.checkin);
try testing.expectEqual(@as(u64, 1), hit.depth);
try testing.expect(loaded.lookupOid(repeat("e", 40)) == null);
// A check-in name is not a Git OID.
try testing.expect(loaded.lookupOid(checkin_b) == null);
const ref = loaded.lookupRef("refs/heads/main") orelse return error.TestUnexpectedResult;
try testing.expectEqualStrings(oid_c, ref.oid);
try testing.expect(loaded.lookupRef("refs/heads/mai") == null);
try testing.expect(loaded.lookupRef("main") == null);
}
test "import map: 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 commits = sampleCommits();
const refs = sampleRefs();
try map_mod.write(gpa, io, tmp.dir, sampleContents(&commits, &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, map_mod.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, map_mod.load(gpa, io, tmp.dir));
}
// A flipped byte anywhere past the version is a checksum mismatch.
{
var offset: usize = 10;
while (offset < good.len) : (offset += 5) {
const bad = try gpa.dupe(u8, good);
defer gpa.free(bad);
bad[offset] ^= 0xFF;
try rewrite(io, tmp.dir, bad);
try testing.expectError(error.ChecksumMismatch, map_mod.load(gpa, io, tmp.dir));
}
}
// Truncation anywhere: a named error, never success.
{
var cut: usize = 0;
while (cut < good.len) : (cut += 3) {
try rewrite(io, tmp.dir, good[0..cut]);
if (map_mod.load(gpa, io, tmp.dir)) |_| return error.TestUnexpectedResult else |err| switch (err) {
error.Truncated, error.ChecksumMismatch, error.WrongMagic, error.WrongVersion => {},
else => return err,
}
}
}
// A commit count lying about the body, checksum honest.
{
const bad = try gpa.dupe(u8, good);
defer gpa.free(bad);
std.mem.writeInt(u64, bad[commit_count_at..][0..8], 1 << 40, .little);
try rewriteHonest(io, tmp.dir, bad);
try testing.expectError(error.LyingLength, map_mod.load(gpa, io, tmp.dir));
}
// Trailing bytes the counts do not account for.
{
const bad = try gpa.alloc(u8, good.len + 1);
defer gpa.free(bad);
@memcpy(bad[0 .. good.len - 4], good[0 .. good.len - 4]);
bad[good.len - 4] = 0;
try rewriteHonest(io, tmp.dir, bad);
try testing.expectError(error.LyingLength, map_mod.load(gpa, io, tmp.dir));
}
// Control: the untouched bytes still load.
try rewrite(io, tmp.dir, good);
var loaded = try map_mod.load(gpa, io, tmp.dir);
loaded.deinit();
}
test "import map: each invalid field value has its own error" {
const io = testing.io;
const gpa = testing.allocator;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
const commits = sampleCommits();
const refs = sampleRefs();
try map_mod.write(gpa, io, tmp.dir, sampleContents(&commits, &refs));
const good = try storedBytes(gpa, io, tmp.dir);
defer gpa.free(good);
const Case = struct { at: usize, byte: u8, want: anyerror };
const cases = [_]Case{
.{ .at = object_format_at, .byte = 2, .want = error.UnknownEnum },
.{ .at = submanifest_at, .byte = 9, .want = error.UnknownEnum },
.{ .at = attribution_at, .byte = 2, .want = error.UnknownEnum },
.{ .at = refuse_signed_at, .byte = 2, .want = error.UnknownEnum },
.{ .at = classifier_at, .byte = 0xFF, .want = error.UnknownEnum },
.{ .at = k_at, .byte = 0, .want = error.KOutOfRange },
.{ .at = k_at, .byte = 13, .want = error.KOutOfRange },
// Flat with a nonzero k is not a state the writer can produce.
.{ .at = submanifest_at, .byte = 0, .want = error.KOutOfRange },
.{ .at = first_root_at, .byte = 'A', .want = error.BadHex },
.{ .at = first_commit_oid_at + 39, .byte = 'g', .want = error.BadHex },
// Roots swapped out of ascending order.
.{ .at = first_root_at, .byte = '2', .want = error.NotCanonical },
};
for (cases) |case| {
const bad = try gpa.dupe(u8, good);
defer gpa.free(bad);
bad[case.at] = case.byte;
try rewriteHonest(io, tmp.dir, bad);
try testing.expectError(case.want, map_mod.load(gpa, io, tmp.dir));
}
// A sha1 file whose OIDs are sha256-length is BadHex, not a format
// switch: flipping the object format byte alone mismatches every OID.
{
const bad = try gpa.dupe(u8, good);
defer gpa.free(bad);
bad[object_format_at] = 1;
try rewriteHonest(io, tmp.dir, bad);
try testing.expectError(error.BadHex, map_mod.load(gpa, io, tmp.dir));
}
}
test "import map: duplicate commits and refs are refused at write and at load" {
const io = testing.io;
const gpa = testing.allocator;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
const commits = sampleCommits();
const refs = sampleRefs();
const dup_commits = [_]map_mod.CommitRecord{ commits[0], commits[0] };
try testing.expectError(error.InvalidContents, map_mod.write(gpa, io, tmp.dir, sampleContents(&dup_commits, &refs)));
const dup_refs = [_]map_mod.RefRecord{ refs[0], refs[0] };
try testing.expectError(error.InvalidContents, map_mod.write(gpa, io, tmp.dir, sampleContents(&commits, &dup_refs)));
// Neither refusal published anything.
try testing.expectError(error.Absent, map_mod.load(gpa, io, tmp.dir));
// The load side, reached by rewriting a valid file's second commit OID
// to equal its first.
try map_mod.write(gpa, io, tmp.dir, sampleContents(&commits, &refs));
const good = try storedBytes(gpa, io, tmp.dir);
defer gpa.free(good);
const bad = try gpa.dupe(u8, good);
defer gpa.free(bad);
const first_record_len = 1 + 40 + 1 + 64 + 2 + "trunk".len + 8 + 1;
const second_oid_at = first_commit_oid_at + first_record_len;
@memcpy(bad[second_oid_at..][0..40], oid_root_a);
try rewriteHonest(io, tmp.dir, bad);
try testing.expectError(error.NotCanonical, map_mod.load(gpa, io, tmp.dir));
}
test "import map: a file over the size ceiling is TooLarge before any read" {
const io = testing.io;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
// Sparse: the length is claimed, no data blocks are written.
var f = try tmp.dir.createFile(io, map_mod.map_filename, .{});
defer f.close(io);
try f.setLength(io, map_mod.max_map_bytes + 1);
try testing.expectError(error.TooLarge, map_mod.load(testing.allocator, io, tmp.dir));
}
test "import map: a failed or interrupted write never unseats the published map" {
const io = testing.io;
const gpa = testing.allocator;
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
const commits = sampleCommits();
const refs = sampleRefs();
try map_mod.write(gpa, io, tmp.dir, sampleContents(&commits, &refs));
// A write that fails before the rename (contents the loader would
// refuse: a pinned k of zero) leaves the old map in place.
{
var bad = sampleContents(commits[0..1], &refs);
bad.settings.submanifest = .{ .tree = 0 };
try testing.expectError(error.InvalidContents, map_mod.write(gpa, io, tmp.dir, bad));
var loaded = try map_mod.load(gpa, io, tmp.dir);
defer loaded.deinit();
try testing.expectEqual(@as(usize, 3), loaded.contents.commits.len);
}
// A temp sibling left by a crash between temp write and rename does not
// affect the load.
{
var f = try tmp.dir.createFile(io, map_mod.map_filename ++ ".tmp-crash", .{});
try f.writePositionalAll(io, "torn temp garbage", 0);
f.close(io);
var loaded = try map_mod.load(gpa, io, tmp.dir);
defer loaded.deinit();
try testing.expectEqual(@as(usize, 3), loaded.contents.commits.len);
}
// A successor write replaces the map whole.
{
try map_mod.write(gpa, io, tmp.dir, sampleContents(commits[2..], &refs));
var loaded = try map_mod.load(gpa, io, tmp.dir);
defer loaded.deinit();
try testing.expectEqual(@as(usize, 1), loaded.contents.commits.len);
try testing.expectEqualStrings(oid_c, loaded.contents.commits[0].oid);
}
}