Artifact 1981fee54d414988faad7607ef8ec67935e2074b1f7dec89e32e66138270f10e:
- File
src/smoke.zig
— part of check-in
[16c197b4dc]
at
2026-08-29 17:46:18
on branch trunk
— listings take Fossil's shape: ls names, leaves lines, day-grouped timeline, annotate line numbers; branch -v (zit#170)
Twelve-character hashes and the weekday in day headers and leaf lines are the departures; -F oneline keeps the one-line timeline for scripts. (user: dmestas, size: 98622) [annotate] [blame] [check-ins using]
//! 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 builtin = @import("builtin"); const zit = @import("zit"); const artifact = zit.artifact; const rfc_vectors = zit.rfc_vectors; const delta = zit.delta; const build_options = @import("build_options"); const net = std.Io.net; /// The compiled `zit` binary, threaded in by build.zig (a `LazyPath`, always /// rebuilt first). `build_options.zit_exe` is relative to the build root, which /// is this process's cwd; S5 resolves it to an absolute path before spawning it /// as a subprocess (`zit init/add/commit/pack/serve`). Confined to S5 — the /// other scenarios drive only the `zit` LIBRARY, never the binary. const zit_exe: []const u8 = build_options.zit_exe; // --------------------------------------------------------------------------- // RFC fixtures -- the same vectors src/artifact.zig's own tests cite. // --------------------------------------------------------------------------- const a1_1_manifest = rfc_vectors.artifact_a1_1_sha1_manifest; const a1_2_technote = rfc_vectors.artifact_a1_2_sha3_technote; const a3_manifest = rfc_vectors.artifact_a3_rcard_manifest; /// §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; // Record the exact fossil the scenarios confirm against, verbatim, at the // head of the smoke log: the version line and the PASS/FAIL results then sit // in one block the release notes cite (readiness item S7). A null return is // the milestone-manual skip — fossil absent, nothing to confirm against. if (try fossilVersion(gpa, io)) |ver| { defer gpa.free(ver); try out.print("fossil version: {s}\n", .{std.mem.trim(u8, ver, " \t\r\n")}); } else { 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; const s5_pass = runCloneFromRepackedScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: { try out.print("FAIL S5 (§7.5/§3): unexpected error running scenario: {s}\n", .{@errorName(err)}); break :blk false; }; all_pass = all_pass and s5_pass; const s6_pass = runOpenFromFossilServerScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: { try out.print("FAIL S6 (§3.7/§4): unexpected error running scenario: {s}\n", .{@errorName(err)}); break :blk false; }; all_pass = all_pass and s6_pass; const s7_pass = runPushToFossilServerScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: { try out.print("FAIL S7 (§3.3/§3.7): unexpected error running scenario: {s}\n", .{@errorName(err)}); break :blk false; }; all_pass = all_pass and s7_pass; const s8_pass = runPullFossilExtensionScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: { try out.print("FAIL S8 (§3.2/§3.7): unexpected error running scenario: {s}\n", .{@errorName(err)}); break :blk false; }; all_pass = all_pass and s8_pass; const s9_pass = runCloneOfShunnedScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: { try out.print("FAIL S9 (§9.5/§8.2): unexpected error running scenario: {s}\n", .{@errorName(err)}); break :blk false; }; all_pass = all_pass and s9_pass; const s10_pass = runPushOfShunnedScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: { try out.print("FAIL S10 (§7.7): unexpected error running scenario: {s}\n", .{@errorName(err)}); break :blk false; }; all_pass = all_pass and s10_pass; const s11_pass = runPushPolicyRefusalScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: { try out.print("FAIL S11 (#88): unexpected error running scenario: {s}\n", .{@errorName(err)}); break :blk false; }; all_pass = all_pass and s11_pass; const s12_pass = runUserConfigPullScenario(arena, gpa, io, tmp_root, &env, out) catch |err| blk: { try out.print("FAIL S12 (zit#125): unexpected error running scenario: {s}\n", .{@errorName(err)}); break :blk false; }; all_pass = all_pass and s12_pass; try out.flush(); if (!all_pass) std.process.exit(1); } /// S12 (zit#125) — the user-table configuration area, against a REAL fossil /// server: `zit config pull user` adopts the peer's logins and capabilities, /// and an ANONYMOUS attempt gets nothing and changes nothing. /// /// This is the one lane where the admin gate can be proved rather than /// assumed. Fossil strips `CONFIGSET_USER` from a non-administrator's group /// mask (`configure.c`, `xfer.c`), so the boundary is fossil's, not zit's — /// and a unit test against zit's own server could not observe it at all, /// because zit's server has no capability model and therefore never serves /// this area. The server is spawned WITHOUT `--localauth`, which would /// auto-authenticate a loopback client as the default administrator and /// quietly turn the anonymous half of this test into a second credentialed /// one. fn runUserConfigPullScenario( 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 = "zit#125"; const abs_exe = try zitAbsPath(arena, io); const base = try std.fs.path.join(arena, &.{ tmp_root, "S12" }); try std.Io.Dir.cwd().createDirPath(io, base); const peer_repo = try std.fs.path.join(arena, &.{ base, "peer.fossil" }); const mirror = try std.fs.path.join(arena, &.{ base, "mirror" }); { const res = try runFossil(gpa, io, &.{ "fossil", "init", peer_repo }, base, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S12 ({s}): fossil init failed\n stderr: {s}\n", .{ citation, res.stderr }); return false; } } // A named administrator whose capabilities this pull must reproduce. { const res = try runFossil(gpa, io, &.{ "fossil", "user", "new", "smokeadmin", "Smoke Admin", "sekret", "-R", peer_repo }, base, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S12 ({s}): fossil user new failed\n stderr: {s}\n", .{ citation, res.stderr }); return false; } } { const res = try runFossil(gpa, io, &.{ "fossil", "user", "capabilities", "smokeadmin", "s", "-R", peer_repo }, base, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S12 ({s}): fossil user capabilities failed\n stderr: {s}\n", .{ citation, res.stderr }); return false; } } const port = try freshLoopbackPort(io); const port_str = try std.fmt.allocPrint(arena, "{d}", .{port}); var server = spawnFossilServe(gpa, io, out, peer_repo, base, env, port_str) catch |err| { try out.print("FAIL S12 ({s}): could not spawn fossil server: {s}\n", .{ citation, @errorName(err) }); return false; }; defer stopFossilServe(io, &server); waitForListen(io, port) catch { try out.print("FAIL S12 ({s}): fossil server never bound 127.0.0.1:{d}\n", .{ citation, port }); return false; }; const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port}); // A mirror of the peer: the project code must match or fossil refuses the // session outright (§9.1), so the clone is part of the fixture. { const res = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "clone", url, mirror }, .cwd = .{ .path = base }, .environ_map = env }); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S12 ({s}): zit clone of the fossil peer failed\n stderr: {s}\n", .{ citation, res.stderr }); return false; } } // ANONYMOUS: fossil serves no user rows, zit says so, and nothing lands. { const res = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "config", "pull", "user" }, .cwd = .{ .path = mirror }, .environ_map = env }); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (okExit(res.term)) { try out.print("FAIL S12 ({s}): an anonymous user pull SUCCEEDED; fossil's admin gate is not being observed\n", .{citation}); try printMismatchFooter(out, citation); return false; } const listed = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "users" }, .cwd = .{ .path = mirror }, .environ_map = env }); defer gpa.free(listed.stdout); defer gpa.free(listed.stderr); if (listed.stdout.len != 0) { try out.print("FAIL S12 ({s}): a refused pull still wrote rows: {s}\n", .{ citation, listed.stdout }); return false; } } // CREDENTIALED: the rows arrive, with fossil's capability string verbatim. { const res = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "config", "pull", "user", "--user=smokeadmin", "--password=sekret" }, .cwd = .{ .path = mirror }, .environ_map = env, }); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S12 ({s}): a credentialed user pull failed\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr }); try printMismatchFooter(out, citation); return false; } } { const listed = try std.process.run(gpa, io, .{ .argv = &.{ abs_exe, "users" }, .cwd = .{ .path = mirror }, .environ_map = env }); defer gpa.free(listed.stdout); defer gpa.free(listed.stderr); if (std.mem.indexOf(u8, listed.stdout, "smokeadmin\ts\t") == null) { try out.print("FAIL S12 ({s}): the pulled table does not carry smokeadmin with fossil's caps\n got: {s}\n", .{ citation, listed.stdout }); try printMismatchFooter(out, citation); return false; } // The hash is withheld from the default listing even though the row // carries one — the same rule the unit lane pins, proved here over a // password fossil itself hashed. if (std.mem.count(u8, listed.stdout, "\t") > 3 * std.mem.count(u8, listed.stdout, "\n")) { try out.print("FAIL S12 ({s}): the default listing has too many columns; is it printing the hash?\n got: {s}\n", .{ citation, listed.stdout }); return false; } } try out.print("PASS S12 ({s}): a credentialed 'config pull user' reproduces a fossil peer's logins and capabilities; an anonymous one is refused and writes nothing\n", .{citation}); return true; } // --------------------------------------------------------------------------- // Preflight // --------------------------------------------------------------------------- /// The runner's `fossil version` output, captured verbatim, or null when fossil /// is absent or errors. Doubles as the availability gate (null ⇒ skip) and the /// version record printed at the head of the smoke log. Caller frees a non-null /// return. fn fossilVersion(gpa: std.mem.Allocator, io: std.Io) !?[]u8 { const res = std.process.run(gpa, io, .{ .argv = &.{ "fossil", "version" } }) catch return null; defer gpa.free(res.stderr); if (!okExit(res.term)) { gpa.free(res.stdout); return null; } return res.stdout; } 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; } // --------------------------------------------------------------------------- // S5 -- clone from a re-packed store (§7.5, covering the §3 // serve-regenerated-delta note). The one scenario that drives the real `zit` // BINARY: build a repo, evolve a file so its blob is stored as a delta, run // `zit pack` so that delta is REGENERATED against our size-minimizing base, then // serve the repacked store and have the real `fossil` client CLONE it. fossil // hash-verifies every artifact it reconstructs on receipt, so a clone that exits // 0 proves the regenerated delta container zit serves reconstructs byte-for-byte // over the wire; the checked-out file is compared to zit's own committed bytes as // the final oracle. Every byte fossil sees is zit's own output — fossil only // confirms, never authors (the file policy at the top of this file). // --------------------------------------------------------------------------- /// Allocates a fresh loopback port for a child server. The reservation listener /// closes before the child binds it; that close→child-bind race is unavoidable /// because child servers cannot inherit the listener. fn freshLoopbackPort(io: std.Io) !u16 { const addr = try net.IpAddress.parseIp4("127.0.0.1", 0); var server = try addr.listen(io, .{}); const port = server.socket.address.getPort(); server.deinit(io); if (port == 0) return error.NoLoopbackPort; return port; } /// Build the S5 file blob: 200 similar lines so `zit pack`'s best-of-N base /// selection produces a profitable delta. `changed` mutates one line and appends /// a tail, so the second commit's blob deltifies well against the first. fn buildBlob(arena: std.mem.Allocator, changed: bool) ![]u8 { var aw = std.Io.Writer.Allocating.init(arena); // arena-owned; no deinit var i: usize = 0; while (i < 200) : (i += 1) { if (changed and i == 50) { try aw.writer.print("line {d}: CHANGED CONTENT for the regenerated-delta smoke\n", .{i}); } else { try aw.writer.print("line {d}: the quick brown fox jumps over the lazy dog number {d}\n", .{ i, i }); } } if (changed) try aw.writer.writeAll("line 200: appended tail exercises the delta insert op\n"); return aw.toOwnedSlice(); } /// Absolute path to the compiled `zit` binary. `zit_exe` is relative to the /// build root (this process's cwd); a spawned child chdirs before exec, so it /// must be resolved first. Allocated on `arena`. fn zitAbsPath(arena: std.mem.Allocator, io: std.Io) ![]const u8 { const cwd_abs = try std.process.currentPathAlloc(io, arena); return std.fs.path.resolve(arena, &.{ cwd_abs, zit_exe }); } /// Blocking `zit <tail...>` with cwd `cwd_path` and the shared smoke env /// (FOSSIL_HOME set). Caller owns `result.stdout`/`result.stderr`. fn runZit( gpa: std.mem.Allocator, io: std.Io, abs_exe: []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, tail: []const []const u8, ) !std.process.RunResult { var argv_buf: [8][]const u8 = undefined; argv_buf[0] = abs_exe; std.debug.assert(tail.len <= argv_buf.len - 1); for (tail, argv_buf[1..][0..tail.len]) |src, *dst| dst.* = src; return std.process.run(gpa, io, .{ .argv = argv_buf[0 .. 1 + tail.len], .cwd = .{ .path = cwd_path }, .environ_map = env, }); } /// Spawn `zit serve --port <port>` detached, cwd `cwd_path` (so `discoverRoot` /// finds `cwd_path/.zit`). stdout/stderr discarded — driven purely over the /// socket. Caller `kill`s the returned child. fn spawnZitServe( io: std.Io, abs_exe: []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, port_str: []const u8, ) !std.process.Child { return std.process.spawn(io, .{ .argv = &.{ abs_exe, "serve", "--port", port_str }, .cwd = .{ .path = cwd_path }, .environ_map = env, .stdout = .ignore, .stderr = .ignore, }); } /// Bounded wait for a server to bind: retry-connect to the loopback port /// until accepted or a wall-clock deadline elapses (never a fixed sleep). A /// successful TCP connect proves the listener is up. fn waitForListen(io: std.Io, port: u16) !void { const addr = net.IpAddress.parseIp4("127.0.0.1", port) catch return error.BadTestAddress; const deadline_ns: i96 = 15_000_000_000; // 15 s cap — orders of magnitude of slack const start = std.Io.Clock.now(.awake, io).nanoseconds; while (true) { if (addr.connect(io, .{ .mode = .stream })) |stream| { stream.close(io); return; } else |_| {} if (std.Io.Clock.now(.awake, io).nanoseconds - start > deadline_ns) return error.ServerNeverBound; } } /// The count parsed from `zit pack`'s "<N> re-deltaed" clause, or null if the /// clause is absent. Used to assert S5's fixture still forces a REGENERATED /// delta (so the scenario keeps testing serve-of-a-regenerated-delta, not just a /// full-form clone). fn reDeltaCount(text: []const u8) ?usize { const idx = std.mem.indexOf(u8, text, " re-deltaed") orelse return null; var start = idx; while (start > 0 and std.ascii.isDigit(text[start - 1])) start -= 1; if (start == idx) return null; return std.fmt.parseInt(usize, text[start..idx], 10) catch null; } /// Run `zit <tail...>` and fail the scenario (printing stderr) unless it exits 0. /// Returns the captured result on success (caller frees), or null on failure /// (already freed + reported). fn zitStep( gpa: std.mem.Allocator, io: std.Io, out: *std.Io.Writer, abs_exe: []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, tail: []const []const u8, label: []const u8, ) !?std.process.RunResult { return zitStepIn(gpa, io, out, abs_exe, cwd_path, env, tail, label, "S5", "§7.5/§3"); } /// `zitStep` for a named scenario: the same run-or-report-and-abort shape, with /// the scenario name and citation supplied rather than fixed to S5's. fn zitStepIn( gpa: std.mem.Allocator, io: std.Io, out: *std.Io.Writer, abs_exe: []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, tail: []const []const u8, label: []const u8, scenario: []const u8, citation: []const u8, ) !?std.process.RunResult { const res = try runZit(gpa, io, abs_exe, cwd_path, env, tail); if (!okExit(res.term)) { try out.print("FAIL {s} ({s}): {s} failed\n stderr: {s}\n", .{ scenario, citation, label, res.stderr }); try printMismatchFooter(out, citation); gpa.free(res.stdout); gpa.free(res.stderr); return null; } return res; } fn runCloneFromRepackedScenario( 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 = "§7.5/§3"; const abs_exe = try zitAbsPath(arena, io); const base = try std.fs.path.join(arena, &.{ tmp_root, "S5" }); const src_dir = try std.fs.path.join(arena, &.{ base, "src" }); try std.Io.Dir.cwd().createDirPath(io, src_dir); // A source repo with two check-ins of an evolving file: the second blob is // stored as a delta, giving `pack` something to re-base. { const r = (try zitStep(gpa, io, out, abs_exe, src_dir, env, &.{"init"}, "zit init")) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } const blob_path = try std.fs.path.join(arena, &.{ src_dir, "big.txt" }); const seed = try buildBlob(arena, false); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = blob_path, .data = seed }); inline for (.{ .{ &[_][]const u8{ "add", "big.txt" }, "zit add (seed)" }, .{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit (seed)" }, }) |step| { const r = (try zitStep(gpa, io, out, abs_exe, src_dir, env, step[0], step[1])) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } const edited = try buildBlob(arena, true); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = blob_path, .data = edited }); inline for (.{ .{ &[_][]const u8{ "add", "big.txt" }, "zit add (grow)" }, .{ &[_][]const u8{ "commit", "-m", "grow", "--user", "u" }, "zit commit (grow)" }, }) |step| { const r = (try zitStep(gpa, io, out, abs_exe, src_dir, env, step[0], step[1])) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // Re-pack: every artifact is re-deltified against our chosen base, so a // served delta container is REGENERATED, not the one the write path stored. { const r = (try zitStep(gpa, io, out, abs_exe, src_dir, env, &.{"pack"}, "zit pack")) orelse return false; defer gpa.free(r.stdout); defer gpa.free(r.stderr); const n = reDeltaCount(r.stdout) orelse { try out.print("FAIL S5 ({s}): could not parse pack's re-delta count\n pack stdout: {s}\n", .{ citation, r.stdout }); return false; }; if (n == 0) { try out.print("FAIL S5 ({s}): pack regenerated no delta — the fixture no longer exercises a served regenerated delta container\n pack stdout: {s}\n", .{ citation, r.stdout }); return false; } } // Serve the repacked store; the real fossil client clones it over the wire. const s5_serve_port = try freshLoopbackPort(io); const port_str = try std.fmt.allocPrint(arena, "{d}", .{s5_serve_port}); var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str); defer server.kill(io); waitForListen(io, s5_serve_port) catch { try out.print("FAIL S5 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, s5_serve_port }); return false; }; // A bare-root URL: a conformant `fossil clone` POSTs to "/" (sync §4). This // is the fossil-client-from-root case e2e explicitly defers to smoke. const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{s5_serve_port}); const clone_repo = try std.fs.path.join(arena, &.{ base, "verify.fossil" }); { const res = try runFossil(gpa, io, &.{ "fossil", "clone", url, clone_repo }, base, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S5 ({s}): fossil clone from the repacked zit store failed\n stderr: {s}\n", .{ citation, res.stderr }); try printMismatchFooter(out, citation); return false; } } // Check out the clone (autosync off — the server is about to be killed) so // fossil reconstructs the file from the artifacts it received. const co_dir = try std.fs.path.join(arena, &.{ base, "co" }); try std.Io.Dir.cwd().createDirPath(io, co_dir); { const res = try runFossil(gpa, io, &.{ "fossil", "open", "--nosync", clone_repo }, co_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S5 ({s}): fossil open of the clone failed\n stderr: {s}\n", .{ citation, res.stderr }); try printMismatchFooter(out, citation); return false; } } // THE PROOF: fossil's reconstructed file == zit's own committed bytes. A // regenerated delta that dropped or corrupted a byte fails here. const co_blob = try std.fs.path.join(arena, &.{ co_dir, "big.txt" }); const fossil_bytes = try std.Io.Dir.cwd().readFileAlloc(io, co_blob, gpa, .limited(1 << 20)); defer gpa.free(fossil_bytes); if (!std.mem.eql(u8, edited, fossil_bytes)) { try out.print("FAIL S5 ({s}): fossil clone from the repacked store did not reconstruct zit's committed bytes\n", .{citation}); try printBytesPreview(out, "zit committed", edited); try printBytesPreview(out, "fossil clone", fossil_bytes); try printDiffWindow(out, edited, fossil_bytes); try printMismatchFooter(out, citation); return false; } try out.print("PASS S5 ({s}): fossil clone from a repacked zit store reconstructs zit's committed bytes byte-for-byte\n", .{citation}); return true; } // --------------------------------------------------------------------------- // S6 -- clone → open against a real fossil SERVER (sync §3.7/§4, P3-F3). The // mirror of S5: there zit served and fossil cloned; here fossil serves and the // real `zit` binary clones AND opens, so the working tree zit materializes is // the thing under test. This is the half of the clone story digest-set equality // cannot see — a repository can converge perfectly on the wire and still be // unusable if nothing turns it into files on disk. // // The file policy at the top of this file holds: the compared bytes are OUR OWN // (we author `s6_payload`, hand it to `fossil add`/`fossil commit`, and compare // it to what zit materializes). fossil is the container and the transport in // between; it never authors a byte of the oracle. A mismatch means zit's // artifact reconstruction or its working-tree materialization dropped or // corrupted content that fossil carried faithfully. // --------------------------------------------------------------------------- /// The bytes S6 round-trips. Authored here, never by fossil: multi-line with a /// trailing newline, so a materialization that mangled line endings or dropped /// the tail is caught. const s6_payload = \\line one of the fossil-served source tree \\line two, so a truncation is visible \\line three \\ ; /// Run `fossil <argv...>` and fail the scenario (printing stderr) unless it /// exits 0. Returns true on success; the caller aborts the scenario on false. fn fossilStep( gpa: std.mem.Allocator, io: std.Io, out: *std.Io.Writer, argv: []const []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, label: []const u8, ) !bool { const res = try runFossil(gpa, io, argv, cwd_path, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S6 (§3.7/§4): {s} failed\n stderr: {s}\n", .{ label, res.stderr }); return false; } return true; } fn runOpenFromFossilServerScenario( 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 = "§3.7/§4"; const abs_exe = try zitAbsPath(arena, io); const base = try std.fs.path.join(arena, &.{ tmp_root, "S6" }); const co_dir = try std.fs.path.join(arena, &.{ base, "co" }); try std.Io.Dir.cwd().createDirPath(io, co_dir); const repo_path = try std.fs.path.join(arena, &.{ base, "source.fossil" }); // A real fossil repository holding one check-in of OUR bytes. if (!try fossilStep(gpa, io, out, &.{ "fossil", "init", repo_path }, base, env, "fossil init")) return false; if (!try fossilStep(gpa, io, out, &.{ "fossil", "open", "--nosync", repo_path }, co_dir, env, "fossil open")) return false; const payload_path = try std.fs.path.join(arena, &.{ co_dir, "big.txt" }); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = payload_path, .data = s6_payload }); if (!try fossilStep(gpa, io, out, &.{ "fossil", "add", "big.txt" }, co_dir, env, "fossil add")) return false; if (!try fossilStep( gpa, io, out, &.{ "fossil", "commit", "-m", "s6 seed", "--user-override", "u", "--no-warnings" }, co_dir, env, "fossil commit", )) return false; // Serve it with the real fossil server and clone it with the real zit // binary over the bare-root URL (sync §4: the client posts the repo URL path // unchanged and MUST NOT append /xfer). const s6_serve_port = try freshLoopbackPort(io); const port_str = try std.fmt.allocPrint(arena, "{d}", .{s6_serve_port}); var server = spawnFossilServe(gpa, io, out, repo_path, base, env, port_str) catch |err| { try out.print("FAIL S6 ({s}): could not spawn fossil server: {s}\n", .{ citation, @errorName(err) }); return false; }; defer stopFossilServe(io, &server); // The same retry-connect wait S5 uses: a successful TCP connect proves the // listener is up, whichever binary owns it. waitForListen(io, s6_serve_port) catch { try out.print("FAIL S6 ({s}): fossil server never bound 127.0.0.1:{d}\n", .{ citation, s6_serve_port }); return false; }; const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{s6_serve_port}); { const r = (try zitStep(gpa, io, out, abs_exe, base, env, &.{ "clone", "--no-open", url, "cloned" }, "zit clone (from fossil server)")) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // OPEN: the clone's checkout is still unborn, so this is what turns the // converged repository into a working tree. const cloned_dir = try std.fs.path.join(arena, &.{ base, "cloned" }); { const r = (try zitStep(gpa, io, out, abs_exe, cloned_dir, env, &.{"open"}, "zit open")) orelse return false; defer gpa.free(r.stdout); defer gpa.free(r.stderr); if (std.mem.indexOf(u8, r.stdout, "trunk") == null) { try out.print("FAIL S6 ({s}): zit open did not report opening trunk\n stdout: {s}\n", .{ citation, r.stdout }); return false; } } // THE PROOF: the file zit materialized == the bytes we committed through // fossil. A dropped artifact, a mis-reconstructed delta, or a materialization // that never wrote the file fails here. const opened_blob = try std.fs.path.join(arena, &.{ cloned_dir, "big.txt" }); const zit_bytes = std.Io.Dir.cwd().readFileAlloc(io, opened_blob, gpa, .limited(1 << 20)) catch |err| { try out.print("FAIL S6 ({s}): zit open left no readable big.txt in the cloned tree: {s}\n", .{ citation, @errorName(err) }); try printMismatchFooter(out, citation); return false; }; defer gpa.free(zit_bytes); if (!std.mem.eql(u8, s6_payload, zit_bytes)) { try out.print("FAIL S6 ({s}): the tree zit opened does not match the bytes committed through fossil\n", .{citation}); try printBytesPreview(out, "committed", s6_payload); try printBytesPreview(out, "zit opened", zit_bytes); try printDiffWindow(out, s6_payload, zit_bytes); try printMismatchFooter(out, citation); return false; } try out.print("PASS S6 ({s}): zit clone+open against a real fossil server materializes the committed bytes byte-for-byte\n", .{citation}); return true; } // --------------------------------------------------------------------------- // S7/S8 -- the PUSH-direction interop pair (sync §3.3/§3.7 push, §3.2/§3.7 // pull). S5/S6 exercise only pull/clone wire (fossil pulls from zit, zit pulls // from fossil); the push-side wire -- zit as the sync CLIENT uploading to a // server, its igot/gimme ordering and capability negotiation -- was invisible. // Both scenarios share the same prelude: zit authors a seed check-in, a real // fossil repo CLONES it (so client and server share a project-code, without // which the sync layer refuses the exchange), zit grows a second check-in the // fossil repo lacks, then zit PUSHES that growth up to a live `fossil server`. // // The file policy at the top holds throughout: every compared byte is OUR OWN // -- we author the pushed file blob and (in S8) the file blob a fossil commit // carries. fossil is the container and the transport; it never authors a byte // of an oracle. A mismatch means zit's push wire, or its pull-and-reconstruct, // dropped or corrupted content fossil relayed faithfully. // --------------------------------------------------------------------------- /// The seed the fossil repo clones and the zit repo shares -- our own bytes. const interop_seed_payload = \\seed line shared by the fossil clone and the zit repo \\ ; /// The file zit authors, commits, and PUSHES to the fossil server. Multi-line /// with a trailing newline so a truncated or line-ending-mangled push is caught. const push_grow_payload = \\zit authored this line and pushed it to a fossil server \\second line so a dropped tail is visible \\ ; /// S8: authored here, committed THROUGH a fossil checkout (fossil is the /// container), that zit must PULL and reconstruct. The library reader confirms /// convergence by reading it back under its own artifact name. const fossil_ext_payload = \\authored here, carried by a fossil commit, pulled by zit \\second line so a truncation is visible \\ ; /// The prelude state both push scenarios build on. const InteropSetup = struct { base: []const u8, zsrc: []const u8, dst_fossil: []const u8, abs_exe: []const u8, }; /// Disable Fossil's detached backoffice worker, then spawn /// `fossil server --port <port> <repo>` in a dedicated process group. The /// repositories served here are disposable smoke fixtures; disabling their /// maintenance worker keeps every process inside the test-owned group. fn spawnFossilServe( gpa: std.mem.Allocator, io: std.Io, out: *std.Io.Writer, repo_path: []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, port_str: []const u8, ) !std.process.Child { const disable = try runFossil( gpa, io, &.{ "fossil", "settings", "backoffice-disable", "1", "-R", repo_path }, cwd_path, env, ); defer gpa.free(disable.stdout); defer gpa.free(disable.stderr); if (!okExit(disable.term)) { try out.print( "fossil settings backoffice-disable failed ({any})\n stderr: {s}\n", .{ disable.term, disable.stderr }, ); return error.CannotDisableFossilBackoffice; } return std.process.spawn(io, .{ .argv = &.{ "fossil", "server", "--port", port_str, repo_path }, .cwd = .{ .path = cwd_path }, .environ_map = env, .stdout = .ignore, .stderr = .ignore, .pgid = if (builtin.os.tag == .windows) null else 0, }); } /// Stop a Fossil server and all request handlers forked from it. `Child.kill` /// addresses only the original PID, which can leave a POSIX handler holding the /// listening socket after the parent is reaped. fn stopFossilServe(io: std.Io, server: *std.process.Child) void { if (comptime builtin.os.tag != .windows) { if (server.id) |pid| std.posix.kill(-pid, .KILL) catch {}; } server.kill(io); } /// `zit <tail...>` step, tagged with the scenario name/citation for failure /// reporting (the S5-scoped `zitStep` bakes in "S5"). Returns the captured /// result on success (caller frees), or null on a non-zero exit (already freed /// and reported). fn zitStepTagged( gpa: std.mem.Allocator, io: std.Io, out: *std.Io.Writer, abs_exe: []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, tail: []const []const u8, tag: []const u8, citation: []const u8, label: []const u8, ) !?std.process.RunResult { const res = try runZit(gpa, io, abs_exe, cwd_path, env, tail); if (!okExit(res.term)) { try out.print("FAIL {s} ({s}): {s} failed\n stderr: {s}\n", .{ tag, citation, label, res.stderr }); try printMismatchFooter(out, citation); gpa.free(res.stdout); gpa.free(res.stderr); return null; } return res; } /// `fossil <argv...>` step, tagged with the scenario name/citation (the /// S6-scoped `fossilStep` bakes in "S6"). True on exit 0; the caller aborts the /// scenario on false. fn fossilStepTagged( gpa: std.mem.Allocator, io: std.Io, out: *std.Io.Writer, argv: []const []const u8, cwd_path: []const u8, env: *const std.process.Environ.Map, tag: []const u8, citation: []const u8, label: []const u8, ) !bool { const res = try runFossil(gpa, io, argv, cwd_path, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL {s} ({s}): {s} failed\n stderr: {s}\n", .{ tag, citation, label, res.stderr }); return false; } return true; } /// The shared prelude: a zit repo and a fossil repo that share a project-code, /// with a zit-authored `grow` check-in PUSHED from zit up to the fossil server. /// Returns the paths on success, or null on any failure (already reported under /// `tag`). fn establishPushedFossil( 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, tag: []const u8, citation: []const u8, sub: []const u8, ) !?InteropSetup { const abs_exe = try zitAbsPath(arena, io); const base = try std.fs.path.join(arena, &.{ tmp_root, sub }); const zsrc = try std.fs.path.join(arena, &.{ base, "zsrc" }); const dst_fossil = try std.fs.path.join(arena, &.{ base, "dst.fossil" }); try std.Io.Dir.cwd().createDirPath(io, zsrc); // zit authors a seed check-in. { const r = (try zitStepTagged(gpa, io, out, abs_exe, zsrc, env, &.{"init"}, tag, citation, "zit init")) orelse return null; gpa.free(r.stdout); gpa.free(r.stderr); } const seed_path = try std.fs.path.join(arena, &.{ zsrc, "a.txt" }); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = seed_path, .data = interop_seed_payload }); inline for (.{ .{ &[_][]const u8{ "add", "a.txt" }, "zit add (seed)" }, .{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit (seed)" }, }) |step| { const r = (try zitStepTagged(gpa, io, out, abs_exe, zsrc, env, step[0], tag, citation, step[1])) orelse return null; gpa.free(r.stdout); gpa.free(r.stderr); } // Serve the seed; the real fossil binary CLONES it, establishing the shared // project-code the sync layer requires before it will accept a push. const seed_port = try freshLoopbackPort(io); const seed_port_str = try std.fmt.allocPrint(arena, "{d}", .{seed_port}); { var server = try spawnZitServe(io, abs_exe, zsrc, env, seed_port_str); waitForListen(io, seed_port) catch { server.kill(io); try out.print("FAIL {s} ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ tag, citation, seed_port }); return null; }; const seed_url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{seed_port}); const ok_clone = try fossilStepTagged(gpa, io, out, &.{ "fossil", "clone", seed_url, dst_fossil }, base, env, tag, citation, "fossil clone of the zit seed"); server.kill(io); if (!ok_clone) return null; } // The clone saved a remote (the now-dead seed server) as the fossil repo's // autosync target; disable autosync so a fossil-side commit (S8) cannot // silently abort trying to reach it. Grant the anonymous loopback sync user // check-in ('i') so the push is authorized (a bare sync request logs in as // "nobody", whose cloned caps are read-only). if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "settings", "autosync", "off", "-R", dst_fossil }, base, env, tag, citation, "fossil settings autosync off")) return null; if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "user", "capabilities", "nobody", "gjorzwik", "-R", dst_fossil }, base, env, tag, citation, "fossil user capabilities nobody")) return null; // zit grows a check-in the fossil repo lacks. const grow_path = try std.fs.path.join(arena, &.{ zsrc, "b.txt" }); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = grow_path, .data = push_grow_payload }); inline for (.{ .{ &[_][]const u8{ "add", "b.txt" }, "zit add (grow)" }, .{ &[_][]const u8{ "commit", "-m", "grow", "--user", "u" }, "zit commit (grow)" }, }) |step| { const r = (try zitStepTagged(gpa, io, out, abs_exe, zsrc, env, step[0], tag, citation, step[1])) orelse return null; gpa.free(r.stdout); gpa.free(r.stderr); } // fossil serves the shared repo; zit PUSHES its growth up over the wire. const push_port = try freshLoopbackPort(io); const push_port_str = try std.fmt.allocPrint(arena, "{d}", .{push_port}); { var server = spawnFossilServe(gpa, io, out, dst_fossil, base, env, push_port_str) catch |err| { try out.print("FAIL {s} ({s}): could not spawn fossil server: {s}\n", .{ tag, citation, @errorName(err) }); return null; }; waitForListen(io, push_port) catch { stopFossilServe(io, &server); try out.print("FAIL {s} ({s}): fossil server never bound 127.0.0.1:{d}\n", .{ tag, citation, push_port }); return null; }; const push_url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{push_port}); const push_res = try zitStepTagged(gpa, io, out, abs_exe, zsrc, env, &.{ "push", push_url }, tag, citation, "zit push to the fossil server"); stopFossilServe(io, &server); const r = push_res orelse return null; gpa.free(r.stdout); gpa.free(r.stderr); } return .{ .base = base, .zsrc = zsrc, .dst_fossil = dst_fossil, .abs_exe = abs_exe }; } /// S7 -- zit push -> fossil server. After the shared prelude has pushed zit's /// grown check-in up to the fossil repo, a fresh fossil checkout of that repo /// must reconstruct the pushed file byte-for-byte: fossil hash-verifies every /// artifact it stores and reconstructs, so a checkout that yields our exact /// bytes proves the push delivered the artifacts intact over the wire. fn runPushToFossilServerScenario( 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 = "§3.3/§3.7"; const setup = (try establishPushedFossil(arena, gpa, io, tmp_root, env, out, "S7", citation, "S7")) orelse return false; // THE PROOF: open the pushed-to repo and read back zit's grown file. fossil // materializes it from the artifacts the push delivered; a push that dropped // or corrupted a byte fails here. const co = try std.fs.path.join(arena, &.{ setup.base, "verify-co" }); try std.Io.Dir.cwd().createDirPath(io, co); if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "open", "--nosync", setup.dst_fossil }, co, env, "S7", citation, "fossil open of the pushed repo")) return false; const co_blob = try std.fs.path.join(arena, &.{ co, "b.txt" }); const fossil_bytes = std.Io.Dir.cwd().readFileAlloc(io, co_blob, gpa, .limited(1 << 20)) catch |err| { try out.print("FAIL S7 ({s}): the pushed repo has no readable b.txt after checkout: {s}\n", .{ citation, @errorName(err) }); try printMismatchFooter(out, citation); return false; }; defer gpa.free(fossil_bytes); if (!std.mem.eql(u8, push_grow_payload, fossil_bytes)) { try out.print("FAIL S7 ({s}): the file fossil reconstructed after zit's push does not match zit's committed bytes\n", .{citation}); try printBytesPreview(out, "zit pushed", push_grow_payload); try printBytesPreview(out, "fossil rebuilt", fossil_bytes); try printDiffWindow(out, push_grow_payload, fossil_bytes); try printMismatchFooter(out, citation); return false; } try out.print("PASS S7 ({s}): fossil reconstructs zit's pushed check-in byte-for-byte from the upload\n", .{citation}); return true; } /// S8 -- fossil commit -> zit pull. On the repo zit pushed to in the prelude, a /// fossil checkout commits one more file (our bytes, carried inside a /// fossil-authored check-in). zit then PULLS that extension. Convergence is /// asserted through the library reader: `zit.Repository` opens the pulled store /// and returns the carried blob under its own artifact name, byte-identical to /// what we authored -- so the pull ingested and reconstructed a check-in it did /// not itself write. fn runPullFossilExtensionScenario( 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 = "§3.2/§3.7"; const setup = (try establishPushedFossil(arena, gpa, io, tmp_root, env, out, "S8", citation, "S8")) orelse return false; // A fossil checkout extends the shared repo with one more commit wrapping // OUR bytes. autosync is already off on the repo, so the commit cannot stall // reaching the dead seed remote; --nosync is belt-and-suspenders. const fco = try std.fs.path.join(arena, &.{ setup.base, "fossil-co" }); try std.Io.Dir.cwd().createDirPath(io, fco); if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "open", "--nosync", setup.dst_fossil }, fco, env, "S8", citation, "fossil open of the shared repo")) return false; if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "update", "trunk" }, fco, env, "S8", citation, "fossil update trunk")) return false; const ext_path = try std.fs.path.join(arena, &.{ fco, "c.txt" }); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = ext_path, .data = fossil_ext_payload }); if (!try fossilStepTagged(gpa, io, out, &.{ "fossil", "add", "c.txt" }, fco, env, "S8", citation, "fossil add")) return false; if (!try fossilStepTagged( gpa, io, out, &.{ "fossil", "commit", "-m", "fossil extends", "--user-override", "u", "--no-warnings", "--nosync" }, fco, env, "S8", citation, "fossil commit", )) return false; // fossil serves the extended repo; zit PULLS the check-in it lacks. const pull_port = try freshLoopbackPort(io); const pull_port_str = try std.fmt.allocPrint(arena, "{d}", .{pull_port}); { var server = spawnFossilServe(gpa, io, out, setup.dst_fossil, setup.base, env, pull_port_str) catch |err| { try out.print("FAIL S8 ({s}): could not spawn fossil server: {s}\n", .{ citation, @errorName(err) }); return false; }; waitForListen(io, pull_port) catch { stopFossilServe(io, &server); try out.print("FAIL S8 ({s}): fossil server never bound 127.0.0.1:{d}\n", .{ citation, pull_port }); return false; }; const pull_url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{pull_port}); const pull_res = try zitStepTagged(gpa, io, out, setup.abs_exe, setup.zsrc, env, &.{ "pull", pull_url }, "S8", citation, "zit pull from the fossil server"); stopFossilServe(io, &server); const r = pull_res orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // THE PROOF, through the library reader: the pulled store holds the // fossil-carried blob under its own artifact name (sha3-256 over our bytes), // and `Repository.blob` re-verifies content against that name (§7.2) before // returning it. A pull that fetched nothing, or reconstructed the blob // wrong, fails here. var root = std.Io.Dir.cwd().openDir(io, setup.zsrc, .{}) catch |err| { try out.print("FAIL S8 ({s}): could not open the pulled zit checkout root: {s}\n", .{ citation, @errorName(err) }); return false; }; defer root.close(io); var repo: zit.Repository = undefined; repo.open(gpa, io, root, .{}) catch |err| { try out.print("FAIL S8 ({s}): opening the pulled zit repo via the library failed: {s}\n", .{ citation, @errorName(err) }); try printMismatchFooter(out, citation); return false; }; defer repo.deinit(gpa); var name_buf: [64]u8 = undefined; const ext_name = artifact.artifactName(.sha3_256, fossil_ext_payload, &name_buf); const got = (repo.blob(gpa, ext_name) catch |err| { try out.print("FAIL S8 ({s}): reading the pulled blob {s} via the library failed: {s}\n", .{ citation, ext_name, @errorName(err) }); try printMismatchFooter(out, citation); return false; }) orelse { try out.print("FAIL S8 ({s}): the pull did not converge — blob {s} absent from zit's store after pull\n", .{ citation, ext_name }); try printMismatchFooter(out, citation); return false; }; defer gpa.free(got); if (!std.mem.eql(u8, fossil_ext_payload, got)) { try out.print("FAIL S8 ({s}): the pulled blob does not match the bytes committed through fossil\n", .{citation}); try printBytesPreview(out, "committed", fossil_ext_payload); try printBytesPreview(out, "zit pulled", got); try printDiffWindow(out, fossil_ext_payload, got); try printMismatchFooter(out, citation); return false; } try out.print("PASS S8 ({s}): zit pulls a fossil-authored check-in and the library reader confirms convergence\n", .{citation}); return true; } // --------------------------------------------------------------------------- // S9 -- a real fossil CLONE from `zit serve` of a DEFER-SHUNNED store: the // shunned artifact must be ABSENT, not CORRUPT (sync §9.5 L1988-1992, §8.2 // L1504-1506). // // §9.5 L1989-1991 — "A subsequent read-authorized transfer path requests igot // for every eligible holding after the Section 3.3 shun, phantom, private, and // peer-has filters" — is a statement about what the server sends. This scenario // is the only place a REAL peer judges the result. // // `zit shun --force --defer` records the row WITHOUT rewriting the generation, so // two things are simultaneously true in the served store: the shunned bytes are // still on disk, and a SURVIVING frame in the same generation is a delta rooted on // the shunned one. Both halves of the send-side filter are therefore under test: // // * the shunned artifact must not be announced or sent — fossil must end up // holding it as a PHANTOM, which is fossil's own word for "absent"; // * the surviving delta must arrive in FULL form. Handing out its delta wire // form would ship a DELTASRC this server never sends; fossil would store a // permanently dangling delta and gimme a basis it is silently skipped on // (§3.3 L532-535) — CORRUPT, the outcome this scenario rules out. // // The oracle is `fossil test-integrity`, read for two things fossil says in its // own words: the shunned name is skipped as a PHANTOM, and the blobs it did check // report ZERO errors. "Absent" and "corrupt" are different outcomes and only one // is acceptable. Every byte fossil sees is zit's own output; fossil only confirms. // --------------------------------------------------------------------------- /// The 64-hex artifact name `zit ls` prints for `want` (its lines are /// `HASH SP PATH`), or null when the path is not listed. /// From a `zit ls --long` listing (`<hash> <size> <perm> <class> <name>`; /// the bare listing is names only, zit#170), the hash of `want`. fn lsHashOf(listing: []const u8, want: []const u8) ?[]const u8 { var it = std.mem.splitScalar(u8, listing, '\n'); while (it.next()) |line| { const sp = std.mem.indexOfScalar(u8, line, ' ') orelse continue; const last = std.mem.lastIndexOfScalar(u8, line, ' ') orelse continue; if (!std.mem.eql(u8, std.mem.trim(u8, line[last + 1 ..], " \t\r"), want)) continue; return line[0..sp]; } return null; } /// The count parsed from `zit stat`'s `deltas N` line, or null if absent. Guards /// S9's premise: with no delta frame in the store, the surviving-delta half of the /// scenario is not being exercised at all. fn statDeltaCount(text: []const u8) ?usize { var it = std.mem.splitScalar(u8, text, '\n'); while (it.next()) |line| { if (!std.mem.startsWith(u8, line, "deltas ")) continue; return std.fmt.parseInt(usize, std.mem.trim(u8, line["deltas ".len..], " \t\r"), 10) catch null; } return null; } /// Does `text` carry a `skip phantom` line naming `hash`? That is fossil's own /// report that it holds the name but no content — ABSENT. fn reportsPhantom(text: []const u8, hash: []const u8) bool { var it = std.mem.splitScalar(u8, text, '\n'); while (it.next()) |line| { if (std.mem.indexOf(u8, line, "skip phantom") == null) continue; if (std.mem.indexOf(u8, line, hash) != null) return true; } return false; } /// Does the clone hold `hash` in fossil's OWN shun table? The stronger form of /// absence, and the one zit now produces (#73/#83). /// /// Before the `/shun` configuration group was carried, a reference to a shunned /// artifact left fossil holding a PHANTOM -- a name with no content, which /// fossil treats as something to FETCH. That is the takedown-defeating state: /// the clone would gimme those bytes from the next peer that had them. Now the /// shun row travels with the clone, so fossil records the artifact as /// SUPPRESSED and never asks anyone for it. /// /// Both are "absent"; only one stays absent. Asked of the database rather than /// of `test-integrity`'s prose because a row that exists is a fact, while the /// absence of a phantom line is only the absence of evidence. fn holdsShunRow(gpa: std.mem.Allocator, io: std.Io, repo_dir: []const u8, hash: []const u8, env: anytype) !bool { const sql = try std.fmt.allocPrint(gpa, "select count(*) from shun where uuid='{s}';", .{hash}); defer gpa.free(sql); const res = try runFossil(gpa, io, &.{ "fossil", "sqlite3", "--readonly", sql }, repo_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) return false; return std.mem.indexOfScalar(u8, std.mem.trim(u8, res.stdout, " \t\r\n"), '0') == null; } /// Does `text` report ZERO integrity errors? Anchored on the integer immediately /// before the word `errors`, because the substring "0 errors" is also inside /// "10 errors", "20 errors" and "100 errors" — the exact outputs that mean CORRUPT, /// which is the outcome this scenario exists to tell apart from ABSENT. True only /// when at least one `N errors` count was found and every one of them read zero. fn reportsZeroErrors(text: []const u8) bool { const word = "errors"; var found = false; var i: usize = 0; while (std.mem.indexOfPos(u8, text, i, word)) |p| { i = p + word.len; // Whole word only: `errorsomething` is not a count. if (i < text.len and (std.ascii.isAlphanumeric(text[i]) or text[i] == '_')) continue; var end = p; while (end > 0 and (text[end - 1] == ' ' or text[end - 1] == '\t')) end -= 1; var start = end; while (start > 0 and std.ascii.isDigit(text[start - 1])) start -= 1; if (start == end) continue; // no count in front of this occurrence // The digits must be their own token, not the tail of an identifier. if (start > 0 and (std.ascii.isAlphanumeric(text[start - 1]) or text[start - 1] == '_')) continue; const n = std.fmt.parseInt(u64, text[start..end], 10) catch continue; if (n != 0) return false; found = true; } return found; } test "smoke S9 oracle: a nonzero integrity count is never read as clean" { try std.testing.expect(reportsZeroErrors("low-level database integrity ok\n0 errors\n")); try std.testing.expect(reportsZeroErrors("skip phantom abc\n0 errors, 3 phantoms\n")); // The defect this helper exists to prevent: "0 errors" is a substring of these. try std.testing.expect(!reportsZeroErrors("10 errors\n")); try std.testing.expect(!reportsZeroErrors("100 errors detected\n")); try std.testing.expect(!reportsZeroErrors("0 errors\n20 errors\n")); // No count at all is not a pass either. try std.testing.expect(!reportsZeroErrors("errors\n")); try std.testing.expect(!reportsZeroErrors("integrity check complete\n")); } fn runCloneOfShunnedScenario( 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 = "§9.5/§8.2"; const abs_exe = try zitAbsPath(arena, io); const base = try std.fs.path.join(arena, &.{ tmp_root, "S9" }); const src_dir = try std.fs.path.join(arena, &.{ base, "src" }); try std.Io.Dir.cwd().createDirPath(io, src_dir); { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{"init"}, "zit init", "S9", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // Two files: one to be shunned, one to prove the clone still converged on // something. The shunned file's FIRST version is the delta basis of its // second, which is what puts the surviving-delta half under test. const blob_path = try std.fs.path.join(arena, &.{ src_dir, "big.txt" }); const keep_path = try std.fs.path.join(arena, &.{ src_dir, "keep.txt" }); const keep_bytes = "a file with nothing against it\n"; const seed = try buildBlob(arena, false); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = blob_path, .data = seed }); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = keep_path, .data = keep_bytes }); inline for (.{ .{ &[_][]const u8{ "add", "big.txt", "keep.txt" }, "zit add (seed)" }, .{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit (seed)" }, }) |st| { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, st[0], st[1], "S9", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // The seed blob's complete artifact name, read off the tip while the seed IS // the tip — no abbreviation to resolve and no timeline parsing. const doomed = blk: { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{ "ls", "--long" }, "zit ls --long (seed)", "S9", citation)) orelse return false; defer gpa.free(r.stdout); defer gpa.free(r.stderr); const h = lsHashOf(r.stdout, "big.txt") orelse { try out.print("FAIL S9 ({s}): 'zit ls' did not list big.txt\n stdout: {s}\n", .{ citation, r.stdout }); return false; }; break :blk try arena.dupe(u8, h); }; const edited = try buildBlob(arena, true); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = blob_path, .data = edited }); inline for (.{ .{ &[_][]const u8{ "add", "big.txt" }, "zit add (grow)" }, .{ &[_][]const u8{ "commit", "-m", "grow", "--user", "u" }, "zit commit (grow)" }, }) |st| { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, st[0], st[1], "S9", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // Record the shun WITHOUT reclaiming: the bytes stay, so only the send-side // filter can keep them off the wire. { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{ "shun", "--force", "--defer", doomed }, "zit shun --force --defer", "S9", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // The fixture's premise, stated rather than assumed: the store still carries // a delta frame. With none, the surviving-delta half proves nothing. { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{"stat"}, "zit stat", "S9", citation)) orelse return false; defer gpa.free(r.stdout); defer gpa.free(r.stderr); const deltas = statDeltaCount(r.stdout) orelse { try out.print("FAIL S9 ({s}): could not parse 'zit stat' delta count\n stdout: {s}\n", .{ citation, r.stdout }); return false; }; if (deltas == 0) { try out.print("FAIL S9 ({s}): the store holds no delta frame — the surviving-delta half of this scenario is not being exercised\n stdout: {s}\n", .{ citation, r.stdout }); return false; } } const port = try freshLoopbackPort(io); const port_str = try std.fmt.allocPrint(arena, "{d}", .{port}); var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str); defer server.kill(io); waitForListen(io, port) catch { try out.print("FAIL S9 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, port }); return false; }; const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port}); const clone_repo = try std.fs.path.join(arena, &.{ base, "verify.fossil" }); { const res = try runFossil(gpa, io, &.{ "fossil", "clone", url, clone_repo }, base, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); // A CORRUPT outcome shows up here first: a dangling DELTASRC aborts the // clone rather than completing it minus one artifact. if (!okExit(res.term)) { try out.print("FAIL S9 ({s}): fossil clone of a defer-shunned zit store failed — the shunned artifact must be ABSENT, not a broken clone\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr }); try printMismatchFooter(out, citation); return false; } } const co_dir = try std.fs.path.join(arena, &.{ base, "co" }); try std.Io.Dir.cwd().createDirPath(io, co_dir); { const res = try runFossil(gpa, io, &.{ "fossil", "open", "--nosync", clone_repo }, co_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S9 ({s}): fossil open of the clone failed\n stderr: {s}\n", .{ citation, res.stderr }); try printMismatchFooter(out, citation); return false; } } // The surviving delta arrived in a form fossil could reconstruct: the tip's // file is zit's own committed bytes, byte-for-byte. { const co_blob = try std.fs.path.join(arena, &.{ co_dir, "big.txt" }); const fossil_bytes = try std.Io.Dir.cwd().readFileAlloc(io, co_blob, gpa, .limited(1 << 20)); defer gpa.free(fossil_bytes); if (!std.mem.eql(u8, edited, fossil_bytes)) { try out.print("FAIL S9 ({s}): the clone did not reconstruct zit's committed tip bytes — a delta whose basis was shunned did not arrive in full form\n", .{citation}); try printBytesPreview(out, "zit committed", edited); try printBytesPreview(out, "fossil clone", fossil_bytes); try printDiffWindow(out, edited, fossil_bytes); try printMismatchFooter(out, citation); return false; } } // THE PROOF, in fossil's own words: the shunned artifact is ABSENT, and every // blob fossil DID receive checks clean. // // Absence now has a stronger shape than it used to. Before zit carried the // `/shun` configuration group (#73/#83), fossil ended up holding the name as // a PHANTOM — absent, but absent in the way that makes fossil go looking for // it, so the very next sync with any third party would undo the takedown. // Now the shun row arrives with the clone and fossil records the artifact as // suppressed instead, which is why the phantom is gone. The shun row is // REQUIRED rather than merely accepted: the phantom is the state the // carriage exists to eliminate, so tolerating it would leave this scenario // unable to notice the carriage silently regressing — which is the one // regression it is now best placed to catch. { const res = try runFossil(gpa, io, &.{ "fossil", "test-integrity" }, co_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S9 ({s}): fossil test-integrity failed on the clone\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr }); try printMismatchFooter(out, citation); return false; } if (!(holdsShunRow(gpa, io, co_dir, doomed, env) catch false)) { // The phantom probe survives as a DIAGNOSTIC, not as an accepted // outcome: it separates "the carriage regressed" from "the artifact // was served", which are different bugs with different fixes. const phantom_there = reportsPhantom(res.stdout, doomed) or reportsPhantom(res.stderr, doomed); if (phantom_there) { try out.print("FAIL S9 ({s}): the clone holds the artifact as a PHANTOM rather than a shun row — the /shun configuration group is no longer being carried, so the next sync with any peer would undo the takedown\n", .{citation}); } else { try out.print("FAIL S9 ({s}): the clone neither shuns nor phantoms the artifact — it received content zit must never have served\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr }); } try printMismatchFooter(out, citation); return false; } const clean = reportsZeroErrors(res.stdout) or reportsZeroErrors(res.stderr); if (!clean) { try out.print("FAIL S9 ({s}): fossil test-integrity did not report zero errors — the clone is CORRUPT, not merely missing the shunned artifact\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr }); try printMismatchFooter(out, citation); return false; } } try out.print("PASS S9 ({s}): a fossil clone of a defer-shunned zit store completes, reconstructs the tip byte-for-byte, and records the shunned artifact in its OWN shun table with zero integrity errors\n", .{citation}); return true; } // --------------------------------------------------------------------------- // S10 -- a real fossil PUSH into `zit serve` against a shunned store (sync // §7.7 L1437-1440, §3.3 L527-530). // // S9 is the SEND direction of a shun against a real peer (fossil clones a // defer-shunned zit store and the artifact is absent). This is the direction // the harness had never driven at all: fossil as the sync CLIENT pushing INTO // `zit serve`. S7 pushes the other way (zit client -> fossil server), so this // closes the last untested corner of the four (client, server) x (zit, fossil) // combinations, and it is the one an operator's shun depends on. // // WHAT THIS PINS, EXACTLY. A real fossil `push --verily` against a repository // that has shunned and reclaimed one of its artifacts completes with no // protocol error and lands nothing. That is the interop claim, and it holds. // // WHAT THIS DOES NOT PIN, STATED PLAINLY. It is NOT coverage of §7.7's // receive-side discard: the scenario passes unchanged with the server's // admission check deleted outright (checked with a real mutant, not assumed). // The reason is structural rather than a harness limitation. Content transfer // in this protocol is RECEIVER-DRIVEN: §3.3 L527-530 already excludes a shunned // name from the receiver's phantom scan, so `zit serve` never emits a gimme for // it, and fossil -- whose push offers `igot` and then uploads what the peer asks // for, plus its own `unsent` artifacts -- has no reason to put the card on the // wire. Reaching the §7.7 arm needs a peer that pushes a shunned target // UNSOLICITED, which a conformant peer against this server never does. // // So §7.7 is defense in depth here, and its mutation-sensitive coverage lives // where an unsolicited card can actually be constructed: the `src/sync_test.zig` // §7.7 unit tests and the `src/e2e.zig` raw-POST scenario, both of which go red // under the same mutant this one survives. This scenario is kept for the interop // property in its PASS line and for the direction it opens, not as that proof -- // do not read its green as evidence the admission check is exercised. // // The store's `.zit` SIZE across the push is the oracle for "makes no repository // mutation": `serveRound` persists only when the round wrote something // (`overlay.writtenCount() > 0`), so a round that lands nothing appends nothing. // Size is the one measure the shun mask cannot fool -- `zit cat` refuses the // name whether or not the bytes came back, so it is no oracle here. // --------------------------------------------------------------------------- /// Byte length of the file at `path` (cwd-relative or absolute). fn fileSizeAt(io: std.Io, path: []const u8) !u64 { var f = try std.Io.Dir.cwd().openFile(io, path, .{}); defer f.close(io); const st = try f.stat(io); return st.size; } fn runPushOfShunnedScenario( 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 = "§7.7"; const abs_exe = try zitAbsPath(arena, io); const base = try std.fs.path.join(arena, &.{ tmp_root, "S10" }); const src_dir = try std.fs.path.join(arena, &.{ base, "src" }); try std.Io.Dir.cwd().createDirPath(io, src_dir); { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{"init"}, "zit init", "S10", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } // Two files: one the operator will remove, one that must be unaffected. const doomed_bytes = "content an operator ordered removed, which fossil will offer back\n"; const keep_bytes = "a file with nothing against it\n"; try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = try std.fs.path.join(arena, &.{ src_dir, "doomed.txt" }), .data = doomed_bytes, }); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = try std.fs.path.join(arena, &.{ src_dir, "keep.txt" }), .data = keep_bytes, }); inline for (.{ .{ &[_][]const u8{ "add", "doomed.txt", "keep.txt" }, "zit add" }, .{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit" }, }) |st| { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, st[0], st[1], "S10", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } const doomed = blk: { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{ "ls", "--long" }, "zit ls --long", "S10", citation)) orelse return false; defer gpa.free(r.stdout); defer gpa.free(r.stderr); const h = lsHashOf(r.stdout, "doomed.txt") orelse { try out.print("FAIL S10 ({s}): 'zit ls' did not list doomed.txt\n stdout: {s}\n", .{ citation, r.stdout }); return false; }; break :blk try arena.dupe(u8, h); }; // A real fossil repo clones the whole thing, so it holds the doomed artifact // and shares zit's project code (without which the exchange is refused). const clone_repo = try std.fs.path.join(arena, &.{ base, "peer.fossil" }); const co_dir = try std.fs.path.join(arena, &.{ base, "co" }); try std.Io.Dir.cwd().createDirPath(io, co_dir); { const port = try freshLoopbackPort(io); const port_str = try std.fmt.allocPrint(arena, "{d}", .{port}); var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str); defer server.kill(io); waitForListen(io, port) catch { try out.print("FAIL S10 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, port }); return false; }; const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port}); const res = try runFossil(gpa, io, &.{ "fossil", "clone", url, clone_repo }, base, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S10 ({s}): fossil clone of the zit store failed\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr }); try printMismatchFooter(out, citation); return false; } } { const res = try runFossil(gpa, io, &.{ "fossil", "open", "--nosync", clone_repo }, co_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S10 ({s}): fossil open of the clone failed\n stderr: {s}\n", .{ citation, res.stderr }); try printMismatchFooter(out, citation); return false; } } // The operator removes it. `--force` reclaims, so the bytes are gone and the // ONLY thing that can keep them out on the way back is §7.7 admission. { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{ "shun", "--force", doomed }, "zit shun --force", "S10", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } const store_path = try std.fs.path.join(arena, &.{ src_dir, ".zit" }); const size_before = try fileSizeAt(io, store_path); // fossil pushes back into `zit serve`. fossil uploads what its own inventory // says the peer lacks, so the shunned artifact goes on the wire unsolicited. { const port = try freshLoopbackPort(io); const port_str = try std.fmt.allocPrint(arena, "{d}", .{port}); var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str); defer server.kill(io); waitForListen(io, port) catch { try out.print("FAIL S10 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, port }); return false; }; const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port}); // `--verily` is fossil's own "exchange extra information ... to ensure no // content is overlooked" mode: it re-offers what its sent-flags would // otherwise suppress, which is what puts a file card for the shunned // artifact back on the wire. A plain `fossil push` after a clone has // nothing marked unsent and sends no content at all. const res = try runFossil(gpa, io, &.{ "fossil", "push", "--verily", url }, co_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); // "it emits no error" — fossil reports a protocol error card as a failed // push, so a clean exit is the interoperable reading of that clause. if (!okExit(res.term)) { try out.print("FAIL S10 ({s}): fossil push into zit serve failed — a discarded shunned target must raise NO protocol error\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr }); try printMismatchFooter(out, citation); return false; } server.kill(io); } // THE PROOF: "makes no repository mutation". The store did not grow, so the // artifact the operator removed did not come back through the serve door. const size_after = try fileSizeAt(io, store_path); if (size_after != size_before) { try out.print("FAIL S10 ({s}): the served store GREW across a fossil push of shunned content ({d} -> {d} bytes) — the receiver re-admitted what the operator removed\n", .{ citation, size_before, size_after }); try printMismatchFooter(out, citation); return false; } try out.print("PASS S10 ({s}): a real fossil push of a shunned artifact into zit serve completes with no protocol error and lands nothing\n", .{citation}); return true; } // S11 -- a real fossil push REFUSED by `zit serve`'s content policy (#88). // // S10 drives the same direction (fossil as sync client into `zit serve`) for a // shunned artifact, and its own comment is careful that the scenario survives // deleting the server's admission check, because a conformant fossil never puts // that card on the wire unsolicited. This one does NOT have that weakness: the // violating file is ordinary new content fossil is pushing because it is new, // so the server genuinely sees it and genuinely decides. // // WHAT THIS PINS. A stock fossil client pushing a check-in that violates the // served repository's declared content policy is refused: the server stores // nothing, and fossil surfaces the server's own refusal TEXT to the operator. // The error card is the §11.1 r2 free-text carrier the auth and sparse-origin // refusals already use; this proves a real fossil reads it rather than choking. // // WHAT THIS DOES NOT PIN, STATED PLAINLY. fossil exits ZERO after printing the // refusal. Verified by hand against 2.28, and it is why the check below asserts // on the store and on fossil's OUTPUT rather than on its exit status. So the // refusal is advisory in exit-code terms to a fossil client: a script running // `fossil push && deploy` proceeds. zit's own client exits 1. That is fossil's // choice about a protocol-legal error card, not something this server can fix, // and an operator relying on a policy gate needs to know it. fn runPushPolicyRefusalScenario( 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 = "#88"; const abs_exe = try zitAbsPath(arena, io); const base = try std.fs.path.join(arena, &.{ tmp_root, "S11" }); const src_dir = try std.fs.path.join(arena, &.{ base, "src" }); try std.Io.Dir.cwd().createDirPath(io, src_dir); { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, &.{"init"}, "zit init", "S11", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = try std.fs.path.join(arena, &.{ src_dir, "seed.txt" }), .data = "a file with nothing against it\n", }); inline for (.{ .{ &[_][]const u8{ "add", "seed.txt" }, "zit add" }, .{ &[_][]const u8{ "commit", "-m", "seed", "--user", "u" }, "zit commit" }, // The server declares its policy: *.ok are the binaries it expects, so // anything else carrying a NUL is refused. Undeclared would accept // everything, which is the state of every server that never asked. .{ &[_][]const u8{ "settings", "binary-glob", "*.ok" }, "zit settings binary-glob" }, }) |st| { const r = (try zitStepIn(gpa, io, out, abs_exe, src_dir, env, st[0], st[1], "S11", citation)) orelse return false; gpa.free(r.stdout); gpa.free(r.stderr); } const clone_repo = try std.fs.path.join(arena, &.{ base, "peer.fossil" }); const co_dir = try std.fs.path.join(arena, &.{ base, "co" }); try std.Io.Dir.cwd().createDirPath(io, co_dir); { const port = try freshLoopbackPort(io); const port_str = try std.fmt.allocPrint(arena, "{d}", .{port}); var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str); defer server.kill(io); waitForListen(io, port) catch { try out.print("FAIL S11 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, port }); return false; }; const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port}); const res = try runFossil(gpa, io, &.{ "fossil", "clone", url, clone_repo }, base, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S11 ({s}): fossil clone of the zit store failed\n stderr: {s}\n", .{ citation, res.stderr }); try printMismatchFooter(out, citation); return false; } } { const res = try runFossil(gpa, io, &.{ "fossil", "open", "--nosync", clone_repo }, co_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S11 ({s}): fossil open of the clone failed\n stderr: {s}\n", .{ citation, res.stderr }); try printMismatchFooter(out, citation); return false; } } // A binary file the policy does not cover, authored and committed by fossil. try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = try std.fs.path.join(arena, &.{ co_dir, "bad.bin" }), .data = "a\x00b\n", }); inline for (.{ .{ &[_][]const u8{ "fossil", "add", "bad.bin" }, "fossil add" }, // --nosync: the served repository is deliberately not listening between // the clone and the push, and fossil aborts a commit whose autosync // cannot reach the peer. The push below is the exchange under test. .{ &[_][]const u8{ "fossil", "commit", "-m", "binary from fossil", "--no-warnings", "--nosync" }, "fossil commit" }, }) |st| { const res = try runFossil(gpa, io, st[0], co_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); if (!okExit(res.term)) { try out.print("FAIL S11 ({s}): {s} failed\n stderr: {s}\n", .{ citation, st[1], res.stderr }); try printMismatchFooter(out, citation); return false; } } const store_path = try std.fs.path.join(arena, &.{ src_dir, ".zit" }); const size_before = try fileSizeAt(io, store_path); { const port = try freshLoopbackPort(io); const port_str = try std.fmt.allocPrint(arena, "{d}", .{port}); var server = try spawnZitServe(io, abs_exe, src_dir, env, port_str); defer server.kill(io); waitForListen(io, port) catch { try out.print("FAIL S11 ({s}): zit serve never bound 127.0.0.1:{d}\n", .{ citation, port }); return false; }; const url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/", .{port}); const res = try runFossil(gpa, io, &.{ "fossil", "push", url }, co_dir, env); defer gpa.free(res.stdout); defer gpa.free(res.stderr); // The interop claim: fossil READS the error card and shows its text. // Asserted on output, not on exit status -- see the header note. const combined_has = std.mem.indexOf(u8, res.stdout, "bad.bin") != null or std.mem.indexOf(u8, res.stderr, "bad.bin") != null; const named_rule = std.mem.indexOf(u8, res.stdout, "binary-glob") != null or std.mem.indexOf(u8, res.stderr, "binary-glob") != null; if (!combined_has or !named_rule) { try out.print("FAIL S11 ({s}): fossil did not surface the server's refusal naming the file and the rule\n stdout: {s}\n stderr: {s}\n", .{ citation, res.stdout, res.stderr }); try printMismatchFooter(out, citation); return false; } server.kill(io); } // THE PROOF: the refused round stored nothing. A policy that printed a // refusal and kept the bytes would be worse than no policy. const size_after = try fileSizeAt(io, store_path); if (size_after != size_before) { try out.print("FAIL S11 ({s}): the served store GREW across a REFUSED push ({d} -> {d} bytes)\n", .{ citation, size_before, size_after }); try printMismatchFooter(out, citation); return false; } try out.print("PASS S11 ({s}): a stock fossil push violating the server's content policy is refused, the store is unchanged, and fossil surfaces the refusal naming the file and the rule\n", .{citation}); return true; }