Skip to content
zit Artifact [0bc032d4e5]
Login

Artifact [0bc032d4e5]

Artifact 0bc032d4e5da4f2cdc2319ae07e9f9fec6c0fd39bf22243ab973e58780b06439:


//! Export planner + seam-enumeration tests: strict topological order with
//! the timeline order as the ready tiebreak, mark-map exclusion, and the
//! §9.3 seam-equivalence proof — the SAME file set enumerated identically
//! through a flat manifest, an S-card tree, and a delta manifest, with the
//! tree case proven to have gone through `Manifest.flattenTree` via the
//! sub-manifest memo's own counters (zero bespoke S-card walks).

const std = @import("std");
const artifact = @import("../artifact.zig");
const logstore = @import("../logstore.zig");
const storage = @import("../storage.zig");
const repostate = @import("../repostate.zig");
const Repository = @import("../repository.zig").Repository;
const export_impl = @import("git_export_impl.zig");

const testing = std.testing;

// ---------------------------------------------------------------------------
// Planner ordering over synthetic RepoState graphs.
// ---------------------------------------------------------------------------

const h = struct {
    fn n(comptime c: u8) *const [64]u8 {
        return &(.{c} ** 64);
    }
};

fn crosslinkChain(rs: *repostate.RepoState) !void {
    _ = try rs.crosslink(.{ .hash = h.n('a'), .mtime = 10, .tags = &.{
        .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" },
    } });
    _ = try rs.crosslink(.{ .hash = h.n('b'), .mtime = 20, .parents = &.{h.n('a')} });
    _ = try rs.crosslink(.{ .hash = h.n('c'), .mtime = 30, .parents = &.{h.n('b')} });
}

fn indexOf(order: []const repostate.Rid, rid: repostate.Rid) usize {
    for (order, 0..) |r, i| {
        if (r == rid) return i;
    }
    unreachable;
}

fn expectParentsFirst(rs: *const repostate.RepoState, order: []const repostate.Rid) !void {
    for (order) |rid| {
        var it = rs.parentsOf(rid);
        while (it.next()) |parent| {
            if (rs.mtimeOf(parent) == null) continue; // phantom parent: not planned
            try testing.expect(indexOf(order, parent) < indexOf(order, rid));
        }
    }
}

test "git export planner: linear history plans oldest-first, every parent before its child" {
    const gpa = testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var rs = repostate.RepoState.init(gpa, ms.store());
    defer rs.deinit();
    try crosslinkChain(&rs);

    var nothing = export_impl.NoneExported{};
    const order = try export_impl.plan(gpa, &rs, nothing.predicate());
    defer gpa.free(order);
    try testing.expectEqual(@as(usize, 3), order.len);
    try expectParentsFirst(&rs, order);
    // Deterministic and oldest-first on a linear chain.
    try testing.expectEqualStrings(h.n('a'), ms.store().hashOf(order[0]));
    try testing.expectEqualStrings(h.n('c'), ms.store().hashOf(order[2]));
}

test "git export planner: fork, merge, octopus, and an mtime-skewed child all stay topological" {
    const gpa = testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var rs = repostate.RepoState.init(gpa, ms.store());
    defer rs.deinit();

    _ = try rs.crosslink(.{ .hash = h.n('a'), .mtime = 10, .tags = &.{
        .{ .type = .propagate, .name = "sym-trunk", .target = null, .value = "trunk" },
    } });
    _ = try rs.crosslink(.{ .hash = h.n('b'), .mtime = 20, .parents = &.{h.n('a')} });
    _ = try rs.crosslink(.{ .hash = h.n('c'), .mtime = 21, .parents = &.{h.n('a')} });
    _ = try rs.crosslink(.{ .hash = h.n('d'), .mtime = 22, .parents = &.{h.n('a')} });
    // Octopus-ish merge of three lines.
    _ = try rs.crosslink(.{ .hash = h.n('e'), .mtime = 30, .parents = &.{ h.n('b'), h.n('c'), h.n('d') } });
    // Mtime-skewed child: D card EARLIER than its parent's — must still plan
    // after it (topology outranks the clock).
    _ = try rs.crosslink(.{ .hash = h.n('f'), .mtime = 5, .parents = &.{h.n('e')} });

    var nothing = export_impl.NoneExported{};
    const order = try export_impl.plan(gpa, &rs, nothing.predicate());
    defer gpa.free(order);
    try testing.expectEqual(@as(usize, 6), order.len);
    try expectParentsFirst(&rs, order);
}

test "git export planner: a pre-seeded mark map excludes exported prefixes from the frontier" {
    const gpa = testing.allocator;
    var ms = repostate.MemStore.init(gpa);
    defer ms.deinit();
    var rs = repostate.RepoState.init(gpa, ms.store());
    defer rs.deinit();
    try crosslinkChain(&rs);

    // 'a' and 'b' already exported: only 'c' plans, and its recorded parent
    // counts as satisfied rather than blocking.
    var seeded = export_impl.SetExported{};
    defer seeded.deinit(gpa);
    try seeded.hashes.put(gpa, h.n('a'), {});
    try seeded.hashes.put(gpa, h.n('b'), {});

    const order = try export_impl.plan(gpa, &rs, seeded.predicate());
    defer gpa.free(order);
    try testing.expectEqual(@as(usize, 1), order.len);
    try testing.expectEqualStrings(h.n('c'), ms.store().hashOf(order[0]));
}

// ---------------------------------------------------------------------------
// Seam equivalence: flat, S-card tree, and delta manifests with ONE file set.
// ---------------------------------------------------------------------------

const FixtureRepo = struct {
    tmp: std.testing.TmpDir,
    fs: storage.FileStorage,
    log: logstore.LogStore,
    raw: std.Io.File,
    io: std.Io,

    fn init(self: *FixtureRepo, io: std.Io) !void {
        self.io = io;
        self.tmp = testing.tmpDir(.{});
        self.raw = try self.tmp.dir.createFile(io, ".zit", .{ .read = true });
        self.fs = storage.FileStorage.init(io, self.raw, 0);
        self.log = try logstore.LogStore.open(testing.allocator, self.fs.storageFile(), 0);
        try self.log.putConfig(.{ .hash_policy = 0, .default_branch = "trunk", .project_id = "p" });
    }

    fn seal(self: *FixtureRepo) !void {
        try self.log.commit();
        self.log.deinit(testing.allocator);
        self.raw.close(self.io);
    }

    fn cleanup(self: *FixtureRepo) void {
        self.tmp.cleanup();
    }

    fn putRaw(self: *FixtureRepo, bytes: []const u8) ![]u8 {
        var digest: [32]u8 = undefined;
        std.crypto.hash.sha3.Sha3_256.hash(bytes, &digest, .{});
        _ = try self.log.put(&digest, bytes);
        const out = try testing.allocator.alloc(u8, 64);
        const digits = "0123456789abcdef";
        for (&digest, 0..) |b, i| {
            out[i * 2] = digits[b >> 4];
            out[i * 2 + 1] = digits[b & 0x0f];
        }
        return out;
    }
};

fn fcard(name: []const u8, hash: []const u8) artifact.FCard {
    return .{ .filename = name, .hash = hash, .perm = null, .old_name = null, .mode = .regular };
}

fn baseManifest(ts: []const u8, parent: ?[]const u8) artifact.Manifest {
    _ = parent;
    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 = &.{},
        .q = &.{},
        .t = &.{.{ .sign = .propagate, .name = "branch", .target = null, .value = "trunk" }},
    };
}

fn collect(gpa: std.mem.Allocator, files: artifact.ManifestFiles, out: *std.ArrayList(artifact.FCard)) !void {
    var it = files.iterator();
    while (it.next()) |f| try out.append(gpa, f);
}

test "git export seam: flat, S-card tree, and delta manifests enumerate one identical file set through flattenTree" {
    const io = testing.io;
    const gpa = testing.allocator;
    var repo: FixtureRepo = undefined;
    try repo.init(io);
    defer repo.cleanup();

    // The shared file set — names diverge on the first byte so k=1 partitions.
    const b1 = try repo.putRaw("alpha\n");
    defer gpa.free(b1);
    const b2 = try repo.putRaw("beta\n");
    defer gpa.free(b2);
    const b3 = try repo.putRaw("gamma\n");
    defer gpa.free(b3);
    const cards = [_]artifact.FCard{
        fcard("adir/a.txt", b1),
        fcard("bdir/b.txt", b2),
        fcard("cdir/c.txt", b3),
    };

    // 1. FLAT manifest.
    var flat_m = baseManifest("2020-01-01T00:00:00", null);
    defer flat_m.deinit();
    flat_m.f = &cards;
    const flat_bytes = try artifact.create(&flat_m, gpa);
    defer gpa.free(flat_bytes);
    const flat_hash = try repo.putRaw(flat_bytes);
    defer gpa.free(flat_hash);

    // 2. S-card TREE over the same set: store every emitted node, then a root
    //    whose S run replaces the F run.
    var build = try artifact.buildTree(gpa, &cards, 1, .sha3_256);
    defer build.deinit();
    try testing.expect(!build.isFlat());
    for (build.subs) |sub| {
        const name = try repo.putRaw(sub.bytes);
        gpa.free(name);
    }
    var tree_m = baseManifest("2020-01-02T00:00:00", null);
    defer tree_m.deinit();
    tree_m.s = build.s;
    const tree_bytes = try artifact.create(&tree_m, gpa);
    defer gpa.free(tree_bytes);
    const tree_hash = try repo.putRaw(tree_bytes);
    defer gpa.free(tree_hash);

    // 3. DELTA manifest: baseline carries two of the cards, the delta patches
    //    in the third — same effective set.
    var base_m = baseManifest("2020-01-03T00:00:00", null);
    defer base_m.deinit();
    base_m.f = cards[0..2];
    const base_bytes = try artifact.create(&base_m, gpa);
    defer gpa.free(base_bytes);
    const base_hash = try repo.putRaw(base_bytes);
    defer gpa.free(base_hash);
    var delta_m = baseManifest("2020-01-04T00:00:00", null);
    defer delta_m.deinit();
    delta_m.b = .{ .hash = base_hash };
    delta_m.f = cards[2..3];
    const delta_bytes = try artifact.create(&delta_m, gpa);
    defer gpa.free(delta_bytes);
    const delta_hash = try repo.putRaw(delta_bytes);
    defer gpa.free(delta_hash);

    try repo.seal();

    var r: Repository = undefined;
    try Repository.open(&r, gpa, io, repo.tmp.dir, .{});
    defer r.deinit(gpa);

    var reference: std.ArrayList(artifact.FCard) = .empty;
    defer reference.deinit(gpa);
    for ([_][]const u8{ flat_hash, tree_hash, delta_hash }) |hash| {
        const rid = r.lookup(hash) orelse return error.TestUnexpectedResult;
        var fl = (try r.files(gpa, rid)) orelse return error.TestUnexpectedResult;
        defer fl.deinit(gpa);
        var got: std.ArrayList(artifact.FCard) = .empty;
        defer got.deinit(gpa);
        var it = fl.iterator();
        while (it.next()) |f| try got.append(gpa, f);
        try testing.expectEqual(@as(usize, 3), got.items.len);
        if (reference.items.len == 0) {
            try reference.appendSlice(gpa, got.items);
            // Reference cards borrow fl — dupe names for later comparison.
            for (reference.items) |*card| {
                card.filename = try gpa.dupe(u8, card.filename);
                card.hash = try gpa.dupe(u8, card.hash.?);
            }
        } else {
            for (reference.items, got.items) |want, gotc| {
                try testing.expectEqualStrings(want.filename, gotc.filename);
                try testing.expectEqualStrings(want.hash.?, gotc.hash.?);
            }
        }
    }
    for (reference.items) |card| {
        gpa.free(card.filename);
        gpa.free(card.hash.?);
    }
    reference.clearRetainingCapacity();

    // The seam counter: the TREE enumeration went through flattenTree's memo
    // (misses were recorded for its sub-manifests) — zero bespoke S walks.
    const memo = &r.sub_memo.?;
    try testing.expect(memo.misses >= 1);
}