//! Private `zit_update.applyCheckin` contract tests.
//!
//! Tests that require update.zig file-private helpers or fault-injection seams
//! remain inline with that implementation.
const std = @import("std");
const zit = @import("zit");
const artifact = zit.artifact;
const checkout = @import("zit_checkout");
const logstore = @import("zit_logstore");
const managed = zit.managed;
const storage = @import("zit_storage");
const update = @import("update.zig");
const ApplyError = update.ApplyError;
const applyCheckin = update.applyCheckin;
fn decodeValidatedName(name: []const u8, out: *[logstore.max_hash_len]u8) ApplyError![]const u8 {
if (!artifact.validateName(name)) return error.InvalidCheckinId;
const raw = out[0 .. name.len / 2];
_ = std.fmt.hexToBytes(raw, name) catch return error.InvalidCheckinId;
return raw;
}
fn completeTestArtifact(gpa: std.mem.Allocator, body: []const u8) ![]u8 {
var checksum_buf: [32]u8 = undefined;
const checksum = artifact.selfChecksum(body, &checksum_buf);
return std.fmt.allocPrint(gpa, "{s}Z {s}\n", .{ body, checksum });
}
fn putTestArtifact(log: *logstore.LogStore, name: []const u8, bytes: []const u8) !void {
var digest: [logstore.max_hash_len]u8 = undefined;
const raw = try decodeValidatedName(name, &digest);
_ = try log.put(raw, bytes);
}
fn putCompleteTestArtifact(
gpa: std.mem.Allocator,
log: *logstore.LogStore,
body: []const u8,
id_buf: *[64]u8,
) ![]const u8 {
const bytes = try completeTestArtifact(gpa, body);
defer gpa.free(bytes);
const id = artifact.artifactName(.sha1, bytes, id_buf);
try putTestArtifact(log, id, bytes);
return id;
}
fn putTestPayload(
log: *logstore.LogStore,
bytes: []const u8,
id_buf: *[64]u8,
) ![]const u8 {
const id = artifact.artifactName(.sha1, bytes, id_buf);
try putTestArtifact(log, id, bytes);
return id;
}
fn putDeltaTestTarget(
gpa: std.mem.Allocator,
log: *logstore.LogStore,
baseline_id: []const u8,
id_buf: *[64]u8,
) ![]const u8 {
const body = try std.fmt.allocPrint(gpa, "B {s}\nD 2007-07-21T14:10:00\nF next {s}\n", .{
baseline_id,
"a" ** 40,
});
defer gpa.free(body);
return putCompleteTestArtifact(gpa, log, body, id_buf);
}
fn writeFixturePayload(
io: std.Io,
root: std.Io.Dir,
path: []const u8,
bytes: []const u8,
mode: artifact.FileMode,
) !void {
var file = try root.createFile(io, path, .{
.read = false,
.exclusive = true,
.permissions = switch (mode) {
.regular => .default_file,
.executable => .executable_file,
.symlink => unreachable,
},
.resolve_beneath = true,
});
defer file.close(io);
try file.writePositionalAll(io, bytes, 0);
try file.sync(io);
}
fn modeMatches(stat: std.Io.File.Stat, mode: artifact.FileMode) bool {
if (comptime std.Io.File.Permissions.has_executable_bit) {
const executable = stat.permissions.toMode() & 0o111 != 0;
return switch (mode) {
.regular => !executable,
.executable => executable,
.symlink => false,
};
}
return mode != .symlink;
}
fn expectFileState(
gpa: std.mem.Allocator,
io: std.Io,
worktree: std.Io.Dir,
path: []const u8,
expected_bytes: []const u8,
expected_mode: artifact.FileMode,
) !void {
const stat = try worktree.statFile(io, path, .{ .follow_symlinks = false });
try std.testing.expectEqual(.file, stat.kind);
try std.testing.expect(modeMatches(stat, expected_mode));
var file = try worktree.openFile(io, path, .{
.allow_directory = false,
.follow_symlinks = false,
.resolve_beneath = true,
});
defer file.close(io);
const bytes = try gpa.alloc(u8, stat.size);
defer gpa.free(bytes);
const n = try file.readPositionalAll(io, bytes, 0);
try std.testing.expectEqual(expected_bytes.len, n);
try std.testing.expectEqualStrings(expected_bytes, bytes);
}
const RootEntrySet = struct {
names: std.ArrayList([]u8) = .empty,
fn deinit(self: *RootEntrySet, gpa: std.mem.Allocator) void {
for (self.names.items) |name| gpa.free(name);
self.names.deinit(gpa);
self.* = undefined;
}
fn contains(self: *const RootEntrySet, name: []const u8) bool {
for (self.names.items) |candidate| {
if (std.mem.eql(u8, candidate, name)) return true;
}
return false;
}
};
fn snapshotRootEntries(gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir) !RootEntrySet {
var entries = RootEntrySet{};
errdefer entries.deinit(gpa);
var iterator = worktree.iterate();
while (try iterator.next(io)) |entry| {
const name = try gpa.dupe(u8, entry.name);
entries.names.append(gpa, name) catch |err| {
gpa.free(name);
return err;
};
}
return entries;
}
fn expectSameRootEntries(gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir, expected: *const RootEntrySet) !void {
var actual = try snapshotRootEntries(gpa, io, worktree);
defer actual.deinit(gpa);
try std.testing.expectEqual(expected.names.items.len, actual.names.items.len);
for (expected.names.items) |name| try std.testing.expect(actual.contains(name));
}
fn seedUntouchedWorktree(gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir) !void {
try checkout.write(io, worktree, .{ .version = "old-checkout", .branch = "old-branch" });
var old_managed = managed.ManagedSet.init(gpa);
defer old_managed.deinit();
_ = try old_managed.insert("old-file");
try old_managed.write(io, worktree);
try writeFixturePayload(io, worktree, "old-file", "old executable bytes", .executable);
try writeFixturePayload(io, worktree, "witness", "unmanaged witness bytes", .regular);
}
fn expectUntouchedWorktree(gpa: std.mem.Allocator, io: std.Io, worktree: std.Io.Dir) !void {
var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined;
const state = (try checkout.read(io, worktree, &checkout_buf)).?;
try std.testing.expectEqualStrings("old-checkout", state.version);
try std.testing.expectEqualStrings("old-branch", state.branch);
var current_managed = try managed.ManagedSet.read(gpa, io, worktree);
defer current_managed.deinit();
try std.testing.expectEqual(@as(usize, 1), current_managed.iterate().len);
try std.testing.expect(current_managed.contains("old-file"));
try expectFileState(gpa, io, worktree, "old-file", "old executable bytes", .executable);
try expectFileState(gpa, io, worktree, "witness", "unmanaged witness bytes", .regular);
}
fn setupRollbackFailureFixture(
gpa: std.mem.Allocator,
io: std.Io,
worktree: std.Io.Dir,
log: *logstore.LogStore,
target_id_buf: *[64]u8,
old_id_buf: *[64]u8,
) ![]const u8 {
var payload_id_buf: [64]u8 = undefined;
const payload_id = artifact.artifactName(.sha1, "", &payload_id_buf);
try putTestArtifact(log, payload_id, "");
const old_body = try std.fmt.allocPrint(gpa, "D 2007-07-21T14:09:59\nF old {s}\n", .{payload_id});
defer gpa.free(old_body);
const old_bytes = try completeTestArtifact(gpa, old_body);
defer gpa.free(old_bytes);
const old_id = artifact.artifactName(.sha1, old_bytes, old_id_buf);
try putTestArtifact(log, old_id, old_bytes);
const target_body = try std.fmt.allocPrint(gpa, "D 2007-07-21T14:10:00\nF new {s}\n", .{payload_id});
defer gpa.free(target_body);
const target_bytes = try completeTestArtifact(gpa, target_body);
defer gpa.free(target_bytes);
const target_id = artifact.artifactName(.sha1, target_bytes, target_id_buf);
try putTestArtifact(log, target_id, target_bytes);
try checkout.write(io, worktree, .{ .version = old_id, .branch = "trunk" });
var old_managed = managed.ManagedSet.init(gpa);
defer old_managed.deinit();
_ = try old_managed.insert("old");
try old_managed.write(io, worktree);
try writeFixturePayload(io, worktree, "old", "", .regular);
return target_id;
}
fn retainedTransactionContainsPrevious(io: std.Io, worktree: std.Io.Dir) !bool {
var iterator = worktree.iterate();
while (try iterator.next(io)) |entry| {
if (entry.kind != .directory or !std.mem.startsWith(u8, entry.name, ".zit-update-txn-")) continue;
var transaction = try worktree.openDir(io, entry.name, .{ .access_sub_paths = true, .follow_symlinks = false });
defer transaction.close(io);
var previous = transaction.openDir(io, "previous", .{ .access_sub_paths = true, .follow_symlinks = false }) catch continue;
defer previous.close(io);
const old = previous.statFile(io, "old", .{ .follow_symlinks = false }) catch continue;
if (old.kind == .file) return true;
}
return false;
}
test "update strict target and baseline failures leave the checkout untouched" {
const t = std.testing;
const Case = struct {
kind: enum {
target_missing,
target_phantom,
target_misnamed,
target_non_manifest,
baseline_missing,
baseline_phantom,
baseline_misnamed,
baseline_non_manifest,
baseline_delta,
},
expected: ApplyError,
};
const cases = [_]Case{
.{ .kind = .target_missing, .expected = error.CheckinNotFound },
.{ .kind = .target_phantom, .expected = error.CheckinContentMissing },
.{ .kind = .target_misnamed, .expected = error.CheckinNameMismatch },
.{ .kind = .target_non_manifest, .expected = error.TargetNotManifest },
.{ .kind = .baseline_missing, .expected = error.BaselineNotFound },
.{ .kind = .baseline_phantom, .expected = error.BaselineContentMissing },
.{ .kind = .baseline_misnamed, .expected = error.BaselineNameMismatch },
.{ .kind = .baseline_non_manifest, .expected = error.BaselineNotManifest },
.{ .kind = .baseline_delta, .expected = error.BaselineIsDelta },
};
for (cases) |case| {
var tmp = t.tmpDir(.{});
defer tmp.cleanup();
const io = t.io;
var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true });
defer raw_file.close(io);
var file_storage = storage.FileStorage.init(io, raw_file, 0);
var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0);
defer log.deinit(t.allocator);
try seedUntouchedWorktree(t.allocator, io, tmp.dir);
var root_entries = try snapshotRootEntries(t.allocator, io, tmp.dir);
defer root_entries.deinit(t.allocator);
var target_id_buf: [64]u8 = undefined;
var target_id: []const u8 = undefined;
switch (case.kind) {
.target_missing => target_id = "a" ** 40,
.target_phantom => {
target_id = "b" ** 40;
var digest: [logstore.max_hash_len]u8 = undefined;
_ = try log.intern(try decodeValidatedName(target_id, &digest));
},
.target_misnamed => {
const expected_bytes = try completeTestArtifact(t.allocator, "D 2007-07-21T14:09:59\nF expected aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n");
defer t.allocator.free(expected_bytes);
target_id = artifact.artifactName(.sha1, expected_bytes, &target_id_buf);
const actual_bytes = try completeTestArtifact(t.allocator, "D 2007-07-21T14:10:00\nF actual bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n");
defer t.allocator.free(actual_bytes);
try putTestArtifact(&log, target_id, actual_bytes);
},
.target_non_manifest => {
target_id = try putCompleteTestArtifact(t.allocator, &log, "D 2007-07-21T14:09:59\nT +branch aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa trunk\nU alice\n", &target_id_buf);
},
.baseline_missing => {
target_id = try putDeltaTestTarget(t.allocator, &log, "c" ** 40, &target_id_buf);
},
.baseline_phantom => {
const baseline_id = "d" ** 40;
var digest: [logstore.max_hash_len]u8 = undefined;
_ = try log.intern(try decodeValidatedName(baseline_id, &digest));
target_id = try putDeltaTestTarget(t.allocator, &log, baseline_id, &target_id_buf);
},
.baseline_misnamed => {
const expected_bytes = try completeTestArtifact(t.allocator, "D 2007-07-21T14:09:59\nF expected aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n");
defer t.allocator.free(expected_bytes);
var baseline_id_buf: [64]u8 = undefined;
const baseline_id = artifact.artifactName(.sha1, expected_bytes, &baseline_id_buf);
const actual_bytes = try completeTestArtifact(t.allocator, "D 2007-07-21T14:10:00\nF actual bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n");
defer t.allocator.free(actual_bytes);
try putTestArtifact(&log, baseline_id, actual_bytes);
target_id = try putDeltaTestTarget(t.allocator, &log, baseline_id, &target_id_buf);
},
.baseline_non_manifest => {
var baseline_id_buf: [64]u8 = undefined;
const baseline_id = try putCompleteTestArtifact(t.allocator, &log, "D 2007-07-21T14:09:59\nT +branch aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa trunk\nU alice\n", &baseline_id_buf);
target_id = try putDeltaTestTarget(t.allocator, &log, baseline_id, &target_id_buf);
},
.baseline_delta => {
var baseline_id_buf: [64]u8 = undefined;
const baseline_body = try std.fmt.allocPrint(t.allocator, "B {s}\nD 2007-07-21T14:09:59\nF inherited aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", .{"e" ** 40});
defer t.allocator.free(baseline_body);
const baseline_id = try putCompleteTestArtifact(t.allocator, &log, baseline_body, &baseline_id_buf);
target_id = try putDeltaTestTarget(t.allocator, &log, baseline_id, &target_id_buf);
},
}
try t.expectError(case.expected, applyCheckin(t.allocator, io, tmp.dir, &log, target_id));
try expectUntouchedWorktree(t.allocator, io, tmp.dir);
try expectSameRootEntries(t.allocator, io, tmp.dir, &root_entries);
}
}
test "update delta applies inherited replacements and deletions across multiple entries" {
const t = std.testing;
var tmp = t.tmpDir(.{});
defer tmp.cleanup();
const io = t.io;
var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true });
defer raw_file.close(io);
var file_storage = storage.FileStorage.init(io, raw_file, 0);
var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0);
defer log.deinit(t.allocator);
var inherited_id_buf: [64]u8 = undefined;
const inherited_id = try putTestPayload(&log, "inherited bytes", &inherited_id_buf);
var original_id_buf: [64]u8 = undefined;
const original_id = try putTestPayload(&log, "original bytes", &original_id_buf);
var removed_id_buf: [64]u8 = undefined;
const removed_id = try putTestPayload(&log, "removed bytes", &removed_id_buf);
var replacement_id_buf: [64]u8 = undefined;
const replacement_id = try putTestPayload(&log, "replacement bytes", &replacement_id_buf);
const baseline_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF inherited {s}\nF removed {s}\nF replaced {s}\n", .{
inherited_id,
removed_id,
original_id,
});
defer t.allocator.free(baseline_body);
var baseline_id_buf: [64]u8 = undefined;
const baseline_id = try putCompleteTestArtifact(t.allocator, &log, baseline_body, &baseline_id_buf);
const target_body = try std.fmt.allocPrint(t.allocator, "B {s}\nD 2007-07-21T14:10:00\nF removed\nF replaced {s}\nF unmatched\n", .{
baseline_id,
replacement_id,
});
defer t.allocator.free(target_body);
var target_id_buf: [64]u8 = undefined;
const target_id = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_id_buf);
try applyCheckin(t.allocator, io, tmp.dir, &log, target_id);
try expectFileState(t.allocator, io, tmp.dir, "inherited", "inherited bytes", .regular);
try expectFileState(t.allocator, io, tmp.dir, "replaced", "replacement bytes", .regular);
try t.expectError(error.FileNotFound, tmp.dir.statFile(io, "removed", .{ .follow_symlinks = false }));
try t.expectError(error.FileNotFound, tmp.dir.statFile(io, "unmatched", .{ .follow_symlinks = false }));
var current_managed = try managed.ManagedSet.read(t.allocator, io, tmp.dir);
defer current_managed.deinit();
try t.expectEqual(@as(usize, 2), current_managed.iterate().len);
try t.expect(current_managed.contains("inherited"));
try t.expect(current_managed.contains("replaced"));
var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined;
const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?;
try t.expectEqualStrings(target_id, state.version);
}
test "update non-ancestor control tags use persisted frame order over RID order" {
const t = std.testing;
var tmp = t.tmpDir(.{});
defer tmp.cleanup();
const io = t.io;
var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true });
defer raw_file.close(io);
var file_storage = storage.FileStorage.init(io, raw_file, 0);
var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0);
defer log.deinit(t.allocator);
var target_payload_id_buf: [64]u8 = undefined;
const target_payload_id = try putTestPayload(&log, "target bytes", &target_payload_id_buf);
const target_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:09:59\nF target {s}\n", .{target_payload_id});
defer t.allocator.free(target_body);
var target_id_buf: [64]u8 = undefined;
const target_id = try putCompleteTestArtifact(t.allocator, &log, target_body, &target_id_buf);
const RID_first_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nT +branch {s} rid-first\nU alice\n", .{target_id});
defer t.allocator.free(RID_first_body);
const RID_first_bytes = try completeTestArtifact(t.allocator, RID_first_body);
defer t.allocator.free(RID_first_bytes);
var RID_first_id_buf: [64]u8 = undefined;
const RID_first_id = artifact.artifactName(.sha1, RID_first_bytes, &RID_first_id_buf);
var RID_first_digest: [logstore.max_hash_len]u8 = undefined;
const RID_first = try log.intern(try decodeValidatedName(RID_first_id, &RID_first_digest));
const persisted_first_body = try std.fmt.allocPrint(t.allocator, "D 2007-07-21T14:10:00\nT +branch {s} persisted-first\nU alice\n", .{target_id});
defer t.allocator.free(persisted_first_body);
const persisted_first_bytes = try completeTestArtifact(t.allocator, persisted_first_body);
defer t.allocator.free(persisted_first_bytes);
var persisted_first_id_buf: [64]u8 = undefined;
const persisted_first_id = artifact.artifactName(.sha1, persisted_first_bytes, &persisted_first_id_buf);
try putTestArtifact(&log, persisted_first_id, persisted_first_bytes);
var persisted_first_digest: [logstore.max_hash_len]u8 = undefined;
const persisted_first = log.lookup(try decodeValidatedName(persisted_first_id, &persisted_first_digest)).?;
try t.expect(RID_first < persisted_first);
try putTestArtifact(&log, RID_first_id, RID_first_bytes);
try applyCheckin(t.allocator, io, tmp.dir, &log, target_id);
var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined;
const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?;
try t.expectEqualStrings("persisted-first", state.branch);
}
test "update applyCheckin rejects a non-manifest before checkout mutation" {
const t = std.testing;
var tmp = t.tmpDir(.{});
defer tmp.cleanup();
const io = t.io;
var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true });
defer raw_file.close(io);
var file_storage = storage.FileStorage.init(io, raw_file, 0);
var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0);
defer log.deinit(t.allocator);
const payload = "ordinary blob, not a structural artifact";
var id_buf: [64]u8 = undefined;
const target_id = artifact.artifactName(.sha1, payload, &id_buf);
var digest: [logstore.max_hash_len]u8 = undefined;
const raw_digest = try decodeValidatedName(target_id, &digest);
_ = try log.put(raw_digest, payload);
try checkout.write(io, tmp.dir, .{ .version = "old", .branch = "trunk" });
try t.expectError(error.TargetNotManifest, applyCheckin(t.allocator, io, tmp.dir, &log, target_id));
var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined;
const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?;
try t.expectEqualStrings("old", state.version);
try t.expectEqualStrings("trunk", state.branch);
}
test "update retains the staged rollback tree when inverse rollback fails" {
const t = std.testing;
const io = t.io;
var probe_tmp = t.tmpDir(.{});
defer probe_tmp.cleanup();
var probe_raw_file = try probe_tmp.dir.createFile(io, "repo.zit", .{ .read = true });
defer probe_raw_file.close(io);
var probe_storage = storage.FileStorage.init(io, probe_raw_file, 0);
var probe_log = try logstore.LogStore.open(t.allocator, probe_storage.storageFile(), 0);
defer probe_log.deinit(t.allocator);
var probe_target_id_buf: [64]u8 = undefined;
var probe_old_id_buf: [64]u8 = undefined;
const probe_target_id = try setupRollbackFailureFixture(t.allocator, io, probe_tmp.dir, &probe_log, &probe_target_id_buf, &probe_old_id_buf);
var probe_allocator = std.testing.FailingAllocator.init(t.allocator, .{});
try applyCheckin(probe_allocator.allocator(), io, probe_tmp.dir, &probe_log, probe_target_id);
const allocation_count = probe_allocator.alloc_index;
try t.expect(allocation_count > 0);
var tmp = t.tmpDir(.{});
defer tmp.cleanup();
var raw_file = try tmp.dir.createFile(io, "repo.zit", .{ .read = true });
defer raw_file.close(io);
var file_storage = storage.FileStorage.init(io, raw_file, 0);
var log = try logstore.LogStore.open(t.allocator, file_storage.storageFile(), 0);
defer log.deinit(t.allocator);
var target_id_buf: [64]u8 = undefined;
var old_id_buf: [64]u8 = undefined;
const target_id = try setupRollbackFailureFixture(t.allocator, io, tmp.dir, &log, &target_id_buf, &old_id_buf);
var failing_allocator = std.testing.FailingAllocator.init(t.allocator, .{ .fail_index = allocation_count - 1 });
try t.expectError(error.RollbackFailed, applyCheckin(failing_allocator.allocator(), io, tmp.dir, &log, target_id));
try t.expect(failing_allocator.has_induced_failure);
try t.expect(try retainedTransactionContainsPrevious(io, tmp.dir));
try t.expect((try tmp.dir.statFile(io, "old", .{ .follow_symlinks = false })).kind == .file);
try t.expectError(error.FileNotFound, tmp.dir.statFile(io, "new", .{ .follow_symlinks = false }));
var checkout_buf: [checkout.max_ck_payload + 1]u8 = undefined;
const state = (try checkout.read(io, tmp.dir, &checkout_buf)).?;
try t.expectEqualStrings(old_id_buf[0..40], state.version);
var restored_managed = try managed.ManagedSet.read(t.allocator, io, tmp.dir);
defer restored_managed.deinit();
try t.expect(restored_managed.contains("old"));
}