//! fossil-muscle-memory — the differential transcript (zit#162).
//!
//! 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 carries its issue number and also passes — until the issue lands 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 issue 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 (zit#170).
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 = "zit#0" } }, 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 = "zit#0" } }, 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);
}