//! Fossil interop smoke check — the one sanctioned use of the real `fossil`
//! binary. `fossil` is invoked nowhere else in this tree.
//!
//! Policy, enforced by this file's shape, not by convention:
//! - Every byte handed to fossil is zit's OWN output (`artifact.create` /
//! `delta.create` over an RFC fixture) -- never the raw RFC transcription,
//! never anything fossil produced.
//! - fossil may only CONFIRM an RFC-cited zit expectation. A scenario that
//! would require comparing zit bytes to fossil-AUTHORED bytes (e.g.
//! `fossil test-delta-create`) does not belong here.
//! - On any mismatch, print the diff and the RFC citation and exit 1. This
//! file never adjusts zit to match fossil -- a mismatch is an operator
//! decision (RFC misread vs RFC wrong), recorded in the untracked
//! ambiguities ledger, not "fixed" in place.
//!
//! Milestone-manual: this executable lives solely behind `zig build smoke`.
//! `zig build test` compiles it (catching type errors) but never runs its
//! scenarios; it is not imported by `root.zig`'s test aggregation.
const std = @import("std");
const zit = @import("zit");
const artifact = zit.artifact;
const delta = zit.delta;
// ---------------------------------------------------------------------------
// RFC fixtures -- the same vectors src/artifact.zig's own tests cite.
// ---------------------------------------------------------------------------
const a1_1_manifest = @embedFile("rfc_vectors/artifact_a1_1_sha1_manifest.txt");
const a1_2_technote = @embedFile("rfc_vectors/artifact_a1_2_sha3_technote.txt");
const a3_manifest = @embedFile("rfc_vectors/artifact_a3_rcard_manifest.txt");
/// §5/Appendix B.1 delta worked-example pair (102/151-byte sizes), reused
/// verbatim from delta.zig's own "create() then apply() round-trips the
/// target" test so S4 exercises the same RFC-cited content.
const delta_source = "The quick brown fox jumps over the lazy dog. A Fossil delta encodes byte-level differences compactly.\n";
const delta_target = "The quick brown fox jumps over the sleeping fox. A Fossil delta encodes byte-level differences compactly.\n An example delta target with appended tail.\n";
const ManifestScenario = struct {
name: []const u8,
citation: []const u8,
fixture: []const u8,
algo: artifact.HashAlgo,
};
const manifest_scenarios = [_]ManifestScenario{
.{ .name = "S1", .citation = "§A.1.1/§A.2", .fixture = a1_1_manifest, .algo = .sha1 },
.{ .name = "S2", .citation = "§A.1.2", .fixture = a1_2_technote, .algo = .sha3_256 },
.{ .name = "S3", .citation = "§A.3", .fixture = a3_manifest, .algo = .sha3_256 },
};
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
const io = init.io;
var stdout_buffer: [4096]u8 = undefined;
var stdout_writer: std.Io.File.Writer = .init(.stdout(), io, &stdout_buffer);
const out = &stdout_writer.interface;
if (!fossilAvailable(gpa, io)) {
try out.writeAll("fossil not found — smoke is milestone-manual\n");
try out.flush();
return;
}
var arena_state = std.heap.ArenaAllocator.init(gpa);
defer arena_state.deinit();
const arena = arena_state.allocator();
const tmp_root = try tempRoot(arena, init.environ_map.*);
std.Io.Dir.cwd().deleteTree(io, tmp_root) catch {};
try std.Io.Dir.cwd().createDirPath(io, tmp_root);
defer std.Io.Dir.cwd().deleteTree(io, tmp_root) catch {};
var env = try init.environ_map.clone(gpa);
defer env.deinit();
const home_dir = try std.fs.path.join(arena, &.{ tmp_root, "home" });
try std.Io.Dir.cwd().createDirPath(io, home_dir);
try env.put("FOSSIL_HOME", home_dir);
var all_pass = true;
for (manifest_scenarios) |sc| {
const pass = runManifestScenario(arena, gpa, io, tmp_root, &env, out, sc) catch |err| blk: {
try out.print("FAIL {s} ({s}): unexpected error running scenario: {s}\n", .{ sc.name, sc.citation, @errorName(err) });
break :blk false;
};
all_pass = all_pass and pass;
}
const s4_pass = runDeltaScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: {
try out.print("FAIL S4 (§5/§Appendix B.1): unexpected error running scenario: {s}\n", .{@errorName(err)});
break :blk false;
};
all_pass = all_pass and s4_pass;
try out.flush();
if (!all_pass) std.process.exit(1);
}
// ---------------------------------------------------------------------------
// Preflight
// ---------------------------------------------------------------------------
fn fossilAvailable(gpa: std.mem.Allocator, io: std.Io) bool {
const res = std.process.run(gpa, io, .{ .argv = &.{ "fossil", "version" } }) catch return false;
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
return okExit(res.term);
}
fn tempRoot(arena: std.mem.Allocator, env: std.process.Environ.Map) ![]const u8 {
const base = env.get("TMPDIR") orelse "/tmp";
const trimmed = std.mem.trimEnd(u8, base, "/");
return std.fs.path.join(arena, &.{ trimmed, "zit-smoke" });
}
// ---------------------------------------------------------------------------
// fossil process helper
// ---------------------------------------------------------------------------
fn okExit(term: std.process.Child.Term) bool {
return switch (term) {
.exited => |code| code == 0,
else => false,
};
}
fn runFossil(
gpa: std.mem.Allocator,
io: std.Io,
argv: []const []const u8,
cwd_path: []const u8,
env: *const std.process.Environ.Map,
) !std.process.RunResult {
return std.process.run(gpa, io, .{
.argv = argv,
.cwd = .{ .path = cwd_path },
.environ_map = env,
});
}
/// First whitespace-delimited token of `text` (the hash column of
/// `fossil sha1sum`/`sha3sum` output: "<hash> <path>\n").
fn firstToken(text: []const u8) []const u8 {
const end = std.mem.indexOfAny(u8, text, " \t\r\n") orelse text.len;
return text[0..end];
}
// ---------------------------------------------------------------------------
// Failure reporting -- never authored from fossil's side, only printed.
// ---------------------------------------------------------------------------
fn printBytesPreview(out: *std.Io.Writer, label: []const u8, bytes: []const u8) !void {
try out.print(" {s} (len {d}): ", .{ label, bytes.len });
const max = @min(bytes.len, 96);
for (bytes[0..max]) |b| try out.print("{x:0>2}", .{b});
if (bytes.len > max) try out.writeAll("...");
try out.writeAll("\n");
}
fn firstDiffOffset(a: []const u8, b: []const u8) ?usize {
const n = @min(a.len, b.len);
var i: usize = 0;
while (i < n) : (i += 1) {
if (a[i] != b[i]) return i;
}
if (a.len != b.len) return n;
return null;
}
fn printDiffWindow(out: *std.Io.Writer, zit_bytes: []const u8, fossil_bytes: []const u8) !void {
const off = firstDiffOffset(zit_bytes, fossil_bytes) orelse return;
const start = if (off > 16) off - 16 else 0;
const zit_end = @min(zit_bytes.len, off + 16);
const fossil_end = @min(fossil_bytes.len, off + 16);
try out.print(" first diff at byte {d}\n", .{off});
try out.writeAll(" zit : ");
for (zit_bytes[start..zit_end]) |b| try out.print("{x:0>2}", .{b});
try out.writeAll("\n fossil : ");
for (fossil_bytes[start..fossil_end]) |b| try out.print("{x:0>2}", .{b});
try out.writeAll("\n");
}
fn printMismatchFooter(out: *std.Io.Writer, citation: []const u8) !void {
try out.print(" RFC citation: {s}\n", .{citation});
try out.writeAll(
\\ Fossil disagrees with zit's RFC-cited expectation. Operator decides
\\ RFC-misread vs RFC-wrong; record in ambiguities.md. Never change zit
\\ to match fossil.
\\
);
}
// ---------------------------------------------------------------------------
// S1/S2/S3 -- manifest scenarios (parse -> create -> confirm via fossil)
// ---------------------------------------------------------------------------
fn runManifestScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
sc: ManifestScenario,
) !bool {
// zit's own product: the artifact fed to fossil is `create()`'s output,
// never the raw RFC transcription (§6.2 round-trip; the two happen to be
// byte-identical for S1/S3 and a strict-lexical reorder for S2, but the
// input to fossil is always what zit itself wrote).
var m = try artifact.parse(gpa, sc.fixture);
defer m.deinit();
const zit_bytes = try artifact.create(&m, gpa);
defer gpa.free(zit_bytes);
var name_buf: [64]u8 = undefined;
const zit_name = artifact.artifactName(sc.algo, zit_bytes, &name_buf);
const base = try std.fs.path.join(arena, &.{ tmp_root, sc.name });
const repo_dir = try std.fs.path.join(arena, &.{ base, "repo" });
const checkout_dir = try std.fs.path.join(arena, &.{ base, "checkout" });
try std.Io.Dir.cwd().createDirPath(io, repo_dir);
try std.Io.Dir.cwd().createDirPath(io, checkout_dir);
const manifest_path = try std.fs.path.join(arena, &.{ repo_dir, "manifest.txt" });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = manifest_path, .data = zit_bytes });
// fossil test-parse-manifest: repo-free syntactic accept (§4/§5). Exit is
// 0 either way -- the VERDICT TEXT is what confirms zit's parse.
{
const res = try runFossil(gpa, io, &.{ "fossil", "test-parse-manifest", manifest_path }, repo_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (std.mem.indexOf(u8, res.stdout, "reports the input is ok") == null) {
try out.print("FAIL {s} ({s}): fossil test-parse-manifest did not report the input well-formed\n", .{ sc.name, sc.citation });
try printBytesPreview(out, "zit bytes", zit_bytes);
try out.print(" fossil verdict: {s}\n", .{res.stdout});
try printMismatchFooter(out, sc.citation);
return false;
}
}
// fossil {sha1,sha3}sum: confirms zit's own artifactName computation.
{
const sum_cmd = if (sc.algo == .sha1) "sha1sum" else "sha3sum";
const res = try runFossil(gpa, io, &.{ "fossil", sum_cmd, manifest_path }, repo_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
const fossil_name = firstToken(res.stdout);
if (!std.mem.eql(u8, fossil_name, zit_name)) {
try out.print("FAIL {s} ({s}): fossil {s} disagrees with zit's artifactName\n", .{ sc.name, sc.citation, sum_cmd });
try out.print(" zit name: {s}\n", .{zit_name});
try out.print(" fossil name: {s}\n", .{fossil_name});
try printMismatchFooter(out, sc.citation);
return false;
}
}
// init + open + put + retrieve: confirms zit's create() output
// round-trips through fossil's own content store under the given name.
const repo_fossil = try std.fs.path.join(arena, &.{ repo_dir, "r.fossil" });
const init_argv: []const []const u8 = if (sc.algo == .sha1)
&.{ "fossil", "init", "--sha1", repo_fossil }
else
&.{ "fossil", "init", repo_fossil };
{
const res = try runFossil(gpa, io, init_argv, repo_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL {s} ({s}): fossil init failed\n stderr: {s}\n", .{ sc.name, sc.citation, res.stderr });
try printMismatchFooter(out, sc.citation);
return false;
}
}
{
const res = try runFossil(gpa, io, &.{ "fossil", "open", repo_fossil }, checkout_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL {s} ({s}): fossil open failed\n stderr: {s}\n", .{ sc.name, sc.citation, res.stderr });
try printMismatchFooter(out, sc.citation);
return false;
}
}
{
const res = try runFossil(gpa, io, &.{ "fossil", "test-content-put", manifest_path }, checkout_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL {s} ({s}): fossil test-content-put failed\n stderr: {s}\n", .{ sc.name, sc.citation, res.stderr });
try printMismatchFooter(out, sc.citation);
return false;
}
}
const artifact_out_path = try std.fs.path.join(arena, &.{ checkout_dir, "out.bin" });
{
const res = try runFossil(gpa, io, &.{ "fossil", "artifact", zit_name, "out.bin" }, checkout_dir, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL {s} ({s}): fossil artifact could not resolve zit's name {s}\n stderr: {s}\n", .{ sc.name, sc.citation, zit_name, res.stderr });
try printMismatchFooter(out, sc.citation);
return false;
}
}
const fossil_bytes = try std.Io.Dir.cwd().readFileAlloc(io, artifact_out_path, gpa, .limited(1 << 20));
defer gpa.free(fossil_bytes);
if (!std.mem.eql(u8, zit_bytes, fossil_bytes)) {
try out.print("FAIL {s} ({s}): fossil artifact returned bytes different from zit's create() output\n", .{ sc.name, sc.citation });
try printBytesPreview(out, "zit bytes", zit_bytes);
try printBytesPreview(out, "fossil bytes", fossil_bytes);
try printDiffWindow(out, zit_bytes, fossil_bytes);
try printMismatchFooter(out, sc.citation);
return false;
}
try out.print("PASS {s} ({s}): zit's create() output confirmed by fossil (name {s})\n", .{ sc.name, sc.citation, zit_name });
return true;
}
// ---------------------------------------------------------------------------
// S4 -- delta round-trip (never compared against fossil test-delta-create;
// fossil only applies zit's own delta and confirms the reconstructed target)
// ---------------------------------------------------------------------------
fn runDeltaScenario(
arena: std.mem.Allocator,
gpa: std.mem.Allocator,
io: std.Io,
tmp_root: []const u8,
env: *const std.process.Environ.Map,
out: *std.Io.Writer,
) !bool {
const citation = "§5/§Appendix B.1";
const zit_delta = try delta.create(gpa, delta_source, delta_target);
defer gpa.free(zit_delta);
// zit's own apply() must already reconstruct the target -- fossil only
// ever confirms this, never establishes it.
const self_applied = try delta.apply(gpa, delta_source, zit_delta);
defer gpa.free(self_applied);
std.debug.assert(std.mem.eql(u8, self_applied, delta_target));
const base = try std.fs.path.join(arena, &.{ tmp_root, "S4" });
try std.Io.Dir.cwd().createDirPath(io, base);
const src_path = try std.fs.path.join(arena, &.{ base, "src.bin" });
const delta_path = try std.fs.path.join(arena, &.{ base, "zit.delta" });
const out_path = try std.fs.path.join(arena, &.{ base, "out.bin" });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = src_path, .data = delta_source });
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = delta_path, .data = zit_delta });
{
const res = try runFossil(gpa, io, &.{ "fossil", "test-delta-apply", src_path, delta_path, out_path }, base, env);
defer gpa.free(res.stdout);
defer gpa.free(res.stderr);
if (!okExit(res.term)) {
try out.print("FAIL S4 ({s}): fossil test-delta-apply rejected zit's delta.create() output\n stderr: {s}\n", .{ citation, res.stderr });
try printBytesPreview(out, "zit delta", zit_delta);
try printMismatchFooter(out, citation);
return false;
}
}
const fossil_out = try std.Io.Dir.cwd().readFileAlloc(io, out_path, gpa, .limited(1 << 20));
defer gpa.free(fossil_out);
if (!std.mem.eql(u8, fossil_out, delta_target)) {
try out.print("FAIL S4 ({s}): fossil test-delta-apply produced bytes different from zit's target\n", .{citation});
try printBytesPreview(out, "zit target", delta_target);
try printBytesPreview(out, "fossil bytes", fossil_out);
try printDiffWindow(out, delta_target, fossil_out);
try printMismatchFooter(out, citation);
return false;
}
try out.print("PASS S4 ({s}): fossil test-delta-apply reconstructs zit's delta.create() target\n", .{citation});
return true;
}