Skip to content
zit Artifact [b378e90bef]
Login

Artifact [b378e90bef]

Artifact b378e90bef4cfb56e2fafd0036dd939e4be26e70d1acfc236808fedb93d0f9d9:


//! fossil-muscle-memory — the differential transcript.
//!
//! One scripted session of the command lines a Fossil user types most, run
//! step for step through the real `fossil` binary and the real `zit` binary
//! in equivalent repositories against local servers. Per step both sides'
//! exit status, stdout and stderr are captured and compared on two axes:
//!
//!   rc     — the exit-status CLASS (success vs failure), not the number:
//!            zit's contract is 0/1/2 where fossil is 0/1, so numbers differ
//!            by design and classes should not.
//!   speech — whether the verb said anything at all (stdout or stderr):
//!            silence where fossil speaks is the "did the update do
//!            anything?" class of gap; a wall of text where fossil says one
//!            line is the typo-dump class.
//!
//! A step whose divergence is DELIBERATE (zit exits 1 on a conflicting
//! merge where fossil exits 0; success is silent on stdout) carries the
//! reason in its `expect`, and the run passes. A divergence that is a known
//! gap names the gap and also passes — until the gap is closed and
//! the entry is removed, at which point the same divergence becomes the
//! failure this harness exists to catch. Anything else fails the run.
//!
//! Milestone-manual like `smoke`: lives behind `zig build muscle`, skips
//! cleanly when `fossil` is absent, is compiled by `zig build test` and
//! never run by it.
const std = @import("std");
const builtin = @import("builtin");
const build_options = @import("build_options");
const net = std.Io.net;

const zit_exe: []const u8 = build_options.zit_exe;

/// What a step expects on one axis.
const Axis = union(enum) {
    same,
    /// A deliberate design difference — passes, forever.
    by_design: []const u8,
    /// A known gap, tracked — passes until the gap is closed and the
    /// entry removed.
    gap: []const u8,
};

const Expect = struct {
    rc: Axis = .same,
    speech: Axis = .same,
    /// Upper bound on zit's stderr line count for this step, when the step
    /// is about diagnostics rather than data (the typo-dump class).
    zit_err_lines_max: ?usize = null,
};

const Where = enum { root, work };

/// A file written into BOTH working trees before the step runs.
const Pre = struct {
    path: []const u8,
    content: []const u8,
    append: bool = false,
};

const Cmd = []const []const u8;

const Step = struct {
    name: []const u8,
    where: Where = .work,
    pre: ?Pre = null,
    fossil: []const Cmd,
    zit: []const Cmd,
    expect: Expect = .{},
};

const binary_blob = "\x00\x01\x02\xff\xfe\x00binary\x00";

/// The session. Every line is something a Fossil user types on an ordinary
/// day; the fossil and zit forms are the closest honest equivalents.
const steps = [_]Step{
    .{
        .name = "clone + open",
        .where = .root,
        .fossil = &.{ &.{ "fossil", "clone", "FOSSIL_URL", "work.fossil" }, &.{ "fossil", "open", "work.fossil", "--workdir", "work" } },
        .zit = &.{&.{ "ZIT", "clone", "ZIT_URL", "work" }},
    },
    .{ .name = "status, clean", .fossil = &.{&.{ "fossil", "status" }}, .zit = &.{&.{ "ZIT", "status" }} },
    .{
        .name = "changes (alias)",
        .fossil = &.{&.{ "fossil", "changes" }},
        .zit = &.{&.{ "ZIT", "changes" }},
    },
    .{ .name = "sync, nothing to do", .fossil = &.{&.{ "fossil", "sync" }}, .zit = &.{&.{ "ZIT", "sync" }} },
    .{
        .name = "edit + commit (autosync)",
        .pre = .{ .path = "f.txt", .content = "two\n", .append = true },
        .fossil = &.{&.{ "fossil", "commit", "-m", "c2", "--no-warnings" }},
        .zit = &.{&.{ "ZIT", "ci", "-m", "c2" }},
    },
    .{ .name = "push, nothing to do", .fossil = &.{&.{ "fossil", "push" }}, .zit = &.{&.{ "ZIT", "push" }} },
    .{ .name = "pull, nothing to do", .fossil = &.{&.{ "fossil", "pull" }}, .zit = &.{&.{ "ZIT", "pull" }} },
    .{ .name = "timeline -n 2", .fossil = &.{&.{ "fossil", "timeline", "-n", "2" }}, .zit = &.{&.{ "ZIT", "timeline", "-n", "2" }} },
    .{ .name = "ti (prefix)", .fossil = &.{&.{ "fossil", "tim", "-n", "1" }}, .zit = &.{&.{ "ZIT", "tim", "-n", "1" }} },
    .{ .name = "info", .fossil = &.{&.{ "fossil", "info" }}, .zit = &.{&.{ "ZIT", "info" }} },
    .{ .name = "ls", .fossil = &.{&.{ "fossil", "ls" }}, .zit = &.{&.{ "ZIT", "ls" }} },
    .{ .name = "extras", .pre = .{ .path = "extra.txt", .content = "x\n" }, .fossil = &.{&.{ "fossil", "extras" }}, .zit = &.{&.{ "ZIT", "extras" }} },
    .{
        .name = "status with an edit",
        .pre = .{ .path = "f.txt", .content = "three\n", .append = true },
        .fossil = &.{&.{ "fossil", "status" }},
        .zit = &.{&.{ "ZIT", "status" }},
    },
    .{ .name = "diff", .fossil = &.{&.{ "fossil", "diff" }}, .zit = &.{&.{ "ZIT", "diff" }} },
    .{ .name = "revert", .fossil = &.{&.{ "fossil", "revert" }}, .zit = &.{&.{ "ZIT", "revert" }} },
    .{ .name = "update, already there", .fossil = &.{&.{ "fossil", "update", "trunk" }}, .zit = &.{&.{ "ZIT", "update", "trunk" }} },
    .{
        .name = "add + commit a binary",
        .pre = .{ .path = "b.dat", .content = binary_blob },
        .fossil = &.{ &.{ "fossil", "add", "b.dat" }, &.{ "fossil", "commit", "-m", "bin" } },
        .zit = &.{ &.{ "ZIT", "add", "b.dat" }, &.{ "ZIT", "commit", "-m", "bin" } },
        .expect = .{ .rc = .{ .by_design = "zit's content gate is opt-in (binary-glob/crlf-glob); fossil warns by default" } },
    },
    .{
        .name = "commit with no message",
        .pre = .{ .path = "f.txt", .content = "four\n", .append = true },
        .fossil = &.{&.{ "fossil", "commit", "--no-warnings" }},
        .zit = &.{&.{ "ZIT", "commit" }},
    },
    .{ .name = "commit -m (clean up)", .fossil = &.{&.{ "fossil", "commit", "-m", "c4", "--no-warnings" }}, .zit = &.{&.{ "ZIT", "commit", "-m", "c4" }} },
    .{ .name = "branch list", .fossil = &.{&.{ "fossil", "branch", "list" }}, .zit = &.{&.{ "ZIT", "branch" }} },
    .{
        .name = "tag list",
        .fossil = &.{&.{ "fossil", "tag", "list" }},
        .zit = &.{&.{ "ZIT", "tag", "list" }},
        .expect = .{ .speech = .{ .by_design = "zit's tag list excludes branch names; 'zit branch' lists them" } },
    },
    .{ .name = "leaves", .fossil = &.{&.{ "fossil", "leaves" }}, .zit = &.{&.{ "ZIT", "leaves" }} },
    .{ .name = "annotate", .fossil = &.{&.{ "fossil", "annotate", "f.txt" }}, .zit = &.{&.{ "ZIT", "annotate", "f.txt" }} },
    .{ .name = "blame (alias)", .fossil = &.{&.{ "fossil", "blame", "f.txt" }}, .zit = &.{&.{ "ZIT", "blame", "f.txt" }} },
    .{
        .name = "stash save",
        .pre = .{ .path = "f.txt", .content = "wip\n", .append = true },
        .fossil = &.{&.{ "fossil", "stash", "save", "-m", "wip" }},
        .zit = &.{&.{ "ZIT", "stash", "save", "-m", "wip" }},
        .expect = .{ .speech = .{ .by_design = "zit names the files it stashed; fossil is silent" } },
    },
    .{ .name = "stash list", .fossil = &.{&.{ "fossil", "stash", "list" }}, .zit = &.{&.{ "ZIT", "stash", "list" }} },
    .{ .name = "stash pop", .fossil = &.{&.{ "fossil", "stash", "pop" }}, .zit = &.{&.{ "ZIT", "stash", "pop" }} },
    .{ .name = "revert one file", .fossil = &.{&.{ "fossil", "revert", "f.txt" }}, .zit = &.{&.{ "ZIT", "revert", "f.txt" }} },
    .{ .name = "branch new", .fossil = &.{&.{ "fossil", "branch", "new", "feat", "trunk" }}, .zit = &.{&.{ "ZIT", "branch", "new", "feat", "trunk" }} },
    .{ .name = "update to a branch", .fossil = &.{&.{ "fossil", "update", "feat" }}, .zit = &.{&.{ "ZIT", "update", "feat" }} },
    .{
        .name = "commit on the branch",
        .pre = .{ .path = "f.txt", .content = "feature\n", .append = true },
        .fossil = &.{&.{ "fossil", "commit", "-m", "feature", "--no-warnings" }},
        .zit = &.{&.{ "ZIT", "commit", "-m", "feature" }},
    },
    .{ .name = "update to trunk", .fossil = &.{&.{ "fossil", "update", "trunk" }}, .zit = &.{&.{ "ZIT", "update", "trunk" }} },
    .{
        .name = "commit on trunk",
        .pre = .{ .path = "f.txt", .content = "trunk\n", .append = true },
        .fossil = &.{&.{ "fossil", "commit", "-m", "trunk", "--no-warnings" }},
        .zit = &.{&.{ "ZIT", "commit", "-m", "trunk" }},
    },
    .{
        .name = "merge with a conflict",
        .fossil = &.{&.{ "fossil", "merge", "feat" }},
        .zit = &.{&.{ "ZIT", "merge", "feat" }},
        .expect = .{ .rc = .{ .by_design = "zit exits 1 on a conflicting merge; fossil exits 0" } },
    },
    .{ .name = "status with a conflict", .fossil = &.{&.{ "fossil", "status" }}, .zit = &.{&.{ "ZIT", "status" }} },
    .{ .name = "undo", .fossil = &.{&.{ "fossil", "undo" }}, .zit = &.{&.{ "ZIT", "undo" }} },
    .{ .name = "update, bare", .fossil = &.{&.{ "fossil", "update" }}, .zit = &.{&.{ "ZIT", "update" }} },
    .{ .name = "commit --help", .fossil = &.{&.{ "fossil", "commit", "--help" }}, .zit = &.{&.{ "ZIT", "commit", "--help" }} },
    .{
        .name = "sync to a dead peer",
        .fossil = &.{&.{ "fossil", "sync", "http://127.0.0.1:1", "--once" }},
        .zit = &.{&.{ "ZIT", "sync", "http://127.0.0.1:1" }},
        .expect = .{ .rc = .{ .by_design = "zit exits 1 on an unreachable peer; fossil exits 0" } },
    },
    .{
        .name = "unknown verb",
        .fossil = &.{&.{ "fossil", "florble" }},
        .zit = &.{&.{ "ZIT", "florble" }},
        .expect = .{ .zit_err_lines_max = 3 },
    },
    .{
        .name = "ambiguous prefix",
        .fossil = &.{&.{ "fossil", "ti" }},
        .zit = &.{&.{ "ZIT", "ti" }},
        .expect = .{ .zit_err_lines_max = 2 },
    },
};

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;
    defer out.flush() catch {};

    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 — muscle is milestone-manual\n");
        return;
    }

    var arena_state = std.heap.ArenaAllocator.init(gpa);
    defer arena_state.deinit();
    const arena = arena_state.allocator();
    const root = try tempRoot(arena, init.environ_map.*);
    const cwd = std.Io.Dir.cwd();
    cwd.deleteTree(io, root) catch {};
    try cwd.createDirPath(io, root);
    defer cwd.deleteTree(io, root) catch {};

    var env = try init.environ_map.clone(gpa);
    defer env.deinit();
    const home_dir = try std.fs.path.join(arena, &.{ root, "home" });
    try cwd.createDirPath(io, home_dir);
    try env.put("FOSSIL_HOME", home_dir);
    try env.put("USER", "muscle");
    const abs_exe = try zitAbsPath(arena, io);

    // ---- origins, served ----
    const f_root = try std.fs.path.join(arena, &.{ root, "f" });
    const z_root = try std.fs.path.join(arena, &.{ root, "z" });
    const f_origin_co = try std.fs.path.join(arena, &.{ f_root, "o" });
    const z_origin = try std.fs.path.join(arena, &.{ z_root, "o" });
    try cwd.createDirPath(io, f_origin_co);
    try cwd.createDirPath(io, z_origin);
    try mustRun(gpa, io, out, &.{ "fossil", "init", "origin.fossil" }, f_root, &env);
    try mustRun(gpa, io, out, &.{ "fossil", "open", "../origin.fossil" }, f_origin_co, &env);
    try writeFile(io, f_origin_co, "f.txt", "one\n", false);
    try mustRun(gpa, io, out, &.{ "fossil", "add", "f.txt" }, f_origin_co, &env);
    try mustRun(gpa, io, out, &.{ "fossil", "commit", "-m", "base", "--no-warnings" }, f_origin_co, &env);
    try mustRun(gpa, io, out, &.{ abs_exe, "init" }, z_origin, &env);
    try writeFile(io, z_origin, "f.txt", "one\n", false);
    try mustRun(gpa, io, out, &.{ abs_exe, "add", "f.txt" }, z_origin, &env);
    try mustRun(gpa, io, out, &.{ abs_exe, "commit", "-m", "base" }, z_origin, &env);

    const f_port = try freshLoopbackPort(io);
    const z_port = try freshLoopbackPort(io);
    const f_port_str = try std.fmt.allocPrint(arena, "{d}", .{f_port});
    const z_port_str = try std.fmt.allocPrint(arena, "{d}", .{z_port});
    const origin_repo = try std.fs.path.join(arena, &.{ f_root, "origin.fossil" });
    var f_server = try spawnFossilServe(gpa, io, out, origin_repo, f_root, &env, f_port_str);
    defer stopFossilServe(io, &f_server);
    var z_server = try std.process.spawn(io, .{
        .argv = &.{ abs_exe, "serve", "--port", z_port_str },
        .cwd = .{ .path = z_origin },
        .environ_map = &env,
        .stdout = .ignore,
        .stderr = .ignore,
    });
    defer z_server.kill(io);
    try waitForListen(io, f_port);
    try waitForListen(io, z_port);
    const f_url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}", .{f_port});
    const z_url = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}", .{z_port});

    const f_work = try std.fs.path.join(arena, &.{ f_root, "work" });
    const z_work = try std.fs.path.join(arena, &.{ z_root, "work" });

    // `zig build ux`: every side's exact bytes, one file per stream, for
    // the fossil-vs-zit reading page.
    const transcript_dir = init.environ_map.get("MUSCLE_TRANSCRIPT");
    if (transcript_dir) |dir| {
        cwd.deleteTree(io, dir) catch {};
        try cwd.createDirPath(io, try std.fs.path.join(arena, &.{ dir, "f" }));
        try cwd.createDirPath(io, try std.fs.path.join(arena, &.{ dir, "z" }));
    }
    try out.writeAll("step                          | fossil rc out/err | zit rc out/err | verdict\n");
    var failures: usize = 0;
    for (steps, 1..) |step, index| {
        const f_cwd = if (step.where == .root) f_root else f_work;
        const z_cwd = if (step.where == .root) z_root else z_work;
        if (step.pre) |pre| {
            try writeFile(io, f_work, pre.path, pre.content, pre.append);
            try writeFile(io, z_work, pre.path, pre.content, pre.append);
        }
        const f_res = try runSeq(gpa, arena, io, step.fossil, f_cwd, &env, abs_exe, f_url, z_url);
        const z_res = try runSeq(gpa, arena, io, step.zit, z_cwd, &env, abs_exe, f_url, z_url);
        const verdict = judge(step.expect, f_res, z_res);
        if (transcript_dir) |dir| {
            try writeTranscript(io, arena, dir, "f", index, step.name, step.fossil, f_res);
            try writeTranscript(io, arena, dir, "z", index, step.name, step.zit, z_res);
        }
        try out.print("{s:<30}| {d:>2} {d:>3}/{d:<3}       | {d:>2} {d:>3}/{d:<3}      | {s}\n", .{
            step.name,
            f_res.rc,
            f_res.out_lines,
            f_res.err_lines,
            z_res.rc,
            z_res.out_lines,
            z_res.err_lines,
            verdict.text,
        });
        if (!verdict.pass) {
            failures += 1;
            try out.print("  fossil stdout: {s}\n  fossil stderr: {s}\n  zit stdout:    {s}\n  zit stderr:    {s}\n", .{
                firstLine(f_res.stdout), firstLine(f_res.stderr), firstLine(z_res.stdout), firstLine(z_res.stderr),
            });
        }
    }
    if (failures != 0) {
        try out.print("FAIL muscle: {d} unexpected divergence(s)\n", .{failures});
        try out.flush();
        std.process.exit(1);
    }
    try out.print("PASS muscle: {d} steps, every divergence deliberate or tracked\n", .{steps.len});
    if (transcript_dir) |dir| try out.print("transcript: {s}\n", .{dir});
}

const Res = struct {
    rc: u8,
    stdout: []const u8,
    stderr: []const u8,
    out_lines: usize,
    err_lines: usize,
    fn speaks(self: Res) bool {
        return std.mem.trim(u8, self.stdout, " \t\r\n").len != 0 or std.mem.trim(u8, self.stderr, " \t\r\n").len != 0;
    }
};

const Verdict = struct { pass: bool, text: []const u8 };

fn judge(expect: Expect, f: Res, z: Res) Verdict {
    const rc_same = (f.rc == 0) == (z.rc == 0);
    const speech_same = f.speaks() == z.speaks();
    switch (expect.rc) {
        .same => if (!rc_same) return .{ .pass = false, .text = "FAIL rc class differs" },
        .by_design => |why| return checkRest(expect, speech_same, z, why, "by design"),
        .gap => |why| return checkRest(expect, speech_same, z, why, "tracked gap"),
    }
    return checkRest(expect, speech_same, z, null, null);
}

fn checkRest(expect: Expect, speech_same: bool, z: Res, rc_why: ?[]const u8, rc_kind: ?[]const u8) Verdict {
    switch (expect.speech) {
        .same => if (!speech_same) return .{ .pass = false, .text = "FAIL one side is silent" },
        .by_design, .gap => {},
    }
    if (expect.zit_err_lines_max) |max| {
        if (z.err_lines > max) return .{ .pass = false, .text = "FAIL zit stderr too long" };
    }
    if (rc_why) |_| return .{ .pass = true, .text = if (std.mem.eql(u8, rc_kind.?, "by design")) "ok (rc differs by design)" else "ok (rc gap, tracked)" };
    switch (expect.speech) {
        .by_design => return .{ .pass = true, .text = "ok (speech differs by design)" },
        .gap => return .{ .pass = true, .text = "ok (speech gap, tracked)" },
        .same => return .{ .pass = true, .text = "ok" },
    }
}

/// Run each command of a step in order; the step's result is the LAST
/// command's, unless an earlier one fails, in which case that failure is the
/// result (a clone that fails has no open to speak of).
fn runSeq(
    gpa: std.mem.Allocator,
    arena: std.mem.Allocator,
    io: std.Io,
    cmds: []const Cmd,
    cwd_path: []const u8,
    env: *const std.process.Environ.Map,
    abs_exe: []const u8,
    f_url: []const u8,
    z_url: []const u8,
) !Res {
    var last: ?Res = null;
    for (cmds) |cmd| {
        var argv = try arena.alloc([]const u8, cmd.len);
        var cwd_here = cwd_path;
        for (cmd, 0..) |a, i| {
            argv[i] = if (std.mem.eql(u8, a, "ZIT")) abs_exe else if (std.mem.eql(u8, a, "FOSSIL_URL")) f_url else if (std.mem.eql(u8, a, "ZIT_URL")) z_url else a;
        }
        // `open --in DIR` is the harness's spelling for "run open inside DIR"
        // (zit opens in place; fossil opens with --workdir).
        if (cmd.len >= 4 and std.mem.eql(u8, cmd[1], "open") and std.mem.eql(u8, cmd[2], "--in")) {
            cwd_here = try std.fs.path.join(arena, &.{ cwd_path, cmd[3] });
            argv = argv[0..2];
        }
        const r = try std.process.run(gpa, io, .{ .argv = argv, .cwd = .{ .path = cwd_here }, .environ_map = env });
        const rc: u8 = switch (r.term) {
            .exited => |c| c,
            else => 255,
        };
        const res: Res = .{
            .rc = rc,
            .stdout = try arena.dupe(u8, r.stdout),
            .stderr = try arena.dupe(u8, r.stderr),
            .out_lines = countLines(r.stdout),
            .err_lines = countLines(r.stderr),
        };
        gpa.free(r.stdout);
        gpa.free(r.stderr);
        last = res;
        if (rc != 0) break;
    }
    return last.?;
}

fn countLines(s: []const u8) usize {
    const t = std.mem.trimEnd(u8, s, "\n");
    if (t.len == 0) return 0;
    return std.mem.count(u8, t, "\n") + 1;
}

fn firstLine(s: []const u8) []const u8 {
    const t = std.mem.trim(u8, s, "\r\n");
    const i = std.mem.indexOfScalar(u8, t, '\n') orelse return t;
    return t[0..i];
}

fn writeFile(io: std.Io, dir_path: []const u8, name: []const u8, content: []const u8, append: bool) !void {
    var dir = try std.Io.Dir.cwd().openDir(io, dir_path, .{});
    defer dir.close(io);
    var buf: [1 << 16]u8 = undefined;
    var existing: []const u8 = "";
    if (append) {
        if (dir.openFile(io, name, .{})) |f| {
            defer f.close(io);
            const n = try f.readPositionalAll(io, &buf, 0);
            existing = buf[0..n];
        } else |_| {}
    }
    var f = try dir.createFile(io, name, .{});
    defer f.close(io);
    try f.writePositionalAll(io, existing, 0);
    try f.writePositionalAll(io, content, existing.len);
}

fn mustRun(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) !void {
    const r = try std.process.run(gpa, io, .{ .argv = argv, .cwd = .{ .path = cwd_path }, .environ_map = env });
    defer gpa.free(r.stdout);
    defer gpa.free(r.stderr);
    if (!okExit(r.term)) {
        try out.print("setup failed: {s} in {s}\n  stderr: {s}\n", .{ argv[1], cwd_path, r.stderr });
        return error.SetupFailed;
    }
}

fn okExit(term: std.process.Child.Term) bool {
    return switch (term) {
        .exited => |code| code == 0,
        else => false,
    };
}

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;
}

/// One step's side under `DIR/{f,z}/NN-slug.{cmd,rc,out,err}`, plus an
/// `ORDER` file naming the steps in sequence.
fn writeTranscript(io: std.Io, arena: std.mem.Allocator, dir: []const u8, side: []const u8, index: usize, name: []const u8, cmds: []const Cmd, res: Res) !void {
    const slug = try arena.dupe(u8, name);
    for (slug) |*c| c.* = if (std.ascii.isAlphanumeric(c.*)) std.ascii.toLower(c.*) else '-';
    const base = try std.fmt.allocPrint(arena, "{d:0>2}-{s}", .{ index, slug });
    const side_dir = try std.fs.path.join(arena, &.{ dir, side });
    var cmd_text: std.ArrayList(u8) = .empty;
    for (cmds) |cmd| {
        for (cmd, 0..) |arg, i| {
            if (i != 0) try cmd_text.append(arena, ' ');
            try cmd_text.appendSlice(arena, if (std.mem.eql(u8, arg, "ZIT")) "zit" else arg);
        }
        try cmd_text.append(arena, '\n');
    }
    try writeFile(io, side_dir, try std.fmt.allocPrint(arena, "{s}.cmd", .{base}), cmd_text.items, false);
    try writeFile(io, side_dir, try std.fmt.allocPrint(arena, "{s}.rc", .{base}), try std.fmt.allocPrint(arena, "{d}\n", .{res.rc}), false);
    try writeFile(io, side_dir, try std.fmt.allocPrint(arena, "{s}.out", .{base}), res.stdout, false);
    try writeFile(io, side_dir, try std.fmt.allocPrint(arena, "{s}.err", .{base}), res.stderr, false);
    try writeFile(io, side_dir, "ORDER", try std.fmt.allocPrint(arena, "{s}\n", .{base}), true);
}

fn tempRoot(arena: std.mem.Allocator, env: std.process.Environ.Map) ![]const u8 {
    const base = env.get("TMPDIR") orelse "/tmp";
    return std.fs.path.join(arena, &.{ std.mem.trimEnd(u8, base, "/"), "zit-muscle" });
}

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 });
}

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 {
    try mustRun(gpa, io, out, &.{ "fossil", "settings", "backoffice-disable", "1", "-R", repo_path }, cwd_path, env);
    return std.process.spawn(io, .{
        .argv = &.{ "fossil", "server", "--port", port_str, "--localauth", repo_path },
        .cwd = .{ .path = cwd_path },
        .environ_map = env,
        .stdout = .ignore,
        .stderr = .ignore,
        .pgid = if (builtin.os.tag == .windows) null else 0,
    });
}

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);
}

fn freshLoopbackPort(io: std.Io) !u16 {
    const addr = net.IpAddress.parseIp4("127.0.0.1", 0) catch return error.BadTestAddress;
    var server = try addr.listen(io, .{ .reuse_address = true });
    defer server.deinit(io);
    return server.socket.address.getPort();
}

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;
    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;
    }
}

test "muscle: countLines and firstLine" {
    try std.testing.expectEqual(@as(usize, 0), countLines(""));
    try std.testing.expectEqual(@as(usize, 1), countLines("a\n"));
    try std.testing.expectEqual(@as(usize, 2), countLines("a\nb"));
    try std.testing.expectEqualStrings("a", firstLine("a\nb\n"));
}

test "muscle: judge classes rc by success, not by number, and honours tracked gaps" {
    const ok0: Res = .{ .rc = 0, .stdout = "x\n", .stderr = "", .out_lines = 1, .err_lines = 0 };
    const ok2: Res = .{ .rc = 2, .stdout = "", .stderr = "e\n", .out_lines = 0, .err_lines = 1 };
    const fail1: Res = .{ .rc = 1, .stdout = "", .stderr = "e\n", .out_lines = 0, .err_lines = 1 };
    try std.testing.expect(judge(.{}, fail1, ok2).pass); // both non-zero: same class
    try std.testing.expect(!judge(.{}, ok0, ok2).pass); // 0 vs 2: differs
    try std.testing.expect(judge(.{ .rc = .{ .gap = "a tracked gap" } }, ok0, ok2).pass);
    const silent: Res = .{ .rc = 0, .stdout = "", .stderr = "", .out_lines = 0, .err_lines = 0 };
    try std.testing.expect(!judge(.{}, ok0, silent).pass);
    try std.testing.expect(judge(.{ .speech = .{ .gap = "a tracked gap" } }, ok0, silent).pass);
    const loud: Res = .{ .rc = 2, .stdout = "", .stderr = "a\nb\nc\nd\n", .out_lines = 0, .err_lines = 4 };
    try std.testing.expect(!judge(.{ .zit_err_lines_max = 3 }, ok2, loud).pass);
}