Artifact 3bba281466e6cfb17985865d75f6eaef46eacb4242fd21cbda81c5e36e38cff9:
- File
src/smoke.zig
— part of check-in
[230c2bbebe]
at
2026-07-30 10:34:15
on branch shun-implementation
— shun S4: stop advertising what the shun table names
LogIndexStore keeps a shunned holding in its enumeration and MASKS it rather than removing it, so a shun row appended between two rounds of one clone cannot renumber the survivors under the pager's cursor. held() answers false, the name leaves by_name, and SS9.5's 'igot for every eligible holding after the SS3.3 filters' is where that mask lives.
The plan overstated what was missing. S1's log-level mask already nulled content and wire, so a clone silently skipped a shunned artifact before this slice. What was actually broken is narrower and worse: because by_name still resolved the name, emitCloneReply's basis proof fired in a safety build and the SERVE PROCESS PANICKED -- a fossil clone got 'server did not reply', not an absent artifact. Reproduced against fossil 2.28 and pinned as smoke S9.
That also means the planned e2e ('a clone does not receive it') was never mutation-sensitive: it passes with this slice fully removed. The observable that pins the slice is the ABSENCE of an igot card naming a shunned artifact in a non-clone round's inventory walk, which now has its own test and a second round in the raw-HTTP e2e case.
Smoke uses a defer-shunned fixture, not the planned packed one: pack rebuilds the delta chains and so removes the exact hazard the check exists to catch.
logstore.zig carries a comment-only change: LogStore.shunned's doc claimed two questions were open that S4 and S5 have since closed. (user: dmestas, size: 70475) [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 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 = @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; // 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; try out.flush(); if (!all_pass) std.process.exit(1); } // --------------------------------------------------------------------------- // 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", 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. 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; if (!std.mem.eql(u8, std.mem.trim(u8, line[sp + 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 `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"}, "zit ls (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 name is a PHANTOM (it has the // name, not the content — absent), and every blob it DID receive checks clean. { 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 (!reportsPhantom(res.stdout, doomed) and !reportsPhantom(res.stderr, doomed)) { try out.print("FAIL S9 ({s}): fossil does not report the shunned artifact as a phantom — it either received it or holds it in some other state\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 reports the shunned artifact ABSENT (phantom) with zero integrity errors\n", .{citation}); return true; }