kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

Build coordinator — testing strategy

Historical testing notes. Companion to ../BUILD_COORDINATOR.md (the feature spec) and BUILD_INTERNALS.md (the module breakdown). This doc specifies how the subsystem is tested — the shared harness, the per-module plan, the integration scenarios, and the cross-cutting properties (byte-stability, fail-safe parsing, minimal-rebuild, crash-safety, concurrency, determinism). It mirrors how the layer below is tested: the dist subsystem has no test/dist; it is covered by test/cas + test/pkg (per public surface) plus INTERNAL unit tests. kit/build_coord.h gets test/build.

Why the subsystem is testable by construction

Two properties — both deliberate — carry the plan:

  1. L0–L1 are pure (no I/O): build, cfg, defn, trace, protocol, client are byte/value transforms. They get straight INTERNAL unit tests and golden corpus, exactly like the ISA/DWARF units.
  2. Every side effect is a host vtable: KitBuildStoreIo, KitBuildExec, KitBuildTransport, KitBuildSched, and KitCasHost. So every L2+ test injects a fake and stays hermetic and deterministic — no real sockets, processes, clock, or network, and no flakiness.

Guiding principle: test what ships. The integration harness drives the real protocol codec, service loop, runner, resolver, and coordinator; only the OS socket and process spawn are stubbed (see Integration).

Repo conventions this plugs into

Shared harness — test/build/lib/

Written once, reused everywhere. The harness is the test investment.

Fake Backs Capability
FakeStoreIo KitBuildStoreIo rename/remove/temp/sync/lock over a real temp dir, plus fault injection: fail or half-apply the Nth op, to drive crash-safety
FakeExec KitBuildExec runs scripted recipes (target → {sources, needs, config-gets, env reads, output files, exit code}) and counts invocations per target; a corrupt-bytes mode backs remote-fetch
FakeWorkspace KitCasHost.file_io an in-memory file set a test mutates between builds (edit / delete / create) to drive invalidation; the source of source/glob bytes
ipc_transport KitBuildTransport in-memory framed transport over cond-var queues; the recipe callback runs on a KitBuildSched thread (the real protocol path)
thread_sched KitBuildSched real pthread-backed threads/mutex/cond for integration + concurrency; a NULL sched exercises sequential degrade
count_source (a FakeWorkspace view) counts rehashes/reglobs so refresh tests assert a shared subtree is checked once

Every integration test wraps its kit_build in a watchdog timeout (a deadlock-bug must fail, not wedge CI) — the one cost of the framed-over-thread harness (decision below).

Per-module plan

Layer Module Regime Faked Key cases
L0 build INTERNAL build_target_key / build_glob_result_hash id-stability vectors; build_id_eq
L1 cfg INTERNAL set/overlay/get; canonical sort+dedup; empty-argv fixed id; emit→parse→emit identity; bad-body rejection
L1 trace INTERNAL shallow/deep/deepset/record emit↔parse; absent leaf (<path> -); record MRU dedup+cap; byte-stable golden + known id
L1 defn INTERNAL parse + find; sorted-unique enforcement; no whole-file dep (find(T) stable under unrelated edits)
L1 protocol INTERNAL req/resp encode↔decode; need overlay+argv; multi-frame glob + GLOB_END; frame-bound + malformed rejection
L1 client PUBLIC loopback each kit_build_client_* round-trips against a scripted server
L2 store INTERNAL FakeStoreIo + real CAS put/get idempotence; record RMW dedup+cap; cache materialize verifies; deepset stored as a CAS blob; crash-safety (below)
L2 remote INTERNAL FakeExec verified install; corrupt mirror discarded; remotes tried in order; tree=manifest-then-blobs; deepset via {kind}=blob
L3 coord INTERNAL all memo (file hashed once); futures dedup; sequential degrade (sched==NULL); deepset_load interns+recurses, absent→ERR; pull_once idempotent; stat counters
L4 resolve INTERNAL/PUBLIC ipc + FakeExec the scenario spine + soundness scenarios
L5 runner INTERNAL ipc + FakeExec service loop logs every dep; nonzero exit → no trace; both traces + deepset written; jobs slot released across need (chain deeper than jobs completes)
L5 bundle PUBLIC real pkg export→import round-trip; untrusted signer rejected; tofu pin; deepset + overlay/argv blobs present so an imported trace refreshes/replays
L6 api + driver PUBLIC + run.sh ipc / real kit build end-to-end; kit_build_stats deltas

Integration

The scenario spine

The worked example is the backbone. Each step asserts (output tree-id, KitBuildStats delta) — the stats diff names the path taken:

  1. coldrecipes_run += 2;
  2. no-op rebuilddeep_hits += 1, recipes_run += 0;
  3. comment-edit a dep source → dep recipes_run += 1, parent served by shallow_hits (Phase-2 output-compare), parent recipe not run;
  4. flip a consumed config key → only the consuming recipe runs.

Soundness scenarios (the regression suite for the design review)

These are the tests that would have caught the bugs found while pressing on the design — each maps to a finding:

Tiers

Cross-cutting properties

Observability — KitBuildStats

Six cumulative counters on the coordinator (deep_hits, shallow_hits, recipes_run, materialize_misses, object_fetches, trace_pulls), bumped under the coordinator lock and read via kit_build_stats. Tests snapshot before / diff after each kit_build to assert which path served the request and how much work ran — turning "did the recipe run?" and "deep or shallow hit?" into exact assertions. The same surface is user-facing build introspection.

Decisions (this round)

Map to the implementation waves

Tests land with their modules (Wave order):