Artifact a4b754d41131d04cdca85fd292994b3a949fe602a71ccf966a50169345ab56cd:
- File
build.zig
— part of check-in
[4e355fbfd1]
at
2026-08-18 14:12:05
on branch trunk
— build: how to measure this project without being lied to
Every rule was paid for by a wrong answer that looked convincing: an issue filed against this repository proposed an architectural change on the strength of a 58s figure that was a bare-zig-build Debug number; the ~25x pack figure this file carried came from a --release=fast that was quietly building ReleaseSafe; a single run reported a 20% regression for a change that was a 3.7x improvement; and an optimization that won by 50x was erroring out on the first artifact and printing nothing.
It sits beside the build rather than in the wiki because each rule is only true of THIS build script -- bare zig build means Debug because of the line above it, and every --release resolves to the preference for the same reason. A page elsewhere would rot the day one of those changes and nothing would fail. (user: dmestas, size: 30903) [annotate] [blame] [check-ins using]
const std = @import("std"); /// Config threaded into each per-artifact builder: target/optimize choice plus /// the single `zit` library module every root imports. storage, logstore, /// checkout, and update are folded into that one module (re-exported from /// root.zig), so there are no separate store peers to wire. const Env = struct { b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, mod: *std.Build.Module, }; pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); // `--release` means ReleaseFast: zit SHIPS ReleaseFast, so that is the mode // the default has to name. This reverses an earlier ReleaseSafe pin, and the // reversal is deliberate rather than incidental — the cost that decided it // is real, but it is SMALLER than this comment used to claim, and the // correction matters more than the decision it supports. // // Measured on wapp (6.4 MB store), warm page cache, modes interleaved and // repeated so neither pays a cold-start penalty: // // zit pack fast ~8.5 s safe ~9.5 s small ~10.7 s // ls tip fast 0.31 s safe 0.33 s small 0.36 s (tcl, 300 MB) // leaves 0.01 s in all three // // So fast buys ~12% on the write path and nothing measurable on the read // path. The figure previously recorded here — "599 ms fast against 15.2 s // safe, ~25x" — DOES NOT REPRODUCE on current code, in either the ratio or // the absolute numbers, and should be treated as stale rather than as a // second opinion: the delta encoder's search was reworked since it was // taken. A ~25x claim would justify almost any correctness cost, which is // exactly why leaving it uncorrected would have been the expensive part. // // WHAT THIS COSTS, stated plainly because it is not free. src/ carries over // a thousand `std.debug.assert` calls, and ReleaseFast compiles every one of // them to nothing. TigerStyle's rule is that assertions "downgrade // catastrophic correctness bugs into liveness bugs"; that trade only exists // while the assertion does, so in this mode a guarded corruption becomes an // unguarded one — in an append-only store, where a bad frame is permanent // and a crash is not. // // `release_assert_test.zig` is what keeps that from being dangerous, and its // job is now load-bearing rather than belt-and-braces: it forbids a NEW // `std.debug.assert` from standing in for validation of untrusted input in a // `pub fn` that returns an error union. An assert is not a rejection, and in // this mode it is not even a crash — it is nothing at all, and the bad data // flows through. Validation of anything reaching zit from a peer or a // repository file must be a returned error. The guard is the enforcement. // // STATING A PREFERENCE MAKES IT THE ONLY REACHABLE RELEASE MODE, and that is // stronger than it reads. It replaces `-Doptimize=<mode>` with // `-Drelease=<bool>` in the option surface, so `-Doptimize=ReleaseSafe` no // longer parses — and the top-level `--release=safe` does not reach it // either: every `--release[=mode]` resolves to the preference. Verified by // building each and checking for the safety-check panic strings; all three // of `--release`, `--release=safe` and `--release=fast` produce one // byte-identical binary. // // The prior ReleaseSafe pin had the same property in the other direction, // which is worth recording because the comment here used to claim otherwise: // `--release=fast` was documented as "the explicit escape hatch" and was not // one. It produced a ReleaseSafe binary, carrying the safety strings to prove // it, for as long as the preference said ReleaseSafe. Anything measured // through that flag before this commit measured the checked build. // // So changing the shipped mode means editing this line. That is the intended // cost: the mode is a project decision, not a packaging accident. // // `zig build bench` builds its executables in this same mode. A benchmark // must measure optimized code, and it must measure the code that SHIPS. const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast }); // The zit library: a Zig implementation of the Fossil formats and protocol // as specified by the four RFC drafts in docs/rfcs/. root.zig re-exports the // store layers (storage/logstore/checkout/update) and the Repository facade; // one module is the whole embeddable surface. const mod = b.addModule("zit", .{ .root_source_file = b.path("src/root.zig"), .target = target, }); const env = Env{ .b = b, .target = target, .optimize = optimize, .mod = mod, }; // `zig build test` — library + CLI unit tests, and nothing else. The store // layers and their sibling tests aggregate into `mod_tests` via root.zig's // test block; build_cli wires the CLI's inline tests in below. Every other // lane hangs off `gate`, not here. const test_step = b.step("test", "Run unit tests"); const mod_tests = b.addTest(.{ .root_module = build_unit_test_module(b, target, optimize) }); test_step.dependOn(&b.addRunArtifact(mod_tests).step); const git_import_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("src/git/git_import_test.zig"), .target = target, .optimize = optimize, .imports = &.{.{ .name = "zit", .module = mod }}, }) }); test_step.dependOn(&b.addRunArtifact(git_import_tests).step); // Task 9 refs/provenance coverage intentionally remains opt-in until the // finalization API exists, so translation tests still run independently. const git_import_refs_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("src/git/git_import_refs_test.zig"), .target = target, .optimize = optimize, .imports = &.{.{ .name = "zit", .module = mod }}, }) }); const git_import_refs_run = b.addRunArtifact(git_import_refs_tests); const git_import_refs_step = b.step("git-import-refs", "Run git import refs tests"); git_import_refs_step.dependOn(&git_import_refs_run.step); test_step.dependOn(&git_import_refs_run.step); const git_import_pipe_helper = b.addExecutable(.{ .name = "git-import-cli-pipe-helper", .root_module = b.createModule(.{ .root_source_file = b.path("src/git/git_import_cli_pipe_helper.zig"), .target = target, .optimize = optimize, }), }); const git_import_cli_options = b.addOptions(); git_import_cli_options.addOptionPath("git_import_pipe_helper", git_import_pipe_helper.getEmittedBin()); // Task 10 transaction coverage guards the executable adapter as part of // the ordinary unit-test gate, and remains separately runnable for focus. const git_import_cli_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("src/git/git_import_cli_test.zig"), .target = target, .optimize = optimize, .imports = &.{ .{ .name = "zit", .module = mod }, .{ .name = "git_import_cli_options", .module = git_import_cli_options.createModule() }, }, }) }); const git_export_cli_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("src/git/git_export_cli_test.zig"), .target = target, .optimize = optimize, .imports = &.{.{ .name = "zit", .module = mod }}, }) }); test_step.dependOn(&b.addRunArtifact(git_export_cli_tests).step); const git_import_cli_run = b.addRunArtifact(git_import_cli_tests); git_import_cli_run.step.dependOn(&git_import_pipe_helper.step); const git_import_cli_step = b.step("git-import-cli", "Run git import CLI transaction tests"); git_import_cli_step.dependOn(&git_import_cli_run.step); test_step.dependOn(&git_import_cli_run.step); // `zig build gate` — everything `test` runs, plus the compile-only proofs // that the e2e and smoke harnesses still build against the current library. // // Those two compiles used to hang off `test`, and they are not cheap: both // rebuild the whole `zit` module in the pinned release mode. Measured at the // time of the split, `zig build test` was 9m13s wall while the tests it // actually RAN accounted for well under a minute of it -- the rest was // compilation, a large share of it these two artifacts that `test` builds // and then never executes. A suite that slow between edits stops being run // between edits, which is the opposite of what a unit lane is for. // // What did NOT move: the dst, bench and smoke *unit* tests. Those are unit // tests despite their siblings' names, and `zig build dst`/`smoke` run the // simulation and the interop check rather than them -- so moving them here // would have dropped them from CI entirely, since CI invokes the lanes // individually and never invokes `gate`. Cheap to run, and the only thing // covering that code. const gate_step = b.step("gate", "Full pre-merge gate: unit tests plus the e2e and smoke compile proofs"); gate_step.dependOn(test_step); // Formatting is a merge gate because the git mirror's CI checks it: an // unformatted commit passes every local lane and then fails in Actions, // which is exactly the discovery order a gate exists to prevent. const fmt_check = b.addFmt(.{ .paths = &.{ "src", "build.zig" }, .check = true }); gate_step.dependOn(&fmt_check.step); const cli = build_cli(env, test_step); build_e2e(env, cli.exe, gate_step); build_git_import_e2e(env, cli.exe, gate_step); build_git_export_e2e(env, cli.exe, gate_step); build_fuzz(env); const dst_tests = build_dst(env, test_step); build_bench(env, cli.exe, test_step); build_git_import_bench(env, test_step); build_smoke(env, cli.exe, test_step, gate_step); build_cov(env, &.{ mod_tests, cli.tests, dst_tests }); } /// The unit-test compile of root.zig. A *separate* module from the exported /// `zit` one, purely so the repository files that src/packaging_test.zig pins /// (LICENSE, the package manifest, the versioned ignore-glob) can be reached /// as anonymous imports without attaching them to the public module. /// /// Attaching them to `zit` itself made every consumer's build depend on files /// outside `build.zig.zon`'s `.paths` — a fetched package then failed with /// `failed to check cache: '.fossil-settings/ignore-glob' file_hash /// FileNotFound` before compiling a line. The need is test-only, so the scope /// is test-only. fn build_unit_test_module( b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, ) *std.Build.Module { const test_mod = b.createModule(.{ .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, }); test_mod.addAnonymousImport("license_text", .{ .root_source_file = b.path("LICENSE") }); test_mod.addAnonymousImport("build_manifest", .{ .root_source_file = b.path("build.zig.zon") }); test_mod.addAnonymousImport("ignore_glob", .{ .root_source_file = b.path(".fossil-settings/ignore-glob") }); test_mod.addAnonymousImport("build_source", .{ .root_source_file = b.path("build.zig") }); test_mod.addAnonymousImport("ci_workflow", .{ .root_source_file = b.path(".github/workflows/ci.yml") }); // The repository root, handed to the tests as a constant. src/packaging_test.zig // asks fossil which files are version-controlled, and a test binary's working // directory is not the checkout; without this the query has nowhere to run. // Embedding a *file* cannot answer the question — whether a path is tracked is // a property of the repository, not of the working tree. // // A null `path` gets a panic rather than a "." fallback. "." is the test // binary's working directory, not the checkout, so the fallback would not // degrade the pin to a wrong answer — it would degrade it to a *silent // skip*, since `fossil ls` outside a checkout fails and the pin treats that // as "no fossil here". A configure-time panic naming the cause is the // legible failure; a pin that quietly stops checking is not. const repo = b.addOptions(); const repo_root = b.build_root.path orelse @panic("build.zig: build root has no path; src/packaging_test.zig's version-control " ++ "pin needs the checkout directory and would otherwise skip itself into silence"); repo.addOption([]const u8, "root", repo_root); test_mod.addOptions("repo_options", repo); return test_mod; } /// build_cli's two artifacts: the installed `zit` exe (the e2e harness spawns /// it) and its unit-test compile (the out-of-band cov step traces it). const Cli = struct { exe: *std.Build.Step.Compile, tests: *std.Build.Step.Compile, }; /// The `zit` command-line tool: `zig build` installs it, `zig build run -- /// <args>` runs it, and its own unit tests join `test_step`. fn build_cli(env: Env, test_step: *std.Build.Step) Cli { const exe = env.b.addExecutable(.{ .name = "zit", .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/main.zig"), .target = env.target, .optimize = env.optimize, .imports = &.{ .{ .name = "zit", .module = env.mod }, }, }), }); env.b.installArtifact(exe); // `zig build run -- <args>` const run_step = env.b.step("run", "Run the zit CLI"); const run_cmd = env.b.addRunArtifact(exe); run_step.dependOn(&run_cmd.step); run_cmd.step.dependOn(env.b.getInstallStep()); if (env.b.args) |args| run_cmd.addArgs(args); const exe_tests = env.b.addTest(.{ .root_module = exe.root_module }); test_step.dependOn(&env.b.addRunArtifact(exe_tests).step); return .{ .exe = exe, .tests = exe_tests }; } /// `zig build e2e` — end-to-end tests that spawn the REAL `zit` binary in an /// isolated, pinned environment and read the resulting repository back through /// the library. The exe's path is threaded in as a build option (a `LazyPath`, /// so the binary is always rebuilt before the tests run); the `zit` module is /// imported so the tests can decode `.zit`/`.zit-checkout` state. /// /// The subprocess-driven run stays out of `zig build test` (it spawns the real /// binary ~150 times), but the test binary is *compiled* by `test` so this /// 4.6k-line file cannot silently rot — a type error here fails `test`, at no /// execution cost. fn build_e2e(env: Env, exe: *std.Build.Step.Compile, gate_step: *std.Build.Step) void { const opts = env.b.addOptions(); opts.addOptionPath("zit_exe", exe.getEmittedBin()); const filter: ?[]const u8 = blk: { const args = env.b.args orelse break :blk null; if (args.len == 2 and std.mem.eql(u8, args[0], "--test-filter")) break :blk args[1]; if (args.len == 1 and std.mem.startsWith(u8, args[0], "--test-filter=")) break :blk args[0]["--test-filter=".len..]; break :blk null; }; const e2e_mod = env.b.createModule(.{ .root_source_file = env.b.path("src/e2e.zig"), .target = env.target, .optimize = env.optimize, .imports = &.{ .{ .name = "zit", .module = env.mod }, .{ .name = "build_options", .module = opts.createModule() }, }, }); const e2e_tests = env.b.addTest(.{ .root_module = e2e_mod, .filters = if (filter) |value| &.{value} else &.{} }); // Compile-only gate: `test` builds the e2e binary but never runs it. gate_step.dependOn(&e2e_tests.step); const e2e_run = env.b.addRunArtifact(e2e_tests); const e2e_step = env.b.step("e2e", "End-to-end tests: spawn the real zit binary"); e2e_step.dependOn(&e2e_run.step); } /// `zig build git-import-e2e -Dgit-import-e2e-filter=<fixture>` — real-Git /// differential tests for `import-git`. Filtering is compiled into the Zig /// test artifact, rather than forwarded to its runtime argument parser. /// Like the general e2e binary its compile is proved by `gate` rather than by /// `test`, while process execution remains opt-in; the emitted CLI path is a /// LazyPath, so the test cannot accidentally use a stale binary. fn build_git_export_e2e(env: Env, exe: *std.Build.Step.Compile, gate_step: *std.Build.Step) void { const opts = env.b.addOptions(); opts.addOptionPath("zit_exe", exe.getEmittedBin()); const test_mod = env.b.createModule(.{ .root_source_file = env.b.path("src/git/git_export_e2e.zig"), .target = env.target, .optimize = env.optimize, .imports = &.{ .{ .name = "zit", .module = env.mod }, .{ .name = "build_options", .module = opts.createModule() }, }, }); const tests = env.b.addTest(.{ .root_module = test_mod }); gate_step.dependOn(&tests.step); const run = env.b.addRunArtifact(tests); const step = env.b.step("git-export-e2e", "Run real-Git export differential tests"); step.dependOn(&run.step); } fn build_git_import_e2e(env: Env, exe: *std.Build.Step.Compile, gate_step: *std.Build.Step) void { const opts = env.b.addOptions(); opts.addOptionPath("zit_exe", exe.getEmittedBin()); const filter = env.b.option([]const u8, "git-import-e2e-filter", "Compile only matching real-Git import E2E tests"); const test_mod = env.b.createModule(.{ .root_source_file = env.b.path("src/git/git_import_e2e.zig"), .target = env.target, .optimize = env.optimize, .imports = &.{ .{ .name = "zit", .module = env.mod }, .{ .name = "build_options", .module = opts.createModule() }, }, }); const tests = env.b.addTest(.{ .root_module = test_mod, .filters = if (filter) |value| &.{value} else &.{}, }); gate_step.dependOn(&tests.step); const run = env.b.addRunArtifact(tests); const step = env.b.step("git-import-e2e", "Run real-Git import differential tests"); step.dependOn(&run.step); } /// `zig build fuzz -- <target> [--seed N] [--rounds N]` — seeded deterministic /// fuzz targets, driven in a loop by an out-of-band runner; not part of `test`. fn build_fuzz(env: Env) void { const fuzz_exe = env.b.addExecutable(.{ .name = "fuzz", .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/fuzz_tests.zig"), .target = env.target, .optimize = env.optimize, .imports = &.{ .{ .name = "zit", .module = env.mod }, }, }), }); const fuzz_step = env.b.step("fuzz", "Run a seeded fuzz target (zig build fuzz -- <target> --seed N)"); const fuzz_run = env.b.addRunArtifact(fuzz_exe); if (env.b.args) |args| fuzz_run.addArgs(args); fuzz_step.dependOn(&fuzz_run.step); } /// `zig build dst [-- --seed N | --seeds K] [--steps M]` — deterministic /// simulation of the repostate derivation. No args runs the fixed smoke /// sweep (seeds 1..25 x 10_000 steps); deep sweeps pass `--seeds`. /// /// dst.zig's module reaches sim/*.zig (kept out of the public `zit` module); /// running it as a test binary is how those sibling harness files' inline unit /// tests join `zig build test`. fn build_dst(env: Env, test_step: *std.Build.Step) *std.Build.Step.Compile { const dst_exe = env.b.addExecutable(.{ .name = "dst", .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/dst.zig"), .target = env.target, .optimize = env.optimize, .imports = &.{ .{ .name = "zit", .module = env.mod }, }, }), }); const dst_step = env.b.step("dst", "Deterministic simulation (default smoke sweep; zig build dst -- --seed N)"); const dst_run = env.b.addRunArtifact(dst_exe); if (env.b.args) |args| dst_run.addArgs(args); dst_step.dependOn(&dst_run.step); const dst_tests = env.b.addTest(.{ .root_module = dst_exe.root_module }); test_step.dependOn(&env.b.addRunArtifact(dst_tests).step); return dst_tests; } /// `zig build bench -- <put|read|open|sync|submanifest> [flags]` — LogStore /// throughput, sync timing, and reproducible clone-shape measurement. Pinned to /// ReleaseFast: the mode that ships, so a measurement describes the real binary. /// A ReleaseSafe number would describe one nobody runs. The margin is modest -- /// ~12% on `pack`, nothing measurable on the read path -- but a benchmark's job /// is to describe the shipped binary, not to be worth the difference. /// /// HOW TO MEASURE THIS PROJECT WITHOUT BEING LIED TO. Every rule here was paid /// for by a wrong answer that looked convincing, and they live beside the build /// because each is only true of THIS build script -- a wiki page would rot the /// day one of these lines changes, silently, while nothing failed. /// /// * BARE `zig build` IS DEBUG, and Debug is ~150x slower than an optimized /// build on history-walking work (`zit log tip` over tcl: 58.5 s Debug /// against 0.33 s optimized, same store, byte-identical output). An issue /// was once filed against this repository proposing an architectural change /// on the strength of a 58 s figure that was simply a Debug number. Build /// with `--release` before believing any timing. /// * TRUST THE BINARY, NOT THE FLAG. `preferred_optimize_mode` above makes /// the preference the ONLY reachable release mode: `--release`, /// `--release=safe` and `--release=fast` all resolve to it and produce one /// byte-identical binary. The `~25x pack` figure this file used to carry /// came from a `--release=fast` that was quietly building ReleaseSafe. /// Check which mode you actually got by looking for safety-check panic /// strings in the binary ("integer overflow", "index out of bounds"): /// present means ReleaseSafe, absent means Fast or Small. /// * REPORT THE MINIMUM OF N, NEVER THE MEAN. Timing noise is one-sided -- a /// run can be delayed by another process but never finish faster than the /// machine allows -- so the minimum estimates the true cost and the mean /// mostly measures how busy the box was. A single run once showed a 20% /// REGRESSION for a change that was a 3.7x improvement. /// * INTERLEAVE THE THINGS BEING COMPARED inside each cell, and warm the /// store first. Run in blocks and whichever went first pays the cold read; /// that alone reversed a fast-vs-safe verdict here. /// * DIFF THE OUTPUT EVERY TIME. An optimization that "won" by 50x turned out /// to be erroring out on the first artifact and printing nothing. Compare /// against the previous binary's output before believing a speedup. /// The executable reaches the store layers through the `zit` module; its pure /// corpus tests join `zig build test` in Debug (no timing loop ever runs under /// `test`). /// /// The `serve` subcommand spawns the REAL `zit` binary (the #13 listen-before- /// prime probe), so the exe's path is threaded in as a build option — a /// `LazyPath`, so the binary is always rebuilt before the bench runs — exactly /// as e2e/smoke do. The option module is shared by the bench exe and its test /// compile so `serve_benchmark.zig` resolves `build_options.zit_exe` in both. fn build_bench(env: Env, _: *std.Build.Step.Compile, test_step: *std.Build.Step) void { const bench_zit = env.b.addExecutable(.{ .name = "zit-bench", .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/main.zig"), .target = env.target, .optimize = .ReleaseFast, .imports = &.{.{ .name = "zit", .module = env.mod }}, }), }); const opts = env.b.addOptions(); opts.addOptionPath("zit_exe", bench_zit.getEmittedBin()); const bench_imports = [_]std.Build.Module.Import{ .{ .name = "zit", .module = env.mod }, .{ .name = "build_options", .module = opts.createModule() }, }; const bench_exe = env.b.addExecutable(.{ .name = "bench", .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/logstore_benchmark.zig"), .target = env.target, .optimize = .ReleaseFast, .imports = &bench_imports, }), }); const bench_step = env.b.step("bench", "LogStore + clone benchmarks (zig build bench -- <put|read|open|sync|submanifest> [flags])"); const bench_run = env.b.addRunArtifact(bench_exe); if (env.b.args) |args| bench_run.addArgs(args); bench_step.dependOn(&bench_run.step); const bench_tests = env.b.addTest(.{ .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/logstore_benchmark.zig"), .target = env.target, .optimize = env.optimize, .imports = &bench_imports, }) }); test_step.dependOn(&env.b.addRunArtifact(bench_tests).step); } /// `zig build import-git-bench -- [flags]` — bounded native fast-export /// translation controls. Deliberately opt-in: its full-corpus mode is a /// qualification workload, not an ordinary unit-test dependency. fn build_git_import_bench(env: Env, test_step: *std.Build.Step) void { const opts = env.b.addOptions(); opts.addOption([]const u8, "revision", env.b.option([]const u8, "revision", "Source revision recorded by import-git-bench") orelse "unknown"); const exe = env.b.addExecutable(.{ .name = "import-git-bench", .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/git/git_import_benchmark.zig"), .target = env.target, .optimize = .ReleaseFast, .imports = &.{ .{ .name = "build_options", .module = opts.createModule() }, .{ .name = "zit", .module = env.mod }, }, }), }); const step = env.b.step("import-git-bench", "Bounded native Git import scaling benchmark"); const run = env.b.addRunArtifact(exe); if (env.b.args) |args| run.addArgs(args); step.dependOn(&run.step); const tests = env.b.addTest(.{ .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/git/git_import_benchmark.zig"), .target = env.target, .optimize = env.optimize, .imports = &.{.{ .name = "zit", .module = env.mod }}, }) }); test_step.dependOn(&env.b.addRunArtifact(tests).step); } /// `zig build smoke` — the one sanctioned use of the real `fossil` binary: /// confirms zit-produced artifacts/deltas against fossil's own /// parser/hasher/store. Confined to this step; never part of `test` or /// pre-commit. `zig build test` still compiles smoke.zig (catching type /// errors) without running its fossil-driving scenarios. fn build_smoke(env: Env, exe: *std.Build.Step.Compile, test_step: *std.Build.Step, gate_step: *std.Build.Step) void { // Thread the real `zit` binary's path in as a build option (a `LazyPath`, so // it is always rebuilt first), same as e2e: S5 spawns `zit serve`/`pack` as // subprocesses and clones from them with the real `fossil` client. const opts = env.b.addOptions(); opts.addOptionPath("zit_exe", exe.getEmittedBin()); const smoke_exe = env.b.addExecutable(.{ .name = "smoke", .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/smoke.zig"), .target = env.target, .optimize = env.optimize, .imports = &.{ .{ .name = "zit", .module = env.mod }, .{ .name = "build_options", .module = opts.createModule() }, }, }), }); gate_step.dependOn(&smoke_exe.step); // The scenarios need a real `fossil` and stay out of `test`, but smoke.zig's // pure REPORT ORACLES (the helpers that decide pass/fail from fossil's stdout) // are ordinary string code and are unit-tested here: an oracle that cannot // fail is worse than no oracle, and only a test says which it is. Runs no // subprocess — only the `test` blocks in this file execute. const smoke_tests = env.b.addTest(.{ .root_module = env.b.createModule(.{ .root_source_file = env.b.path("src/smoke.zig"), .target = env.target, .optimize = env.optimize, .imports = &.{ .{ .name = "zit", .module = env.mod }, .{ .name = "build_options", .module = opts.createModule() }, }, }) }); test_step.dependOn(&env.b.addRunArtifact(smoke_tests).step); const smoke_step = env.b.step("smoke", "Fossil interop smoke check (milestone-manual)"); const smoke_run = env.b.addRunArtifact(smoke_exe); if (env.b.args) |args| smoke_run.addArgs(args); smoke_step.dependOn(&smoke_run.step); } /// Reads kcov's merged `coverage.json` and prints a `src/`-only per-file line /// summary, ascending by coverage so the least-tested regions surface first. const cov_summary_py = \\import json,sys \\d=json.load(open(sys.argv[1])) \\for f in sorted(d["files"],key=lambda f:float(f["percent_covered"])): \\ print(f"{float(f['percent_covered']):6.2f}% {f['covered_lines']:>5}/{f['total_lines']:<5} {f['file'].split('/zit/')[-1]}") \\print(f"total {d['percent_covered']}% html: zig-out/cov/kcov-merged/index.html") ; /// `zig build cov` — merged line coverage over every unit-test binary via /// kcov (`brew install kcov`). Out-of-band: never part of `test`/pre-commit, /// so untested regions surface mechanically without gating a commit. fn build_cov(env: Env, test_bins: []const *std.Build.Step.Compile) void { const b = env.b; const merge = b.addSystemCommand(&.{ "kcov", "--merge" }); const merged = merge.addOutputDirectoryArg("cov"); for (test_bins) |t| { const run = b.addSystemCommand(&.{"kcov"}); run.addArg(b.fmt("--include-pattern={s}/", .{b.pathFromRoot("src")})); merge.addDirectoryArg(run.addOutputDirectoryArg("kcov")); run.addArtifactArg(t); } const install = b.addInstallDirectory(.{ .source_dir = merged, .install_dir = .prefix, .install_subdir = "cov", }); const summary = b.addSystemCommand(&.{ "python3", "-c", cov_summary_py }); summary.addFileArg(merged.path(b, "kcov-merged/coverage.json")); summary.step.dependOn(&install.step); b.step("cov", "Merged kcov line coverage (out-of-band)").dependOn(&summary.step); }