kit

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

commit f9bfe3fe931e96825fa793075b9ee35a47429514
parent 5706656aa786c39f181907b28e7a9a8d16cff7e0
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 18 Jun 2026 08:21:11 -0700

Add build coordinator test caching

Diffstat:
Mdoc/BUILD_COORDINATOR.md | 109++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mdriver/cmd/build_coord.c | 50++++++++++++++++++++++++++++++++++++++------------
Mdriver/env/build_host_posix.c | 46++++++++++++++++++++++++++++++++++++++++++----
Minclude/kit/build_coord.h | 33+++++++++++++++++++++++++++++++++
Mmk/test.mk | 2+-
Msrc/api/build_coord.c | 33+++++++++++++++++++++++++++++++++
Msrc/api/config_stubs.c | 11+++++++++++
Msrc/build/build.c | 22+++++++++++++++++-----
Msrc/build/build.h | 6++++++
Msrc/build/bundle.c | 263++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/build/coord.c | 3+++
Msrc/build/coord.h | 18++++++++++++++++++
Msrc/build/resolve.c | 262++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msrc/build/resolve.h | 12+++++++++++-
Msrc/build/runner.c | 286+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/build/runner.h | 6++++++
Msrc/build/trace.c | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/build/trace.h | 9+++++++++
Mtest/build/build_public_link_test.c | 13++++++++++++-
Mtest/build/build_pure_test.c | 72+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mtest/buildcoord/run.sh | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
21 files changed, 1472 insertions(+), 59 deletions(-)

diff --git a/doc/BUILD_COORDINATOR.md b/doc/BUILD_COORDINATOR.md @@ -51,6 +51,9 @@ the resolution algorithm that drives it. | **Recipe** | The executable the coordinator runs to produce a target's output. Identified by `recipe-id` = `BLAKE2b(recipe file bytes)`. | | **Configuration** | Tunables a recipe reads (target triple, opt level, feature flags). Two scopes — *propagated* (a key→value map, `config-id`) and *local* (the argv vector, `argv-id`) — see [below](#configuration-model). Both content-addressed: `config-id`/`argv-id` = `BLAKE2b(canonical bytes)`. | | **Output tree** | The directory a recipe produces, captured as a CAS tree (`tree-id`). The unit a build request returns. | +| **Test target request** | A request to execute a target as a test (`kit build test ...`). It uses the same recipe protocol and dependency discovery as a build request, but returns a typed test result. | +| **Test result tree** | The directory a test recipe produces, plus coordinator-injected `stdout` and `stderr` files, captured as a CAS tree (`result-tree-id`). | +| **PASS-only test trace** | A test trace is a cacheable claim only when the recipe exits 0. Nonzero exits return FAIL results for inspection but write no reusable trace. | | **Base inputs** | The *leaves* of the dependency graph, dynamically requested by recipes: **config values**, **source files**, and **globs**. | | **Target dep** | A dependency of one target on the *output tree* of another. The graph's interior edges, created by `need`. | | **Shallow trace** | A record of one build: its direct base inputs + its direct target deps (by output `tree-id`) → its output `tree-id`. | @@ -184,7 +187,10 @@ convention). `trace-id` = `BLAKE2b` of the canonical trace body. `target-key` = `BLAKE2b("kit build target v1" ‖ target-name)` — note it depends on the target *name only*, never on config, argv, or recipe-id, so a target's index entry is a stable handle holding candidate traces across every -`(config, argv)` it was recently built under, while its inputs churn. +`(config, argv)` it was recently built under, while its inputs churn. Test +records use a separate domain, `BLAKE2b("kit build test target v1" ‖ target-name)`, +so build output traces and test result traces cannot collide or satisfy each +other. The line that sorts `cas/` from `build/` is **self-verifiability, not content-addressedness**. Config maps, argv vectors, and **deepset closure nodes** @@ -243,6 +249,40 @@ argv <argv-id> ; local config (serialized vector blob) deepset <deep-set-id> ; the transitive input closure, as a deepset DAG (a CAS blob) ``` +**Test traces** use the same sections and matching inputs, but distinct magics +and a `result` scalar instead of `output`: + +``` +kit-build-test-shallow 1 +target //pkg:unit +recipe <recipe-id> +result <result-tree-id> +config <config-id> +argv <argv-id> +[config] +... +[source] +... +[glob] +... +[dep] +... +``` + +``` +kit-build-test-deep 1 +target //pkg:unit +recipe <recipe-id> +result <result-tree-id> +root-config <config-id> +argv <argv-id> +deepset <deep-set-id> +``` + +Test deepsets reuse `kit-build-deepset 1`, because the input-closure semantics +are unchanged. Test target records use the same `kit-build-record 1` body shape +but are stored under the test-record key domain described above. + **Deepset** — one node of the transitive input-closure DAG, a CAS blob. The deep trace inlines none of the closure; it points at the root deepset, and each node points at its children. A subtree reached through many parents is *one* node @@ -503,6 +543,41 @@ whose recipe maps it to an unchanged output (`tree-id` identical), the dep compa passes and `T`'s recipe is skipped. The deep trace is the cheap "nothing moved" check; the shallow trace is the "did the churn actually reach me?" check. +### Test caching + +`kit build test` is first-class test execution in the coordinator, not a separate +task runner. A test target uses the same build definition, recipe protocol, +dependency discovery, config/argv identity, deepset refresh, materialization, +remotes, and verify mode as `kit build`; the difference is the result type and +record namespace. + +Test execution policy: + +- The recipe writes arbitrary inspection artifacts under `$KIT_BUILD_OUT`, as a + build recipe does. The coordinator also captures the recipe's stdout and stderr + and injects them into the result directory as `stdout` and `stderr` before + ingesting the directory into the CAS. +- Exit code 0 returns `KIT_TEST_PASS`. The resulting `result-tree-id` is a + reusable cache claim, so the coordinator writes test shallow/deep traces and + prepends them to the test target record. +- Any nonzero exit returns `KIT_TEST_FAIL`. The result directory, stdout, and + stderr are still ingested and materialized for inspection, but no test trace and + no test-record entry are written. Re-running the same failing test runs the + recipe again. +- Protocol, host, CAS, or malformed-trace failures are infrastructure errors and + return `KitStatus` errors rather than typed test results. + +Deep and shallow matching are identical to build matching, except that only test +traces are considered and hits materialize a **result tree**. A shallow test hit +may skip the test recipe after a dependency's inputs changed if every recorded dep +still resolves to the same output tree. Verify mode audits cached PASS claims by +re-running the test and comparing the fresh result tree with the cached +`result-tree-id`; a FAIL or different result tree reports a verify mismatch. + +Task-runner policy (test discovery, selection expressions, sharding, retries, +report aggregation) is deliberately deferred. The v1 surface is one target +request at a time: `kit build test ... TARGET [-- ARG...]`. + ### Running a recipe ``` @@ -648,6 +723,29 @@ command. - **Outputs** need no command: the recipe writes under `$KIT_BUILD_OUT` and the coordinator snapshots that directory on exit. +## CLI + +Build execution: + +``` +kit build [--store DIR] [--root DIR] [--def FILE] + [--config K=V]... [--verify] [--stats] TARGET [-- ARG...] +``` + +Test execution: + +``` +kit build test [--store DIR] [--root DIR] [--def FILE] + [--config K=V]... [--verify] [--stats] TARGET [-- ARG...] +``` + +`kit build` prints `<output-tree-hex> <path>` and exits 0 on success. `kit build +test` prints `<status> <result-tree-hex> <path>`, where status is `PASS` or +`FAIL`; it exits 0 for PASS, 1 for FAIL, and 2 for bad CLI usage. Infrastructure +errors also exit nonzero and print diagnostics on stderr. `--stats` prints +cumulative coordinator counters, including `test_runs`, `test_cache_hits`, and +`test_failures`, to stderr. + ## Remote CAS and shared traces Two independent network capabilities, split along the DISTRIBUTE.md line — @@ -692,6 +790,10 @@ machinery wholesale: - Import: verify signature → anchor key → install the trace bodies into `build/trace/` and prepend them to the relevant target records. Output bytes come from the bundle or from the remote CAS — either way **hash-verified** on use. +- Signed test traces are trusted PASS claims. Importing one means the signer + asserts that the named test target, under the recorded inputs, exits 0 and + produces the recorded result tree. FAIL results are never shared as reusable + traces. The result is a precise security split: a local build can now deep/shallow-**hit** on a remote builder's trace — obtaining the output *without running the recipe* — @@ -723,6 +825,11 @@ deterministically. would serve a stale-but-believed-current result. Mitigations: declare such inputs (a "now" config value, a pinned URL+hash), or mark a target **no-cache** so it always runs Phase 3. +- **Tests are deterministic programs too.** Anything observable to a test runner + that can affect PASS/FAIL, stdout/stderr, or result artifacts must be declared + as config or argv: platform, runner version, VM image, libc, target arch, + locale, environment, and feature flags all belong in the recorded inputs when + they are observable. - **Verify mode.** A diagnostic mode re-runs a recipe whose trace says "unchanged" and compares the fresh `tree-id` to the recorded one; a mismatch flags a nondeterministic or under-declared recipe — the recommended audit before diff --git a/driver/cmd/build_coord.c b/driver/cmd/build_coord.c @@ -28,6 +28,7 @@ typedef struct BuildCli { size_t nargs; int verify; int stats; + int test_mode; } BuildCli; void driver_help_build(void) { @@ -37,6 +38,8 @@ void driver_help_build(void) { "USAGE\n" " kit build [--store DIR] [--root DIR] [--def FILE]\n" " [--config K=V]... [--verify] [--stats] TARGET [-- ARG...]\n" + " kit build test [--store DIR] [--root DIR] [--def FILE]\n" + " [--config K=V]... [--verify] [--stats] TARGET [-- ARG...]\n" " kit build config-get KEY # inside a build recipe\n" " kit build source PATH # inside a build recipe\n" " kit build depfile [--lines] FILE # inside a build recipe\n" @@ -345,12 +348,14 @@ out_env: return rc; } -static int build_parse_args(BuildCli* cli, int argc, char** argv) { +static int build_parse_args(BuildCli* cli, int argc, char** argv, int first_arg, + int test_mode) { int i; memset(cli, 0, sizeof *cli); cli->root = "."; cli->def = BUILD_DEFAULT_DEF; - for (i = 1; i < argc; ++i) { + cli->test_mode = test_mode; + for (i = first_arg; i < argc; ++i) { const char* a = argv[i]; if (driver_streq(a, "--store") && i + 1 < argc) { cli->store = argv[++i]; @@ -407,13 +412,17 @@ static void build_print_stats(const KitBuildStats* s) { if (!s) return; driver_errf(BUILD_TOOL, "stats deep_hits=%llu shallow_hits=%llu recipes_run=%llu " - "materialize_misses=%llu object_fetches=%llu trace_pulls=%llu", + "materialize_misses=%llu object_fetches=%llu trace_pulls=%llu " + "test_runs=%llu test_cache_hits=%llu test_failures=%llu", (unsigned long long)s->deep_hits, (unsigned long long)s->shallow_hits, (unsigned long long)s->recipes_run, (unsigned long long)s->materialize_misses, (unsigned long long)s->object_fetches, - (unsigned long long)s->trace_pulls); + (unsigned long long)s->trace_pulls, + (unsigned long long)s->test_runs, + (unsigned long long)s->test_cache_hits, + (unsigned long long)s->test_failures); } static int build_verify_tree(DriverEnv* env, const char* store, @@ -446,6 +455,7 @@ int driver_build(int argc, char** argv) { KitBuildOptions opts; KitBuildRequest req; KitBuildResult result; + KitBuildTestResult test_result; KitBuildCoordinator* coord = NULL; KitStatus st; char* default_store = NULL; @@ -467,7 +477,9 @@ int driver_build(int argc, char** argv) { build_client_verb(argv[1])) return build_client_mode(argc, argv, 1); - rc = build_parse_args(&cli, argc, argv); + rc = build_parse_args(&cli, argc, argv, + argc >= 2 && driver_streq(argv[1], "test") ? 2 : 1, + argc >= 2 && driver_streq(argv[1], "test")); if (rc != 0) return rc; driver_env_init(&env); @@ -518,9 +530,13 @@ int driver_build(int argc, char** argv) { req.argv = cli.args; req.argc = cli.nargs; memset(&result, 0, sizeof result); - st = kit_build(coord, &req, &result); + memset(&test_result, 0, sizeof test_result); + st = cli.test_mode ? kit_build_test(coord, &req, &test_result) + : kit_build(coord, &req, &result); if (st != KIT_OK) { - driver_errf(BUILD_TOOL, "build failed: %s", build_status_name(st)); + driver_errf(BUILD_TOOL, "%s failed: %s", + cli.test_mode ? "build test" : "build", + build_status_name(st)); rc = 1; goto out; } @@ -530,15 +546,25 @@ int driver_build(int argc, char** argv) { build_print_stats(&stats); } - if (cli.verify && build_verify_tree(&env, abs_store, result.output_tree) != - 0) { + if (cli.verify && + build_verify_tree(&env, abs_store, + cli.test_mode ? test_result.result_tree + : result.output_tree) != 0) { rc = 1; goto out; } - kit_hex_encode(hex, result.output_tree, KIT_BUILD_HASH_LEN); - driver_printf("%s %s\n", hex, result.path); - rc = 0; + if (cli.test_mode) { + kit_hex_encode(hex, test_result.result_tree, KIT_BUILD_HASH_LEN); + driver_printf("%s %s %s\n", + test_result.status == KIT_TEST_PASS ? "PASS" : "FAIL", hex, + test_result.path); + rc = test_result.status == KIT_TEST_PASS ? 0 : 1; + } else { + kit_hex_encode(hex, result.output_tree, KIT_BUILD_HASH_LEN); + driver_printf("%s %s\n", hex, result.path); + rc = 0; + } out: if (coord) kit_build_coordinator_close(coord); diff --git a/driver/env/build_host_posix.c b/driver/env/build_host_posix.c @@ -171,13 +171,17 @@ static void db_free_strv(DriverEnv* env, char** v, size_t n) { driver_free(env, v, (n + 1u) * sizeof *v); } -static int db_exec_spawn(void* user, const KitSlice* argv, size_t argc, - const KitBuildKV* envv, size_t nenv, KitSlice cwd, - KitBuildProc** out) { +static int db_exec_spawn_common(void* user, const KitSlice* argv, size_t argc, + const KitBuildKV* envv, size_t nenv, + KitSlice cwd, KitSlice stdout_path, + KitSlice stderr_path, int capture, + KitBuildProc** out) { DriverBuildHost* bh = (DriverBuildHost*)user; char** av = NULL; char** ev = NULL; char* cwd_s = NULL; + char* stdout_s = NULL; + char* stderr_s = NULL; KitBuildProc* proc = NULL; pid_t pid; size_t i; @@ -203,11 +207,25 @@ static int db_exec_spawn(void* user, const KitSlice* argv, size_t argc, ev[i][nk + 1u + nv] = '\0'; } cwd_s = db_slice_dup(bh->env, cwd); + if (capture) { + stdout_s = db_slice_dup(bh->env, stdout_path); + stderr_s = db_slice_dup(bh->env, stderr_path); + } proc = (KitBuildProc*)driver_alloc_zeroed(bh->env, sizeof *proc); - if (!cwd_s || !proc) goto err; + if (!cwd_s || !proc || (capture && (!stdout_s || !stderr_s))) goto err; pid = fork(); if (pid < 0) goto err; if (pid == 0) { + if (capture) { + int out_fd = open(stdout_s, O_WRONLY | O_CREAT | O_TRUNC, 0666); + int err_fd = open(stderr_s, O_WRONLY | O_CREAT | O_TRUNC, 0666); + if (out_fd < 0 || err_fd < 0) _exit(127); + if (dup2(out_fd, STDOUT_FILENO) < 0 || + dup2(err_fd, STDERR_FILENO) < 0) + _exit(127); + close(out_fd); + close(err_fd); + } if (chdir(cwd_s) != 0) _exit(127); execve(av[0], av, ev); _exit(127); @@ -217,16 +235,35 @@ static int db_exec_spawn(void* user, const KitSlice* argv, size_t argc, db_free_strv(bh->env, av, argc); db_free_strv(bh->env, ev, nenv); driver_free(bh->env, cwd_s, strlen(cwd_s) + 1u); + if (stdout_s) driver_free(bh->env, stdout_s, strlen(stdout_s) + 1u); + if (stderr_s) driver_free(bh->env, stderr_s, strlen(stderr_s) + 1u); return 0; err: db_free_strv(bh->env, av, argc); db_free_strv(bh->env, ev, nenv); if (cwd_s) driver_free(bh->env, cwd_s, strlen(cwd_s) + 1u); + if (stdout_s) driver_free(bh->env, stdout_s, strlen(stdout_s) + 1u); + if (stderr_s) driver_free(bh->env, stderr_s, strlen(stderr_s) + 1u); if (proc) driver_free(bh->env, proc, sizeof *proc); return 1; } +static int db_exec_spawn(void* user, const KitSlice* argv, size_t argc, + const KitBuildKV* envv, size_t nenv, KitSlice cwd, + KitBuildProc** out) { + return db_exec_spawn_common(user, argv, argc, envv, nenv, cwd, + KIT_SLICE_NULL, KIT_SLICE_NULL, 0, out); +} + +static int db_exec_spawn_capture(void* user, const KitSlice* argv, size_t argc, + const KitBuildKV* envv, size_t nenv, + KitSlice cwd, KitSlice stdout_path, + KitSlice stderr_path, KitBuildProc** out) { + return db_exec_spawn_common(user, argv, argc, envv, nenv, cwd, stdout_path, + stderr_path, 1, out); +} + static int db_exec_wait(void* user, KitBuildProc* proc, int* exit_code) { DriverBuildHost* bh = (DriverBuildHost*)user; int st = 0; @@ -475,6 +512,7 @@ int driver_build_host_init(DriverBuildHost* out, DriverEnv* env) { out->store_io.list_dir = db_store_list_dir; out->store_io.user = out; out->exec.spawn = db_exec_spawn; + out->exec.spawn_capture = db_exec_spawn_capture; out->exec.wait = db_exec_wait; out->exec.poll = db_exec_poll; out->exec.kill = db_exec_kill; diff --git a/include/kit/build_coord.h b/include/kit/build_coord.h @@ -128,6 +128,13 @@ typedef struct KitBuildExec { int (*spawn)(void* user, const KitSlice* argv, size_t argc, const KitBuildKV* env, size_t nenv, KitSlice cwd, KitBuildProc** out); + /* Optional captured spawn, used by test execution. stdout/stderr are + * redirected to the supplied host paths. Hosts that cannot support capture + * leave this NULL; kit_build_test then returns KIT_UNSUPPORTED. */ + int (*spawn_capture)(void* user, const KitSlice* argv, size_t argc, + const KitBuildKV* env, size_t nenv, KitSlice cwd, + KitSlice stdout_path, KitSlice stderr_path, + KitBuildProc** out); /* Block until `proc` exits; *exit_code receives its status; frees `proc`. */ int (*wait)(void* user, KitBuildProc* proc, int* exit_code); /* Optional nonblocking status check for recipe processes. On exit, sets @@ -285,6 +292,20 @@ typedef struct KitBuildResult { char path[KIT_BUILD_PATH_MAX]; /* materialized output directory */ } KitBuildResult; +typedef enum KitTestStatus { + KIT_TEST_PASS = 0, + KIT_TEST_FAIL = 1, +} KitTestStatus; + +typedef struct KitBuildTestResult { + KitTestStatus status; + int exit_code; + uint8_t result_tree[KIT_BUILD_HASH_LEN]; + char path[KIT_BUILD_PATH_MAX]; /* materialized result directory */ + uint8_t stdout_blob[KIT_BUILD_HASH_LEN]; + uint8_t stderr_blob[KIT_BUILD_HASH_LEN]; +} KitBuildTestResult; + /* Resolve `req->target` under the supplied propagated config, returning its * output tree id and a materialized directory path. Runs the least work * necessary: a deep/shallow cache hit returns without re-running the recipe; a @@ -292,6 +313,15 @@ typedef struct KitBuildResult { KIT_API KitStatus kit_build(KitBuildCoordinator*, const KitBuildRequest* req, KitBuildResult* out); +/* Execute a target as a test. PASS results (exit code 0) are reusable cache + * claims using test-only target records and trace magics. FAIL results are + * ingested and returned for inspection, including stdout/stderr blobs, but are + * never recorded as reusable traces. Infrastructure/protocol failures return a + * KitStatus error instead of a test result. */ +KIT_API KitStatus kit_build_test(KitBuildCoordinator*, + const KitBuildRequest* req, + KitBuildTestResult* out); + /* Cumulative resolution counters since the coordinator was opened — build * introspection for users and the assertion surface for tests (which snapshot * before a kit_build and diff after to assert *which* path served a request and @@ -303,6 +333,9 @@ typedef struct KitBuildStats { uint64_t materialize_misses; /* a cache hit whose bytes were gone everywhere */ uint64_t object_fetches; /* objects pulled + verified from an object-remote */ uint64_t trace_pulls; /* signed trace bundles imported from a trace-remote */ + uint64_t test_runs; /* test recipe executions */ + uint64_t test_cache_hits; /* PASS test result served from cache */ + uint64_t test_failures; /* completed test recipe exits != 0 */ } KitBuildStats; KIT_API void kit_build_stats(const KitBuildCoordinator*, KitBuildStats* out); diff --git a/mk/test.mk b/mk/test.mk @@ -347,7 +347,7 @@ test-build-pure: $(BUILD_PURE_TEST_BIN) test-build-public-link: $(BUILD_PUBLIC_LINK_TEST_BIN) $(BUILD_PUBLIC_LINK_TEST_BIN) -test-build: test-build-pure test-build-public-link +test-build: test-build-pure test-build-public-link test-driver-build-coord test-rpn-lang: lib @KIT_BUILD_DIR=$(abspath $(BUILD_DIR)/test/rpn-lang) sh test/rpn-lang/run.sh diff --git a/src/api/build_coord.c b/src/api/build_coord.c @@ -47,6 +47,39 @@ KitStatus kit_build(KitBuildCoordinator* c, const KitBuildRequest* req, return KIT_OK; } +KitStatus kit_build_test(KitBuildCoordinator* c, const KitBuildRequest* req, + KitBuildTestResult* out) { + BuildConfigEntry cfg_entries[128]; + char argv_entries[128][BUILD_VAL_MAX]; + BuildConfig cfg; + BuildArgv argv; + BuildTestResolved r; + size_t i; + if (!c || !req || !out) return KIT_INVALID; + if (!c->host.exec || !c->host.exec->spawn_capture) return KIT_UNSUPPORTED; + build_config_init(&cfg, cfg_entries, + sizeof cfg_entries / sizeof cfg_entries[0]); + for (i = 0; i < req->nconfig; ++i) { + if (build_config_set(&cfg, req->config[i].key, req->config[i].value) != + BUILD_OK) + return KIT_INVALID; + } + build_argv_init(&argv, argv_entries, + sizeof argv_entries / sizeof argv_entries[0]); + if (build_argv_set(&argv, req->argv, req->argc) != BUILD_OK) + return KIT_INVALID; + memset(&r, 0, sizeof r); + if (build_test_resolve(c, req->target, &cfg, &argv, NULL, &r) != BUILD_OK) + return KIT_ERR; + out->status = r.status; + out->exit_code = r.exit_code; + memcpy(out->result_tree, r.result_tree, KIT_BUILD_HASH_LEN); + memcpy(out->path, r.path, sizeof out->path); + memcpy(out->stdout_blob, r.stdout_blob, KIT_BUILD_HASH_LEN); + memcpy(out->stderr_blob, r.stderr_blob, KIT_BUILD_HASH_LEN); + return KIT_OK; +} + void kit_build_stats(const KitBuildCoordinator* c, KitBuildStats* out) { if (!out) return; memset(out, 0, sizeof *out); diff --git a/src/api/config_stubs.c b/src/api/config_stubs.c @@ -39,6 +39,14 @@ KitStatus kit_build(KitBuildCoordinator* c, const KitBuildRequest* req, return KIT_UNSUPPORTED; } +KitStatus kit_build_test(KitBuildCoordinator* c, const KitBuildRequest* req, + KitBuildTestResult* out) { + (void)c; + (void)req; + (void)out; + return KIT_UNSUPPORTED; +} + void kit_build_stats(const KitBuildCoordinator* c, KitBuildStats* out) { (void)c; if (out) { @@ -48,6 +56,9 @@ void kit_build_stats(const KitBuildCoordinator* c, KitBuildStats* out) { out->materialize_misses = 0; out->object_fetches = 0; out->trace_pulls = 0; + out->test_runs = 0; + out->test_cache_hits = 0; + out->test_failures = 0; } } diff --git a/src/build/build.c b/src/build/build.c @@ -37,21 +37,33 @@ static int build_path_blob_cmp(const BuildPathBlob* a, const BuildPathBlob* b) { return strcmp(a->path, b->path); } -int build_target_key(KitSlice target_name, uint8_t out[BUILD_HASH_LEN]) { - uint8_t buf[sizeof(BUILD_TARGET_KEY_DOMAIN) - 1u + BUILD_TARGET_MAX - 1u]; - size_t domain_len = sizeof(BUILD_TARGET_KEY_DOMAIN) - 1u; +static int target_key_with_domain(KitSlice target_name, const char* domain, + size_t domain_len, + uint8_t out[BUILD_HASH_LEN]) { + uint8_t buf[sizeof(BUILD_TEST_TARGET_KEY_DOMAIN) - 1u + BUILD_TARGET_MAX - 1u]; KitBlobInfo info; - if (!out) return BUILD_ERR; + if (!out || !domain) return BUILD_ERR; if (!build_slice_token_valid(target_name, BUILD_TARGET_MAX)) return BUILD_ERR; - memcpy(buf, BUILD_TARGET_KEY_DOMAIN, domain_len); + memcpy(buf, domain, domain_len); memcpy(buf + domain_len, target_name.data, target_name.len); kit_blob_info(&info, buf, domain_len + target_name.len); memcpy(out, info.id, BUILD_HASH_LEN); return BUILD_OK; } +int build_target_key(KitSlice target_name, uint8_t out[BUILD_HASH_LEN]) { + return target_key_with_domain(target_name, BUILD_TARGET_KEY_DOMAIN, + sizeof(BUILD_TARGET_KEY_DOMAIN) - 1u, out); +} + +int build_test_target_key(KitSlice target_name, uint8_t out[BUILD_HASH_LEN]) { + return target_key_with_domain(target_name, BUILD_TEST_TARGET_KEY_DOMAIN, + sizeof(BUILD_TEST_TARGET_KEY_DOMAIN) - 1u, + out); +} + int build_glob_result_hash(KitHeap* heap, const BuildPathBlob* entries, size_t n, uint8_t out[BUILD_HASH_LEN]) { KitWriter* w = NULL; diff --git a/src/build/build.h b/src/build/build.h @@ -37,6 +37,8 @@ * family: a version line, ordered scalar fields, then sorted sections. */ #define BUILD_SHALLOW_MAGIC "kit-build-shallow 1" #define BUILD_DEEP_MAGIC "kit-build-deep 1" +#define BUILD_TEST_SHALLOW_MAGIC "kit-build-test-shallow 1" +#define BUILD_TEST_DEEP_MAGIC "kit-build-test-deep 1" #define BUILD_DEEPSET_MAGIC \ "kit-build-deepset 1" /* one node of the transitive input-closure DAG */ #define BUILD_RECORD_MAGIC "kit-build-record 1" @@ -60,6 +62,7 @@ * traces across every (config, argv) it was recently built under, while its * inputs churn. */ #define BUILD_TARGET_KEY_DOMAIN "kit build target v1" +#define BUILD_TEST_TARGET_KEY_DOMAIN "kit build test target v1" /* Fixed string capacities. A target name and config key/value are bounded; a * workspace/source path is bounded by BUILD_PATH_MAX. These cap a single @@ -96,6 +99,9 @@ static inline int build_id_eq(const uint8_t a[BUILD_HASH_LEN], /* Compute target-key = BLAKE2b(BUILD_TARGET_KEY_DOMAIN || target-name). */ int build_target_key(KitSlice target_name, uint8_t out[BUILD_HASH_LEN]); +/* Compute test-target-key = BLAKE2b(BUILD_TEST_TARGET_KEY_DOMAIN || target-name). + * Test and build records are intentionally disjoint for the same target. */ +int build_test_target_key(KitSlice target_name, uint8_t out[BUILD_HASH_LEN]); /* One (path, blob-id) pair in a glob's canonical match listing. Structurally a * source leaf, but defined at the foundation so the glob-result hash stays a diff --git a/src/build/bundle.c b/src/build/bundle.c @@ -6,6 +6,8 @@ #define BUILD_BUNDLE_MANIFEST_PATH "manifest" #define BUILD_BUNDLE_TRACE_DIR "trace" #define BUILD_BUNDLE_BLOB_DIR "blob" +#define BUILD_BUNDLE_TEST_DEEP 2u +#define BUILD_BUNDLE_TEST_SHALLOW 3u #if defined(__GNUC__) || defined(__clang__) #define BUILD_MAYBE_UNUSED __attribute__((unused)) @@ -265,12 +267,16 @@ static void BUILD_MAYBE_UNUSED id_vec_free(const KitContext* ctx, static int kind_valid(uint8_t kind) { return kind == (uint8_t)BUILD_TRACE_DEEP || - kind == (uint8_t)BUILD_TRACE_SHALLOW; + kind == (uint8_t)BUILD_TRACE_SHALLOW || + kind == (uint8_t)BUILD_BUNDLE_TEST_DEEP || + kind == (uint8_t)BUILD_BUNDLE_TEST_SHALLOW; } static const char* kind_name(uint8_t kind) { if (kind == (uint8_t)BUILD_TRACE_DEEP) return "deep"; if (kind == (uint8_t)BUILD_TRACE_SHALLOW) return "shallow"; + if (kind == (uint8_t)BUILD_BUNDLE_TEST_DEEP) return "test-deep"; + if (kind == (uint8_t)BUILD_BUNDLE_TEST_SHALLOW) return "test-shallow"; return NULL; } @@ -283,6 +289,14 @@ static int kind_parse(const char* s, uint8_t* out) { *out = (uint8_t)BUILD_TRACE_SHALLOW; return BUILD_OK; } + if (strcmp(s, "test-deep") == 0) { + *out = (uint8_t)BUILD_BUNDLE_TEST_DEEP; + return BUILD_OK; + } + if (strcmp(s, "test-shallow") == 0) { + *out = (uint8_t)BUILD_BUNDLE_TEST_SHALLOW; + return BUILD_OK; + } return BUILD_ERR; } @@ -318,6 +332,17 @@ static int BUILD_MAYBE_UNUSED parse_trace_header(const KitContext* ctx, memcpy(out->output, t.output, BUILD_HASH_LEN); return BUILD_OK; } + if (len >= sizeof BUILD_TEST_DEEP_MAGIC && + memcmp(data, BUILD_TEST_DEEP_MAGIC "\n", + sizeof BUILD_TEST_DEEP_MAGIC) == 0) { + BuildDeepTrace t; + if (build_test_deep_parse(data, len, &t, err, sizeof err) != BUILD_OK) + return BUILD_ERR; + snprintf(out->target, sizeof out->target, "%s", t.target); + out->kind = (uint8_t)BUILD_BUNDLE_TEST_DEEP; + memcpy(out->output, t.output, BUILD_HASH_LEN); + return BUILD_OK; + } if (len >= sizeof BUILD_SHALLOW_MAGIC && memcmp(data, BUILD_SHALLOW_MAGIC "\n", sizeof BUILD_SHALLOW_MAGIC) == 0) { BuildShallowTrace t; @@ -358,6 +383,47 @@ static int BUILD_MAYBE_UNUSED parse_trace_header(const KitContext* ctx, heap_free_array(ctx, t.deps, rows, sizeof *t.deps); return BUILD_OK; } + if (len >= sizeof BUILD_TEST_SHALLOW_MAGIC && + memcmp(data, BUILD_TEST_SHALLOW_MAGIC "\n", + sizeof BUILD_TEST_SHALLOW_MAGIC) == 0) { + BuildShallowTrace t; + size_t rows = count_lines(data, len); + memset(&t, 0, sizeof t); + t.config_keys = (BuildConfigKey*)heap_array( + ctx, rows, sizeof *t.config_keys, _Alignof(BuildConfigKey)); + t.sources = (BuildSourceLeaf*)heap_array(ctx, rows, sizeof *t.sources, + _Alignof(BuildSourceLeaf)); + t.globs = (BuildGlobLeaf*)heap_array(ctx, rows, sizeof *t.globs, + _Alignof(BuildGlobLeaf)); + t.deps = (BuildDepEdge*)heap_array(ctx, rows, sizeof *t.deps, + _Alignof(BuildDepEdge)); + if (!t.config_keys || !t.sources || !t.globs || !t.deps) { + heap_free_array(ctx, t.config_keys, rows, sizeof *t.config_keys); + heap_free_array(ctx, t.sources, rows, sizeof *t.sources); + heap_free_array(ctx, t.globs, rows, sizeof *t.globs); + heap_free_array(ctx, t.deps, rows, sizeof *t.deps); + return BUILD_ERR; + } + t.cap_config_keys = rows; + t.cap_sources = rows; + t.cap_globs = rows; + t.cap_deps = rows; + if (build_test_shallow_parse(data, len, &t, err, sizeof err) != BUILD_OK) { + heap_free_array(ctx, t.config_keys, rows, sizeof *t.config_keys); + heap_free_array(ctx, t.sources, rows, sizeof *t.sources); + heap_free_array(ctx, t.globs, rows, sizeof *t.globs); + heap_free_array(ctx, t.deps, rows, sizeof *t.deps); + return BUILD_ERR; + } + snprintf(out->target, sizeof out->target, "%s", t.target); + out->kind = (uint8_t)BUILD_BUNDLE_TEST_SHALLOW; + memcpy(out->output, t.output, BUILD_HASH_LEN); + heap_free_array(ctx, t.config_keys, rows, sizeof *t.config_keys); + heap_free_array(ctx, t.sources, rows, sizeof *t.sources); + heap_free_array(ctx, t.globs, rows, sizeof *t.globs); + heap_free_array(ctx, t.deps, rows, sizeof *t.deps); + return BUILD_OK; + } return BUILD_ERR; } @@ -629,6 +695,19 @@ static int BUILD_MAYBE_UNUSED export_trace_refs(KitBuildCoordinator* c, return BUILD_ERR; return BUILD_OK; } + if (len >= sizeof BUILD_TEST_DEEP_MAGIC && + memcmp(data, BUILD_TEST_DEEP_MAGIC "\n", + sizeof BUILD_TEST_DEEP_MAGIC) == 0) { + BuildDeepTrace t; + if (build_test_deep_parse(data, len, &t, err, sizeof err) != BUILD_OK) + return BUILD_ERR; + if (payload_write_blob_from_cas(c, t.root_config, root, seen) != + BUILD_OK || + payload_write_blob_from_cas(c, t.argv, root, seen) != BUILD_OK || + export_deepset_closure(c, t.deepset, root, seen) != BUILD_OK) + return BUILD_ERR; + return BUILD_OK; + } if (len >= sizeof BUILD_SHALLOW_MAGIC && memcmp(data, BUILD_SHALLOW_MAGIC "\n", sizeof BUILD_SHALLOW_MAGIC) == 0) { BuildShallowTrace t; @@ -671,6 +750,49 @@ shallow_out: heap_free_array(c->ctx, t.deps, rows, sizeof *t.deps); return ok; } + if (len >= sizeof BUILD_TEST_SHALLOW_MAGIC && + memcmp(data, BUILD_TEST_SHALLOW_MAGIC "\n", + sizeof BUILD_TEST_SHALLOW_MAGIC) == 0) { + BuildShallowTrace t; + size_t rows = count_lines(data, len); + size_t i; + int ok = BUILD_ERR; + memset(&t, 0, sizeof t); + t.config_keys = (BuildConfigKey*)heap_array( + c->ctx, rows, sizeof *t.config_keys, _Alignof(BuildConfigKey)); + t.sources = (BuildSourceLeaf*)heap_array(c->ctx, rows, sizeof *t.sources, + _Alignof(BuildSourceLeaf)); + t.globs = (BuildGlobLeaf*)heap_array(c->ctx, rows, sizeof *t.globs, + _Alignof(BuildGlobLeaf)); + t.deps = (BuildDepEdge*)heap_array(c->ctx, rows, sizeof *t.deps, + _Alignof(BuildDepEdge)); + if (!t.config_keys || !t.sources || !t.globs || !t.deps) goto test_out; + t.cap_config_keys = rows; + t.cap_sources = rows; + t.cap_globs = rows; + t.cap_deps = rows; + if (build_test_shallow_parse(data, len, &t, err, sizeof err) != BUILD_OK) + goto test_out; + if (payload_write_blob_from_cas(c, t.config, root, seen) != BUILD_OK || + payload_write_blob_from_cas(c, t.argv, root, seen) != BUILD_OK) + goto test_out; + for (i = 0u; i < t.n_deps; ++i) { + if (payload_write_blob_from_cas(c, t.deps[i].overlay_id, root, seen) != + BUILD_OK || + payload_write_blob_from_cas(c, t.deps[i].config_id, root, seen) != + BUILD_OK || + payload_write_blob_from_cas(c, t.deps[i].argv_id, root, seen) != + BUILD_OK) + goto test_out; + } + ok = BUILD_OK; +test_out: + heap_free_array(c->ctx, t.config_keys, rows, sizeof *t.config_keys); + heap_free_array(c->ctx, t.sources, rows, sizeof *t.sources); + heap_free_array(c->ctx, t.globs, rows, sizeof *t.globs); + heap_free_array(c->ctx, t.deps, rows, sizeof *t.deps); + return ok; + } return BUILD_ERR; } @@ -695,6 +817,23 @@ static int BUILD_MAYBE_UNUSED import_trace_refs(KitBuildCoordinator* c, release_file(c, &fd); return import_deepset_closure(c, t.deepset, root, seen); } + if (len >= sizeof BUILD_TEST_DEEP_MAGIC && + memcmp(data, BUILD_TEST_DEEP_MAGIC "\n", + sizeof BUILD_TEST_DEEP_MAGIC) == 0) { + BuildDeepTrace t; + KitFileData fd; + if (build_test_deep_parse(data, len, &t, err, sizeof err) != BUILD_OK) + return BUILD_ERR; + if (payload_install_blob(c, t.root_config, root, seen, NULL, NULL, &fd) != + BUILD_OK) + return BUILD_ERR; + release_file(c, &fd); + if (payload_install_blob(c, t.argv, root, seen, NULL, NULL, &fd) != + BUILD_OK) + return BUILD_ERR; + release_file(c, &fd); + return import_deepset_closure(c, t.deepset, root, seen); + } if (len >= sizeof BUILD_SHALLOW_MAGIC && memcmp(data, BUILD_SHALLOW_MAGIC "\n", sizeof BUILD_SHALLOW_MAGIC) == 0) { BuildShallowTrace t; @@ -750,6 +889,62 @@ shallow_out: heap_free_array(c->ctx, t.deps, rows, sizeof *t.deps); return ok; } + if (len >= sizeof BUILD_TEST_SHALLOW_MAGIC && + memcmp(data, BUILD_TEST_SHALLOW_MAGIC "\n", + sizeof BUILD_TEST_SHALLOW_MAGIC) == 0) { + BuildShallowTrace t; + size_t rows = count_lines(data, len); + size_t i; + int ok = BUILD_ERR; + KitFileData fd; + memset(&t, 0, sizeof t); + memset(&fd, 0, sizeof fd); + t.config_keys = (BuildConfigKey*)heap_array( + c->ctx, rows, sizeof *t.config_keys, _Alignof(BuildConfigKey)); + t.sources = (BuildSourceLeaf*)heap_array(c->ctx, rows, sizeof *t.sources, + _Alignof(BuildSourceLeaf)); + t.globs = (BuildGlobLeaf*)heap_array(c->ctx, rows, sizeof *t.globs, + _Alignof(BuildGlobLeaf)); + t.deps = (BuildDepEdge*)heap_array(c->ctx, rows, sizeof *t.deps, + _Alignof(BuildDepEdge)); + if (!t.config_keys || !t.sources || !t.globs || !t.deps) goto test_out; + t.cap_config_keys = rows; + t.cap_sources = rows; + t.cap_globs = rows; + t.cap_deps = rows; + if (build_test_shallow_parse(data, len, &t, err, sizeof err) != BUILD_OK) + goto test_out; + if (payload_install_blob(c, t.config, root, seen, NULL, NULL, &fd) != + BUILD_OK) + goto test_out; + release_file(c, &fd); + if (payload_install_blob(c, t.argv, root, seen, NULL, NULL, &fd) != + BUILD_OK) + goto test_out; + release_file(c, &fd); + for (i = 0u; i < t.n_deps; ++i) { + if (payload_install_blob(c, t.deps[i].overlay_id, root, seen, NULL, NULL, + &fd) != BUILD_OK) + goto test_out; + release_file(c, &fd); + if (payload_install_blob(c, t.deps[i].config_id, root, seen, NULL, NULL, + &fd) != BUILD_OK) + goto test_out; + release_file(c, &fd); + if (payload_install_blob(c, t.deps[i].argv_id, root, seen, NULL, NULL, + &fd) != BUILD_OK) + goto test_out; + release_file(c, &fd); + } + ok = BUILD_OK; +test_out: + release_file(c, &fd); + heap_free_array(c->ctx, t.config_keys, rows, sizeof *t.config_keys); + heap_free_array(c->ctx, t.sources, rows, sizeof *t.sources); + heap_free_array(c->ctx, t.globs, rows, sizeof *t.globs); + heap_free_array(c->ctx, t.deps, rows, sizeof *t.deps); + return ok; + } return BUILD_ERR; } @@ -806,6 +1001,49 @@ static int gather_export_claims(KitBuildCoordinator* c, memcpy(cl->output_tree, pt.output, BUILD_HASH_LEN); build_store_release(&c->store, &fd); } + if (build_test_target_key(opts->targets[ti], key) != BUILD_OK) + return BUILD_ERR; + memset(&rec, 0, sizeof rec); + rec.rows = rows; + rec.cap_rows = sizeof rows / sizeof rows[0]; + if (build_store_record_load(&c->store, key, opts->targets[ti], &rec) != + BUILD_OK) + return BUILD_ERR; + for (ri = 0u; ri < rec.n_rows; ++ri) { + KitFileData fd; + ParsedTrace pt; + BuildTraceClaim* cl; + char path[BUILD_PATH_MAX]; + uint8_t want_kind = + rec.rows[ri].kind == (uint8_t)BUILD_TRACE_DEEP + ? (uint8_t)BUILD_BUNDLE_TEST_DEEP + : (uint8_t)BUILD_BUNDLE_TEST_SHALLOW; + if (*nclaims >= cap) return BUILD_ERR; + if (build_store_get_trace(&c->store, rec.rows[ri].trace_id, &fd) != + BUILD_OK) + continue; + if (parse_trace_header(c->ctx, fd.data, fd.size, &pt) != BUILD_OK || + pt.kind != want_kind || + !kit_slice_eq_cstr(opts->targets[ti], pt.target)) { + build_store_release(&c->store, &fd); + continue; + } + if (payload_id_path(path, sizeof path, root, BUILD_BUNDLE_TRACE_DIR, + rec.rows[ri].trace_id) != BUILD_OK || + ensure_parent_dir(c, path) != BUILD_OK || + write_file(c, path, fd.data, fd.size) != BUILD_OK || + export_trace_refs(c, fd.data, fd.size, root, seen_blobs) != + BUILD_OK) { + build_store_release(&c->store, &fd); + return BUILD_ERR; + } + cl = &claims[(*nclaims)++]; + snprintf(cl->target, sizeof cl->target, "%s", pt.target); + cl->kind = pt.kind; + memcpy(cl->trace_id, rec.rows[ri].trace_id, BUILD_HASH_LEN); + memcpy(cl->output_tree, pt.output, BUILD_HASH_LEN); + build_store_release(&c->store, &fd); + } } return BUILD_OK; } @@ -830,7 +1068,7 @@ int build_bundle_export(KitBuildCoordinator* c, if (!c || !opts || !opts->targets || opts->ntargets == 0u || !opts->sk || !opts->keyid || !opts->out_path.s) return BUILD_ERR; - if (opts->ntargets > (size_t)-1 / (2u * KIT_BUILD_RECORD_CAP)) + if (opts->ntargets > (size_t)-1 / (4u * KIT_BUILD_RECORD_CAP)) return BUILD_ERR; if (opts->format == KIT_PKG_FORMAT_AUTO) { build_diagf(c->ctx, "trace bundle export: explicit package format required"); @@ -849,7 +1087,7 @@ int build_bundle_export(KitBuildCoordinator* c, } if (path_set(out_path, sizeof out_path, opts->out_path) != BUILD_OK) return BUILD_ERR; - cap = opts->ntargets * 2u * KIT_BUILD_RECORD_CAP; + cap = opts->ntargets * 4u * KIT_BUILD_RECORD_CAP; claims = (BuildTraceClaim*)heap_array(c->ctx, cap, sizeof *claims, _Alignof(BuildTraceClaim)); if (!claims) return BUILD_ERR; @@ -987,10 +1225,21 @@ int build_bundle_import(KitBuildCoordinator* c, const KitBuildImportOptions* opt if (nclaims > (uint32_t)-1) goto out; for (i = 0u; i < nclaims; ++i) { uint8_t key[BUILD_HASH_LEN]; - if (build_target_key(str_slice(claims[i].target), key) != BUILD_OK || - build_store_record_update(&c->store, key, str_slice(claims[i].target), - (BuildTraceKind)claims[i].kind, - claims[i].trace_id) != BUILD_OK) + BuildTraceKind record_kind; + if (claims[i].kind == (uint8_t)BUILD_BUNDLE_TEST_DEEP || + claims[i].kind == (uint8_t)BUILD_BUNDLE_TEST_SHALLOW) { + record_kind = claims[i].kind == (uint8_t)BUILD_BUNDLE_TEST_DEEP + ? BUILD_TRACE_DEEP + : BUILD_TRACE_SHALLOW; + if (build_test_target_key(str_slice(claims[i].target), key) != BUILD_OK) + goto out; + } else { + record_kind = (BuildTraceKind)claims[i].kind; + if (build_target_key(str_slice(claims[i].target), key) != BUILD_OK) + goto out; + } + if (build_store_record_update(&c->store, key, str_slice(claims[i].target), + record_kind, claims[i].trace_id) != BUILD_OK) goto out; } if (result) { diff --git a/src/build/coord.c b/src/build/coord.c @@ -327,6 +327,9 @@ void build_coord_stat_bump(KitBuildCoordinator* c, BuildStatField f) { case BUILD_STAT_MATERIALIZE_MISS: ++c->stats.materialize_misses; break; case BUILD_STAT_OBJECT_FETCH: ++c->stats.object_fetches; break; case BUILD_STAT_TRACE_PULL: ++c->stats.trace_pulls; break; + case BUILD_STAT_TEST_RUN: ++c->stats.test_runs; break; + case BUILD_STAT_TEST_CACHE_HIT: ++c->stats.test_cache_hits; break; + case BUILD_STAT_TEST_FAILURE: ++c->stats.test_failures; break; } } diff --git a/src/build/coord.h b/src/build/coord.h @@ -50,6 +50,11 @@ typedef struct BuildLeafSet { size_t n_children; } BuildLeafSet; +typedef enum BuildActionKind { + BUILD_ACTION_BUILD = 0, + BUILD_ACTION_TEST = 1, +} BuildActionKind; + /* A completed resolution: what a `need` returns and what the targets memo * caches. `path` is a materialized output directory; `leafset` is borrowed from * the coordinator and lives for the process. */ @@ -59,6 +64,16 @@ typedef struct BuildResolved { const BuildLeafSet* leafset; } BuildResolved; +typedef struct BuildTestResolved { + KitTestStatus status; + int exit_code; + uint8_t result_tree[BUILD_HASH_LEN]; + char path[BUILD_PATH_MAX]; + uint8_t stdout_blob[BUILD_HASH_LEN]; + uint8_t stderr_blob[BUILD_HASH_LEN]; + const BuildLeafSet* leafset; +} BuildTestResolved; + /* Opaque process-lifetime memo tables and the per-target future table. */ typedef struct BuildSourceMemo BuildSourceMemo; /* path -> blob-id (+ stat) */ typedef struct BuildGlobMemo BuildGlobMemo; /* pattern -> glob result */ @@ -108,6 +123,9 @@ typedef enum BuildStatField { BUILD_STAT_MATERIALIZE_MISS, BUILD_STAT_OBJECT_FETCH, BUILD_STAT_TRACE_PULL, + BUILD_STAT_TEST_RUN, + BUILD_STAT_TEST_CACHE_HIT, + BUILD_STAT_TEST_FAILURE, } BuildStatField; void build_coord_stat_bump(KitBuildCoordinator*, BuildStatField); diff --git a/src/build/resolve.c b/src/build/resolve.c @@ -147,11 +147,73 @@ static int try_shallow_trace(KitBuildCoordinator* c, KitSlice target, out->output_tree, &out->leafset); } -static int chain_has(const BuildChainFrame* f, KitSlice target, +static int try_test_shallow_trace(KitBuildCoordinator* c, KitSlice target, + const BuildShallowTrace* st, + const BuildConfig* cfg, + const BuildArgv* argv, + const uint8_t argv_id[BUILD_HASH_LEN], + const BuildChainFrame* chain, + BuildTestResolved* out) { + BuildDepEdge deps_copy[128]; + const BuildLeafSet* child_leafsets[128]; + BuildDepLog log; + size_t i; + if (!shallow_direct_match(c, st, cfg, argv_id)) return BUILD_ERR; + if (st->n_deps > sizeof deps_copy / sizeof deps_copy[0] || + st->n_deps > sizeof child_leafsets / sizeof child_leafsets[0]) + return BUILD_ERR; + for (i = 0; i < st->n_deps; ++i) { + BuildConfigEntry dep_cfg_entries[128]; + BuildConfigEntry overlay_entries[64]; + char dep_argv_entries[64][BUILD_VAL_MAX]; + BuildConfig dep_cfg, overlay_cfg; + BuildArgv dep_argv; + BuildResolved dep_r; + build_config_init(&dep_cfg, dep_cfg_entries, + sizeof dep_cfg_entries / sizeof dep_cfg_entries[0]); + build_config_init(&overlay_cfg, overlay_entries, + sizeof overlay_entries / sizeof overlay_entries[0]); + build_argv_init(&dep_argv, dep_argv_entries, + sizeof dep_argv_entries / sizeof dep_argv_entries[0]); + if (build_coord_config_by_id(c, st->deps[i].overlay_id, &overlay_cfg) != + BUILD_OK || + build_config_overlay_map(cfg, &overlay_cfg, &dep_cfg) != BUILD_OK || + build_coord_argv_by_id(c, st->deps[i].argv_id, &dep_argv) != + BUILD_OK || + build_resolve(c, kit_slice_cstr(st->deps[i].name), &dep_cfg, &dep_argv, + chain, &dep_r) != BUILD_OK || + !build_id_eq(dep_r.output_tree, st->deps[i].output_tree) || + !dep_r.leafset) + return BUILD_ERR; + deps_copy[i] = st->deps[i]; + child_leafsets[i] = dep_r.leafset; + } + if (build_materialize(c, st->output, out->path, sizeof out->path) != BUILD_OK) + return BUILD_ERR; + memcpy(out->result_tree, st->output, BUILD_HASH_LEN); + out->status = KIT_TEST_PASS; + out->exit_code = 0; + memset(&log, 0, sizeof log); + log.config_keys = st->config_keys; + log.n_config_keys = st->n_config_keys; + log.sources = st->sources; + log.n_sources = st->n_sources; + log.globs = st->globs; + log.n_globs = st->n_globs; + log.deps = deps_copy; + log.n_deps = st->n_deps; + log.child_leafsets = child_leafsets; + log.n_children = st->n_deps; + return build_runner_record_test_traces(c, target, cfg, argv, &log, + out->result_tree, &out->leafset); +} + +static int chain_has(const BuildChainFrame* f, BuildActionKind action, + KitSlice target, const uint8_t config_id[BUILD_HASH_LEN], const uint8_t argv_id[BUILD_HASH_LEN]) { for (; f; f = f->parent) { - if (strlen(f->target) == target.len && + if (f->action == action && strlen(f->target) == target.len && memcmp(f->target, target.s, target.len) == 0 && build_id_eq(f->config_id, config_id) && build_id_eq(f->argv_id, argv_id)) return 1; @@ -160,13 +222,14 @@ static int chain_has(const BuildChainFrame* f, KitSlice target, } int build_chain_extend(KitBuildCoordinator* c, const BuildChainFrame* parent, - KitSlice target, const uint8_t config_id[BUILD_HASH_LEN], + BuildActionKind action, KitSlice target, + const uint8_t config_id[BUILD_HASH_LEN], const uint8_t argv_id[BUILD_HASH_LEN], const BuildChainFrame** out, char* err, size_t errcap) { BuildChainFrame* f; if (!c || !config_id || !argv_id || !out) return BUILD_ERR; *out = NULL; - if (chain_has(parent, target, config_id, argv_id)) { + if (chain_has(parent, action, target, config_id, argv_id)) { if (err && errcap) snprintf(err, errcap, "dependency cycle at %.*s", KIT_SLICE_ARG(target)); return BUILD_ERR; @@ -176,6 +239,7 @@ int build_chain_extend(KitBuildCoordinator* c, const BuildChainFrame* parent, if (!f) return BUILD_ERR; memset(f, 0, sizeof *f); f->parent = parent; + f->action = action; if (target_copy(target, f->target) != BUILD_OK) return BUILD_ERR; memcpy(f->config_id, config_id, BUILD_HASH_LEN); memcpy(f->argv_id, argv_id, BUILD_HASH_LEN); @@ -233,6 +297,66 @@ static int verify_cached_hit(KitBuildCoordinator* c, KitSlice target, return BUILD_OK; } +static int blob_from_materialized_file(KitBuildCoordinator* c, const char* dir, + const char* name, + uint8_t out[BUILD_HASH_LEN]) { + char path[BUILD_PATH_MAX]; + KitFileData fd; + KitBlobInfo bi; + if (!c || !dir || !name || !out) return BUILD_ERR; + if (path_join2(path, sizeof path, dir, name) != BUILD_OK) return BUILD_ERR; + fd.data = NULL; + fd.size = 0; + fd.token = NULL; + if (!c->host.cas_host || !c->host.cas_host->file_io || + !c->host.cas_host->file_io->read_all || + c->host.cas_host->file_io->read_all(c->host.cas_host->file_io->user, path, + &fd) != KIT_OK) + return BUILD_ERR; + if (kit_cas_add_blob(c->cas, fd.data, fd.size, &bi) != KIT_OK) { + if (c->host.cas_host->file_io->release) + c->host.cas_host->file_io->release(c->host.cas_host->file_io->user, &fd); + return BUILD_ERR; + } + memcpy(out, bi.id, BUILD_HASH_LEN); + if (c->host.cas_host->file_io->release) + c->host.cas_host->file_io->release(c->host.cas_host->file_io->user, &fd); + return BUILD_OK; +} + +static int fill_test_stdio_blobs(KitBuildCoordinator* c, BuildTestResolved* r) { + if (!c || !r) return BUILD_ERR; + return blob_from_materialized_file(c, r->path, "stdout", r->stdout_blob) == + BUILD_OK && + blob_from_materialized_file(c, r->path, "stderr", r->stderr_blob) == + BUILD_OK + ? BUILD_OK + : BUILD_ERR; +} + +static int verify_cached_test_hit(KitBuildCoordinator* c, KitSlice target, + const BuildConfig* cfg, + const BuildArgv* argv, + const BuildChainFrame* chain, + const uint8_t expected[BUILD_HASH_LEN]) { + BuildTestResolved fresh; + char want[BUILD_HEX_LEN], got[BUILD_HEX_LEN]; + if (!c || !expected) return BUILD_ERR; + if (!c->opts.verify) return BUILD_OK; + memset(&fresh, 0, sizeof fresh); + if (build_run_test_recipe(c, target, cfg, argv, chain, &fresh, 0) != BUILD_OK) + return BUILD_ERR; + if (fresh.status != KIT_TEST_PASS || !build_id_eq(fresh.result_tree, expected)) { + kit_hex_encode(want, expected, BUILD_HASH_LEN); + kit_hex_encode(got, fresh.result_tree, BUILD_HASH_LEN); + build_diagf(c->ctx, + "build test: verify mismatch for %.*s: cached %s fresh %s", + KIT_SLICE_ARG(target), want, got); + return BUILD_ERR; + } + return BUILD_OK; +} + int build_resolve(KitBuildCoordinator* c, KitSlice target, const BuildConfig* cfg, const BuildArgv* argv, const BuildChainFrame* chain, BuildResolved* out) { @@ -248,8 +372,8 @@ int build_resolve(KitBuildCoordinator* c, KitSlice target, const BuildConfig* cf if (build_config_id(c->ctx->heap, cfg, config_id) != BUILD_OK || build_argv_id(c->ctx->heap, argv, argv_id) != BUILD_OK) return BUILD_ERR; - if (build_chain_extend(c, chain, target, config_id, argv_id, &child, err, - sizeof err) != BUILD_OK) { + if (build_chain_extend(c, chain, BUILD_ACTION_BUILD, target, config_id, + argv_id, &child, err, sizeof err) != BUILD_OK) { build_diagf(c->ctx, "build: %s", err); return BUILD_ERR; } @@ -350,6 +474,128 @@ scan_record: return build_run_recipe(c, target, cfg, argv, child, out); } +int build_test_resolve(KitBuildCoordinator* c, KitSlice target, + const BuildConfig* cfg, const BuildArgv* argv, + const BuildChainFrame* chain, BuildTestResolved* out) { + uint8_t config_id[BUILD_HASH_LEN], argv_id[BUILD_HASH_LEN]; + uint8_t target_key[BUILD_HASH_LEN]; + const BuildChainFrame* child; + BuildRecordRow rows[2u * KIT_BUILD_RECORD_CAP]; + BuildTargetRecord rec; + size_t i; + char err[128]; + int pulled_remote = 0; + if (!c || !cfg || !argv || !out) return BUILD_ERR; + if (build_config_id(c->ctx->heap, cfg, config_id) != BUILD_OK || + build_argv_id(c->ctx->heap, argv, argv_id) != BUILD_OK) + return BUILD_ERR; + if (build_chain_extend(c, chain, BUILD_ACTION_TEST, target, config_id, + argv_id, &child, err, sizeof err) != BUILD_OK) { + build_diagf(c->ctx, "build test: %s", err); + return BUILD_ERR; + } + if (build_test_target_key(target, target_key) == BUILD_OK) { +scan_record: + memset(&rec, 0, sizeof rec); + rec.rows = rows; + rec.cap_rows = sizeof rows / sizeof rows[0]; + if (build_store_record_load(&c->store, target_key, target, &rec) == + BUILD_OK) { + if (rec.n_rows == 0u && !pulled_remote) { + int pulled_now = 0; + pulled_remote = 1; + if (build_coord_trace_remote_pull_once(c, target, &pulled_now) == + BUILD_OK && + pulled_now) + goto scan_record; + } + for (i = 0; i < rec.n_rows; ++i) { + KitFileData fd; + BuildDeepTrace dt; + const BuildLeafSet* leaf = NULL; + int match = 0; + if (rec.rows[i].kind != (uint8_t)BUILD_TRACE_DEEP) continue; + fd.data = NULL; + fd.size = 0; + fd.token = NULL; + if (build_store_get_trace(&c->store, rec.rows[i].trace_id, &fd) != + BUILD_OK) + continue; + memset(&dt, 0, sizeof dt); + if (build_test_deep_parse(fd.data, fd.size, &dt, NULL, 0) == BUILD_OK && + strlen(dt.target) == target.len && + memcmp(dt.target, target.s, target.len) == 0 && + build_id_eq(dt.root_config, config_id) && + build_id_eq(dt.argv, argv_id) && + build_coord_deepset_load(c, dt.deepset, &leaf) == BUILD_OK && + build_leafset_refresh(c, leaf, &match) == BUILD_OK && match && + build_materialize(c, dt.output, out->path, sizeof out->path) == + BUILD_OK) { + out->status = KIT_TEST_PASS; + out->exit_code = 0; + memcpy(out->result_tree, dt.output, BUILD_HASH_LEN); + out->leafset = leaf; + if (fill_test_stdio_blobs(c, out) != BUILD_OK || + verify_cached_test_hit(c, target, cfg, argv, child, dt.output) != + BUILD_OK) { + build_store_release(&c->store, &fd); + return BUILD_ERR; + } + build_store_release(&c->store, &fd); + build_coord_stat_bump(c, BUILD_STAT_TEST_CACHE_HIT); + return BUILD_OK; + } + build_store_release(&c->store, &fd); + } + for (i = 0; i < rec.n_rows; ++i) { + KitFileData fd; + BuildConfigKey keys[128]; + BuildSourceLeaf sources[128]; + BuildGlobLeaf globs[128]; + BuildDepEdge deps[128]; + BuildShallowTrace st; + size_t nrows; + if (rec.rows[i].kind != (uint8_t)BUILD_TRACE_SHALLOW) continue; + fd.data = NULL; + fd.size = 0; + fd.token = NULL; + if (build_store_get_trace(&c->store, rec.rows[i].trace_id, &fd) != + BUILD_OK) + continue; + nrows = count_lines(fd.data, fd.size); + memset(&st, 0, sizeof st); + st.config_keys = keys; + st.cap_config_keys = sizeof keys / sizeof keys[0]; + st.sources = sources; + st.cap_sources = sizeof sources / sizeof sources[0]; + st.globs = globs; + st.cap_globs = sizeof globs / sizeof globs[0]; + st.deps = deps; + st.cap_deps = sizeof deps / sizeof deps[0]; + if (nrows <= sizeof keys / sizeof keys[0] && + build_test_shallow_parse(fd.data, fd.size, &st, NULL, 0) == + BUILD_OK && + strlen(st.target) == target.len && + memcmp(st.target, target.s, target.len) == 0 && + try_test_shallow_trace(c, target, &st, cfg, argv, argv_id, child, + out) == BUILD_OK) { + if (fill_test_stdio_blobs(c, out) != BUILD_OK || + verify_cached_test_hit(c, target, cfg, argv, child, st.output) != + BUILD_OK) { + build_store_release(&c->store, &fd); + return BUILD_ERR; + } + build_store_release(&c->store, &fd); + build_coord_stat_bump(c, BUILD_STAT_TEST_CACHE_HIT); + return BUILD_OK; + } + build_store_release(&c->store, &fd); + } + } + } + return build_run_test_recipe(c, target, cfg, argv, child, out, 1); +} + int build_dispatch(KitBuildCoordinator* c, KitSlice target, const BuildConfig* cfg, const BuildArgv* argv, const BuildChainFrame* chain, @@ -362,8 +608,8 @@ int build_dispatch(KitBuildCoordinator* c, KitSlice target, if (build_config_id(c->ctx->heap, cfg, config_id) != BUILD_OK || build_argv_id(c->ctx->heap, argv, argv_id) != BUILD_OK) return BUILD_ERR; - if (build_chain_extend(c, chain, target, config_id, argv_id, &child, err, - errcap) != BUILD_OK) + if (build_chain_extend(c, chain, BUILD_ACTION_BUILD, target, config_id, + argv_id, &child, err, errcap) != BUILD_OK) return BUILD_ERR; if (build_coord_target_intern(c, target, config_id, argv_id, out_future, &fresh) != BUILD_OK) diff --git a/src/build/resolve.h b/src/build/resolve.h @@ -49,6 +49,7 @@ * (parents outlive their concurrent children by construction — no pop). */ typedef struct BuildChainFrame { const struct BuildChainFrame* parent; /* NULL at the root request */ + BuildActionKind action; char target[BUILD_TARGET_MAX]; uint8_t config_id[BUILD_HASH_LEN]; uint8_t argv_id[BUILD_HASH_LEN]; @@ -59,7 +60,8 @@ typedef struct BuildChainFrame { * present) -> BUILD_ERR with the rendered path in err. The frame is allocated off * the coordinator's per-build arena; there is no pop (frames are immutable). */ int build_chain_extend(KitBuildCoordinator*, const BuildChainFrame* parent, - KitSlice target, const uint8_t config_id[BUILD_HASH_LEN], + BuildActionKind action, KitSlice target, + const uint8_t config_id[BUILD_HASH_LEN], const uint8_t argv_id[BUILD_HASH_LEN], const BuildChainFrame** out, char* err, size_t errcap); @@ -75,6 +77,10 @@ int build_resolve(KitBuildCoordinator*, KitSlice target, const BuildConfig* cfg, const BuildArgv* argv, const BuildChainFrame* chain, BuildResolved* out); +int build_test_resolve(KitBuildCoordinator*, KitSlice target, + const BuildConfig* cfg, const BuildArgv* argv, + const BuildChainFrame* chain, BuildTestResolved* out); + /* The async half of resolution — the engine behind recipe-side `need_submit`. * Extends `chain` (cycle-checked here, so submit fails fast on a cycle), interns * the (target, config-id, argv-id) future, and — if fresh — drives the 3 phases @@ -108,6 +114,10 @@ int build_run_recipe(KitBuildCoordinator*, KitSlice target, int build_run_recipe_probe(KitBuildCoordinator*, KitSlice target, const BuildConfig* cfg, const BuildArgv* argv, const BuildChainFrame* chain, BuildResolved* out); +int build_run_test_recipe(KitBuildCoordinator*, KitSlice target, + const BuildConfig* cfg, const BuildArgv* argv, + const BuildChainFrame* chain, BuildTestResolved* out, + int record_traces); /* The materialization ladder shared by every cache hit: * tree cache/ -> local CAS -> remote object fetch. diff --git a/src/build/runner.c b/src/build/runner.c @@ -268,6 +268,52 @@ static int service_recipe_connections(KitBuildCoordinator* c, return BUILD_OK; } +static int write_file_bytes(KitBuildCoordinator* c, const char* path, + const uint8_t* data, size_t len) { + KitWriter* w = NULL; + KitStatus st; + if (!c || !path || (!data && len) || !c->host.cas_host || + !c->host.cas_host->file_io || !c->host.cas_host->file_io->open_writer) + return BUILD_ERR; + if (c->host.cas_host->file_io->open_writer(c->host.cas_host->file_io->user, + path, &w) != KIT_OK || + !w) + return BUILD_ERR; + st = len ? kit_writer_write(w, data, len) : KIT_OK; + if (st == KIT_OK) st = kit_writer_status(w); + kit_writer_close(w); + return st == KIT_OK ? BUILD_OK : BUILD_ERR; +} + +static int copy_capture_into_output(KitBuildCoordinator* c, + const char* capture_path, + const char* out_dir, const char* name, + uint8_t blob[BUILD_HASH_LEN]) { + KitFileData fd; + KitBlobInfo bi; + char dest[BUILD_PATH_MAX]; + int ok = BUILD_ERR; + if (!c || !capture_path || !out_dir || !name || !blob) return BUILD_ERR; + if (path_join2(dest, sizeof dest, out_dir, name) != BUILD_OK) return BUILD_ERR; + fd.data = NULL; + fd.size = 0; + fd.token = NULL; + if (!c->host.cas_host || !c->host.cas_host->file_io || + !c->host.cas_host->file_io->read_all || + c->host.cas_host->file_io->read_all(c->host.cas_host->file_io->user, + capture_path, &fd) != KIT_OK) + return BUILD_ERR; + if (write_file_bytes(c, dest, fd.data, fd.size) != BUILD_OK || + kit_cas_add_blob(c->cas, fd.data, fd.size, &bi) != KIT_OK) + goto out; + memcpy(blob, bi.id, BUILD_HASH_LEN); + ok = BUILD_OK; +out: + if (c->host.cas_host->file_io->release) + c->host.cas_host->file_io->release(c->host.cas_host->file_io->user, &fd); + return ok; +} + static int build_run_recipe_impl(KitBuildCoordinator* c, KitSlice target, const BuildConfig* cfg, const BuildArgv* argv, const BuildChainFrame* chain, @@ -391,6 +437,152 @@ out_cleanup: return ok; } +static int build_run_test_recipe_impl(KitBuildCoordinator* c, KitSlice target, + const BuildConfig* cfg, + const BuildArgv* argv, + const BuildChainFrame* chain, + BuildTestResolved* out, + int record_traces) { + const BuildTargetDefn* defn; + KitBuildListener* listener = NULL; + KitBuildConn* conn = NULL; + KitBuildProc* proc = NULL; + char endpoint[BUILD_PATH_MAX]; + char sandbox[BUILD_PATH_MAX]; + char out_dir[BUILD_PATH_MAX]; + char stdout_path[BUILD_PATH_MAX]; + char stderr_path[BUILD_PATH_MAX]; + char recipe_path[BUILD_PATH_MAX]; + KitSlice proc_argv[129]; + KitBuildKV env[131]; + char env_keys[128][BUILD_KEY_MAX]; + size_t argc = 0, nenv = 0, i; + int exit_code = 1; + int ok = BUILD_ERR; + BuildConfigKey config_keys[128]; + BuildSourceLeaf sources[128]; + BuildGlobLeaf globs[128]; + BuildDepEdge deps[128]; + const BuildLeafSet* child_leafsets[128]; + BuildPendingNeed pending[128]; + BuildDepLog log; + + if (!c || !cfg || !argv || !out || !c->host.exec || !c->host.transport || + !c->host.exec->spawn_capture) + return BUILD_ERR; + endpoint[0] = '\0'; + sandbox[0] = '\0'; + out_dir[0] = '\0'; + stdout_path[0] = '\0'; + stderr_path[0] = '\0'; + recipe_path[0] = '\0'; + defn = build_defn_find(&c->defn, target); + if (!defn) { + build_diagf(c->ctx, "build test: unknown target %.*s", KIT_SLICE_ARG(target)); + return BUILD_ERR; + } + if (path_join2(recipe_path, sizeof recipe_path, c->workspace_root, + defn->recipe_path) != BUILD_OK) + return BUILD_ERR; + if (c->host.transport->listen(c->host.transport->user, endpoint, + sizeof endpoint, &listener) != 0 || + !listener) + return BUILD_ERR; + if (build_store_sandbox_new(&c->store, sandbox, sizeof sandbox, out_dir, + sizeof out_dir) != BUILD_OK) + goto out_cleanup; + if (path_join2(stdout_path, sizeof stdout_path, sandbox, "stdout") != + BUILD_OK || + path_join2(stderr_path, sizeof stderr_path, sandbox, "stderr") != + BUILD_OK) + goto out_cleanup; + + memset(&log, 0, sizeof log); + log.config_keys = config_keys; + log.cap_config_keys = sizeof config_keys / sizeof config_keys[0]; + log.sources = sources; + log.cap_sources = sizeof sources / sizeof sources[0]; + log.globs = globs; + log.cap_globs = sizeof globs / sizeof globs[0]; + log.deps = deps; + log.cap_deps = sizeof deps / sizeof deps[0]; + log.child_leafsets = child_leafsets; + log.cap_children = sizeof child_leafsets / sizeof child_leafsets[0]; + log.pending = pending; + log.cap_pending = sizeof pending / sizeof pending[0]; + log.next_token = 1; + + proc_argv[argc++] = kit_slice_cstr(recipe_path); + for (i = 0; i < argv->n && argc < sizeof proc_argv / sizeof proc_argv[0]; + ++i) + proc_argv[argc++] = kit_slice_cstr(argv->args[i]); + if (i != argv->n) goto out_cleanup; + + env[nenv].key = KIT_SLICE_LIT(KIT_BUILD_ENV_SOCK); + env[nenv++].value = kit_slice_cstr(endpoint); + env[nenv].key = KIT_SLICE_LIT(KIT_BUILD_ENV_OUT); + env[nenv++].value = kit_slice_cstr(out_dir); + env[nenv].key = KIT_SLICE_LIT(KIT_BUILD_ENV_TARGET); + env[nenv++].value = target; + for (i = 0; i < cfg->n && nenv < sizeof env / sizeof env[0]; ++i) { + const char* key = cfg->entries[i].key; + size_t prefix_len = sizeof(KIT_BUILD_ENV_PREFIX) - 1u; + if (strncmp(key, KIT_BUILD_ENV_PREFIX, prefix_len) != 0) continue; + if (append_config_key(&log, kit_slice_cstr(key)) != BUILD_OK) + goto out_cleanup; + snprintf(env_keys[nenv - 3u], sizeof env_keys[nenv - 3u], "%s", + key + prefix_len); + env[nenv].key = kit_slice_cstr(env_keys[nenv - 3u]); + env[nenv].value = kit_slice_cstr(cfg->entries[i].value); + ++nenv; + } + + if (c->host.exec->spawn_capture( + c->host.exec->user, proc_argv, argc, env, nenv, + kit_slice_cstr(c->workspace_root), kit_slice_cstr(stdout_path), + kit_slice_cstr(stderr_path), &proc) != 0 || + !proc) + goto out_cleanup; + build_coord_stat_bump(c, BUILD_STAT_TEST_RUN); + if (service_recipe_connections(c, listener, &proc, target, cfg, chain, &log, + &exit_code) != BUILD_OK) + goto out_kill; + if (copy_capture_into_output(c, stdout_path, out_dir, "stdout", + out->stdout_blob) != BUILD_OK || + copy_capture_into_output(c, stderr_path, out_dir, "stderr", + out->stderr_blob) != BUILD_OK) + goto out_cleanup; + if (build_store_ingest_output(&c->store, out_dir, out->result_tree, out->path, + sizeof out->path) != BUILD_OK) + goto out_cleanup; + out->exit_code = exit_code; + out->status = exit_code == 0 ? KIT_TEST_PASS : KIT_TEST_FAIL; + if (out->status == KIT_TEST_PASS && record_traces) { + if (build_runner_record_test_traces(c, target, cfg, argv, &log, + out->result_tree, &out->leafset) != + BUILD_OK) + goto out_cleanup; + } else { + out->leafset = NULL; + if (out->status == KIT_TEST_FAIL) + build_coord_stat_bump(c, BUILD_STAT_TEST_FAILURE); + } + ok = BUILD_OK; + goto out_cleanup; + +out_kill: + if (proc && c->host.exec->kill) c->host.exec->kill(c->host.exec->user, proc); + if (proc && c->host.exec->wait) + (void)c->host.exec->wait(c->host.exec->user, proc, &exit_code); + proc = NULL; +out_cleanup: + if (conn) c->host.transport->close(c->host.transport->user, conn); + if (listener) + c->host.transport->close_listener(c->host.transport->user, listener); + if (sandbox[0]) build_store_sandbox_done(&c->store, sandbox); + return ok; +} + int build_run_recipe(KitBuildCoordinator* c, KitSlice target, const BuildConfig* cfg, const BuildArgv* argv, const BuildChainFrame* chain, BuildResolved* out) { @@ -403,6 +595,14 @@ int build_run_recipe_probe(KitBuildCoordinator* c, KitSlice target, return build_run_recipe_impl(c, target, cfg, argv, chain, out, 0); } +int build_run_test_recipe(KitBuildCoordinator* c, KitSlice target, + const BuildConfig* cfg, const BuildArgv* argv, + const BuildChainFrame* chain, BuildTestResolved* out, + int record_traces) { + return build_run_test_recipe_impl(c, target, cfg, argv, chain, out, + record_traces); +} + int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn, KitSlice target, const BuildConfig* cfg, const BuildChainFrame* chain, BuildDepLog* log) { @@ -656,3 +856,89 @@ out: if (w) kit_writer_close(w); return ok; } + +int build_runner_record_test_traces(KitBuildCoordinator* c, KitSlice target, + const BuildConfig* cfg, + const BuildArgv* argv, + const BuildDepLog* log, + const uint8_t result[BUILD_HASH_LEN], + const BuildLeafSet** out_leafset) { + BuildLeafSet direct; + BuildShallowTrace shallow; + BuildDeepTrace deep; + KitWriter* w = NULL; + const uint8_t* bytes; + size_t len; + uint8_t trace_id[BUILD_HASH_LEN]; + uint8_t target_key[BUILD_HASH_LEN]; + char err[128]; + int ok = BUILD_ERR; + + if (!c || !cfg || !argv || !log || !result || !out_leafset) + return BUILD_ERR; + memset(&direct, 0, sizeof direct); + if (target_copy(target, direct.target) != BUILD_OK) return BUILD_ERR; + if (build_coord_recipe_id(c, target, direct.recipe) != BUILD_OK) + return BUILD_ERR; + direct.sources = log->sources; + direct.n_sources = log->n_sources; + direct.globs = log->globs; + direct.n_globs = log->n_globs; + direct.children = log->child_leafsets; + direct.n_children = log->n_children; + if (build_leafset_union(c, &direct, log->child_leafsets, log->n_children, + out_leafset) != BUILD_OK) + return BUILD_ERR; + + memset(&shallow, 0, sizeof shallow); + if (target_copy(target, shallow.target) != BUILD_OK) return BUILD_ERR; + memcpy(shallow.recipe, direct.recipe, BUILD_HASH_LEN); + memcpy(shallow.output, result, BUILD_HASH_LEN); + if (store_config_blob(c, cfg, shallow.config) != BUILD_OK || + store_argv_blob(c, argv, shallow.argv) != BUILD_OK) + return BUILD_ERR; + shallow.config_keys = log->config_keys; + shallow.n_config_keys = log->n_config_keys; + shallow.sources = log->sources; + shallow.n_sources = log->n_sources; + shallow.globs = log->globs; + shallow.n_globs = log->n_globs; + shallow.deps = log->deps; + shallow.n_deps = log->n_deps; + + if (kit_writer_mem(c->ctx->heap, &w) != KIT_OK || !w) return BUILD_ERR; + if (build_test_shallow_emit(&shallow, w, err, sizeof err) != BUILD_OK || + kit_writer_status(w) != KIT_OK) + goto out; + bytes = kit_writer_mem_bytes(w, &len); + if (build_store_put_trace(&c->store, bytes, len, trace_id) != BUILD_OK) + goto out; + if (build_test_target_key(target, target_key) != BUILD_OK || + build_store_record_update(&c->store, target_key, target, + BUILD_TRACE_SHALLOW, trace_id) != BUILD_OK) + goto out; + kit_writer_close(w); + w = NULL; + + memset(&deep, 0, sizeof deep); + if (target_copy(target, deep.target) != BUILD_OK) return BUILD_ERR; + memcpy(deep.recipe, direct.recipe, BUILD_HASH_LEN); + memcpy(deep.output, result, BUILD_HASH_LEN); + memcpy(deep.root_config, shallow.config, BUILD_HASH_LEN); + memcpy(deep.argv, shallow.argv, BUILD_HASH_LEN); + memcpy(deep.deepset, (*out_leafset)->id, BUILD_HASH_LEN); + if (kit_writer_mem(c->ctx->heap, &w) != KIT_OK || !w) return BUILD_ERR; + if (build_test_deep_emit(&deep, w, err, sizeof err) != BUILD_OK || + kit_writer_status(w) != KIT_OK) + goto out; + bytes = kit_writer_mem_bytes(w, &len); + if (build_store_put_trace(&c->store, bytes, len, trace_id) != BUILD_OK) + goto out; + if (build_store_record_update(&c->store, target_key, target, BUILD_TRACE_DEEP, + trace_id) != BUILD_OK) + goto out; + ok = BUILD_OK; +out: + if (w) kit_writer_close(w); + return ok; +} diff --git a/src/build/runner.h b/src/build/runner.h @@ -114,5 +114,11 @@ int build_runner_record_traces(KitBuildCoordinator*, KitSlice target, const BuildDepLog*, const uint8_t output[BUILD_HASH_LEN], const BuildLeafSet** out_leafset); +int build_runner_record_test_traces(KitBuildCoordinator*, KitSlice target, + const BuildConfig* cfg, + const BuildArgv* argv, + const BuildDepLog*, + const uint8_t result[BUILD_HASH_LEN], + const BuildLeafSet** out_leafset); #endif diff --git a/src/build/trace.c b/src/build/trace.c @@ -347,14 +347,16 @@ static int emit_record_rows(const BuildRecordRow* rows, size_t n, KitWriter* out return BUILD_OK; } -int build_shallow_emit(const BuildShallowTrace* t, KitWriter* out, char* err, - size_t errcap) { +static int shallow_emit_like(const BuildShallowTrace* t, KitWriter* out, + char* err, size_t errcap, const char* magic, + const char* result_key) { if (!t || !out) return set_err(err, errcap, "missing shallow trace"); if (!valid_target(t->target)) return set_err(err, errcap, "bad target"); - if (emit_bytes(out, BUILD_SHALLOW_MAGIC "\n") != BUILD_OK) return BUILD_ERR; + if (emit_bytes(out, magic) != BUILD_OK || emit_bytes(out, "\n") != BUILD_OK) + return BUILD_ERR; if (emit_text_kv(out, "target", t->target) != BUILD_OK) return BUILD_ERR; if (emit_kv(out, "recipe", t->recipe) != BUILD_OK) return BUILD_ERR; - if (emit_kv(out, "output", t->output) != BUILD_OK) return BUILD_ERR; + if (emit_kv(out, result_key, t->output) != BUILD_OK) return BUILD_ERR; if (emit_kv(out, "config", t->config) != BUILD_OK) return BUILD_ERR; if (emit_kv(out, "argv", t->argv) != BUILD_OK) return BUILD_ERR; if (emit_sorted_config_keys(t->config_keys, t->n_config_keys, out, err, @@ -370,14 +372,27 @@ int build_shallow_emit(const BuildShallowTrace* t, KitWriter* out, char* err, return kit_writer_status(out) == KIT_OK ? BUILD_OK : BUILD_ERR; } -int build_deep_emit(const BuildDeepTrace* t, KitWriter* out, char* err, - size_t errcap) { +int build_shallow_emit(const BuildShallowTrace* t, KitWriter* out, char* err, + size_t errcap) { + return shallow_emit_like(t, out, err, errcap, BUILD_SHALLOW_MAGIC, "output"); +} + +int build_test_shallow_emit(const BuildShallowTrace* t, KitWriter* out, + char* err, size_t errcap) { + return shallow_emit_like(t, out, err, errcap, BUILD_TEST_SHALLOW_MAGIC, + "result"); +} + +static int deep_emit_like(const BuildDeepTrace* t, KitWriter* out, char* err, + size_t errcap, const char* magic, + const char* result_key) { if (!t || !out) return set_err(err, errcap, "missing deep trace"); if (!valid_target(t->target)) return set_err(err, errcap, "bad target"); - if (emit_bytes(out, BUILD_DEEP_MAGIC "\n") != BUILD_OK) return BUILD_ERR; + if (emit_bytes(out, magic) != BUILD_OK || emit_bytes(out, "\n") != BUILD_OK) + return BUILD_ERR; if (emit_text_kv(out, "target", t->target) != BUILD_OK) return BUILD_ERR; if (emit_kv(out, "recipe", t->recipe) != BUILD_OK) return BUILD_ERR; - if (emit_kv(out, "output", t->output) != BUILD_OK) return BUILD_ERR; + if (emit_kv(out, result_key, t->output) != BUILD_OK) return BUILD_ERR; if (emit_kv(out, "root-config", t->root_config) != BUILD_OK) return BUILD_ERR; if (emit_kv(out, "argv", t->argv) != BUILD_OK) return BUILD_ERR; @@ -385,6 +400,16 @@ int build_deep_emit(const BuildDeepTrace* t, KitWriter* out, char* err, return kit_writer_status(out) == KIT_OK ? BUILD_OK : BUILD_ERR; } +int build_deep_emit(const BuildDeepTrace* t, KitWriter* out, char* err, + size_t errcap) { + return deep_emit_like(t, out, err, errcap, BUILD_DEEP_MAGIC, "output"); +} + +int build_test_deep_emit(const BuildDeepTrace* t, KitWriter* out, char* err, + size_t errcap) { + return deep_emit_like(t, out, err, errcap, BUILD_TEST_DEEP_MAGIC, "result"); +} + int build_deepset_emit(const BuildDeepSet* t, KitWriter* out, char* err, size_t errcap) { if (!t || !out) return set_err(err, errcap, "missing deepset"); @@ -673,8 +698,10 @@ static int append_record_row(BuildTargetRecord* out, char** fields, size_t n, #define F_ARGV 0x10u #define F_DEEPSET 0x20u -int build_shallow_parse(const uint8_t* data, size_t len, BuildShallowTrace* out, - char* err, size_t errcap) { +static int shallow_parse_like(const uint8_t* data, size_t len, + BuildShallowTrace* out, char* err, + size_t errcap, const char* magic, + const char* result_key) { TraceParser p; TraceSection sec = TRACE_SEC_TOP; uint32_t seen = 0; @@ -701,7 +728,7 @@ int build_shallow_parse(const uint8_t* data, size_t len, BuildShallowTrace* out, if (r == 0) break; if (p.first) { p.first = 0; - if (strcmp(line, BUILD_SHALLOW_MAGIC) != 0) + if (strcmp(line, magic) != 0) return set_err(err, errcap, "bad shallow magic/version"); continue; } @@ -734,8 +761,8 @@ int build_shallow_parse(const uint8_t* data, size_t len, BuildShallowTrace* out, err, errcap) != BUILD_OK) return BUILD_ERR; } else if (!(seen & F_OUTPUT)) { - if (parse_top_hash(fields, n, "output", out->output, &seen, F_OUTPUT, - err, errcap) != BUILD_OK) + if (parse_top_hash(fields, n, result_key, out->output, &seen, + F_OUTPUT, err, errcap) != BUILD_OK) return BUILD_ERR; } else if (!(seen & F_CONFIG)) { if (parse_top_hash(fields, n, "config", out->config, &seen, F_CONFIG, @@ -769,8 +796,22 @@ int build_shallow_parse(const uint8_t* data, size_t len, BuildShallowTrace* out, return BUILD_OK; } -int build_deep_parse(const uint8_t* data, size_t len, BuildDeepTrace* out, - char* err, size_t errcap) { +int build_shallow_parse(const uint8_t* data, size_t len, BuildShallowTrace* out, + char* err, size_t errcap) { + return shallow_parse_like(data, len, out, err, errcap, BUILD_SHALLOW_MAGIC, + "output"); +} + +int build_test_shallow_parse(const uint8_t* data, size_t len, + BuildShallowTrace* out, char* err, + size_t errcap) { + return shallow_parse_like(data, len, out, err, errcap, + BUILD_TEST_SHALLOW_MAGIC, "result"); +} + +static int deep_parse_like(const uint8_t* data, size_t len, + BuildDeepTrace* out, char* err, size_t errcap, + const char* magic, const char* result_key) { TraceParser p; uint32_t seen = 0; if (!data || !out) return set_err(err, errcap, "missing deep trace"); @@ -788,7 +829,7 @@ int build_deep_parse(const uint8_t* data, size_t len, BuildDeepTrace* out, if (r == 0) break; if (p.first) { p.first = 0; - if (strcmp(line, BUILD_DEEP_MAGIC) != 0) + if (strcmp(line, magic) != 0) return set_err(err, errcap, "bad deep magic/version"); continue; } @@ -805,8 +846,8 @@ int build_deep_parse(const uint8_t* data, size_t len, BuildDeepTrace* out, err, errcap) != BUILD_OK) return BUILD_ERR; } else if (!(seen & F_OUTPUT)) { - if (parse_top_hash(fields, n, "output", out->output, &seen, F_OUTPUT, err, - errcap) != BUILD_OK) + if (parse_top_hash(fields, n, result_key, out->output, &seen, F_OUTPUT, + err, errcap) != BUILD_OK) return BUILD_ERR; } else if (!(seen & F_CONFIG)) { if (parse_top_hash(fields, n, "root-config", out->root_config, &seen, @@ -830,6 +871,18 @@ int build_deep_parse(const uint8_t* data, size_t len, BuildDeepTrace* out, return BUILD_OK; } +int build_deep_parse(const uint8_t* data, size_t len, BuildDeepTrace* out, + char* err, size_t errcap) { + return deep_parse_like(data, len, out, err, errcap, BUILD_DEEP_MAGIC, + "output"); +} + +int build_test_deep_parse(const uint8_t* data, size_t len, BuildDeepTrace* out, + char* err, size_t errcap) { + return deep_parse_like(data, len, out, err, errcap, BUILD_TEST_DEEP_MAGIC, + "result"); +} + int build_deepset_parse(const uint8_t* data, size_t len, BuildDeepSet* out, char* err, size_t errcap) { TraceParser p; diff --git a/src/build/trace.h b/src/build/trace.h @@ -154,6 +154,10 @@ int build_shallow_emit(const BuildShallowTrace*, KitWriter* out, char* err, size_t errcap); int build_deep_emit(const BuildDeepTrace*, KitWriter* out, char* err, size_t errcap); +int build_test_shallow_emit(const BuildShallowTrace*, KitWriter* out, char* err, + size_t errcap); +int build_test_deep_emit(const BuildDeepTrace*, KitWriter* out, char* err, + size_t errcap); int build_deepset_emit(const BuildDeepSet*, KitWriter* out, char* err, size_t errcap); int build_record_emit(const BuildTargetRecord*, KitWriter* out, char* err, @@ -166,6 +170,11 @@ int build_shallow_parse(const uint8_t* data, size_t len, BuildShallowTrace* out, char* err, size_t errcap); int build_deep_parse(const uint8_t* data, size_t len, BuildDeepTrace* out, char* err, size_t errcap); +int build_test_shallow_parse(const uint8_t* data, size_t len, + BuildShallowTrace* out, char* err, + size_t errcap); +int build_test_deep_parse(const uint8_t* data, size_t len, BuildDeepTrace* out, + char* err, size_t errcap); int build_deepset_parse(const uint8_t* data, size_t len, BuildDeepSet* out, char* err, size_t errcap); int build_record_parse(const uint8_t* data, size_t len, BuildTargetRecord* out, diff --git a/test/build/build_public_link_test.c b/test/build/build_public_link_test.c @@ -240,6 +240,7 @@ static void test_public_symbols(void) { KitBuildClient* client = (KitBuildClient*)1; KitBuildNeedToken token; KitBuildResult result; + KitBuildTestResult test_result; KitSlice value; int present = 1; @@ -247,9 +248,19 @@ static void test_public_symbols(void) { kit_build_stats(NULL, &stats); EXPECT(stats.deep_hits == 0 && stats.shallow_hits == 0 && stats.recipes_run == 0 && stats.materialize_misses == 0 && - stats.object_fetches == 0 && stats.trace_pulls == 0, + stats.object_fetches == 0 && stats.trace_pulls == 0 && + stats.test_runs == 0 && stats.test_cache_hits == 0 && + stats.test_failures == 0, "stats null handle clears output"); EXPECT(kit_build(NULL, NULL, NULL) == KIT_INVALID, "kit_build rejects nulls"); + EXPECT(kit_build_test(NULL, NULL, NULL) == KIT_INVALID, + "kit_build_test rejects nulls"); + memset(&test_result, 0, sizeof test_result); + test_result.status = KIT_TEST_FAIL; + EXPECT(test_result.status == KIT_TEST_FAIL && + sizeof test_result.stdout_blob == KIT_BUILD_HASH_LEN && + sizeof test_result.stderr_blob == KIT_BUILD_HASH_LEN, + "test result public fields link"); EXPECT(kit_build_coordinator_open(NULL, NULL, KIT_SLICE_NULL, NULL, NULL) == KIT_INVALID, "coordinator open rejects nulls"); diff --git a/test/build/build_pure_test.c b/test/build/build_pure_test.c @@ -42,6 +42,17 @@ static void fill_id(uint8_t id[BUILD_HASH_LEN], uint8_t seed) { for (i = 0; i < BUILD_HASH_LEN; ++i) id[i] = (uint8_t)(seed + i); } +static int bytes_contains(const void* hay, size_t hay_len, const char* needle) { + const uint8_t* h = (const uint8_t*)hay; + size_t n = strlen(needle); + size_t i; + if (!needle || n == 0u) return 1; + if (!hay || hay_len < n) return 0; + for (i = 0; i + n <= hay_len; ++i) + if (memcmp(h + i, needle, n) == 0) return 1; + return 0; +} + static void test_config_and_argv(void) { BuildConfigEntry cfg_storage[4], parsed_storage[4], over_storage[4]; char argv_storage[3][BUILD_VAL_MAX], parsed_argv_storage[3][BUILD_VAL_MAX]; @@ -158,6 +169,7 @@ static void test_trace(void) { BuildDepEdge deps[1], parsed_deps[1]; BuildConfigKey parsed_keys[2]; BuildShallowTrace parsed; + BuildDeepTrace deep, parsed_deep; BuildTargetRecord rec; BuildRecordRow rows[20], row; KitWriter* w = NULL; @@ -244,6 +256,60 @@ static void test_trace(void) { "reject duplicate source"); kit_writer_close(w); + w = NULL; + EXPECT(kit_writer_mem(&g_u.heap, &w) == KIT_OK && w, "test trace writer"); + EXPECT(build_test_shallow_emit(&st, w, err, sizeof err) == BUILD_OK, + "emit test shallow"); + bytes.data = kit_writer_mem_bytes(w, &bytes.len); + EXPECT(bytes.len > strlen(BUILD_TEST_SHALLOW_MAGIC) && + memcmp(bytes.data, BUILD_TEST_SHALLOW_MAGIC, + strlen(BUILD_TEST_SHALLOW_MAGIC)) == 0 && + bytes_contains(bytes.data, bytes.len, "\nresult ") && + !bytes_contains(bytes.data, bytes.len, "\noutput "), + "test shallow uses result field"); + memset(&parsed, 0, sizeof parsed); + parsed.config_keys = parsed_keys; + parsed.cap_config_keys = 2; + parsed.sources = parsed_sources; + parsed.cap_sources = 2; + parsed.globs = parsed_globs; + parsed.cap_globs = 1; + parsed.deps = parsed_deps; + parsed.cap_deps = 1; + EXPECT(build_test_shallow_parse(bytes.data, bytes.len, &parsed, err, + sizeof err) == BUILD_OK && + build_id_eq(parsed.output, st.output), + "parse test shallow"); + EXPECT(build_shallow_parse(bytes.data, bytes.len, &parsed, err, sizeof err) == + BUILD_ERR, + "build shallow rejects test magic"); + kit_writer_close(w); + + memset(&deep, 0, sizeof deep); + strcpy(deep.target, "//app:bin"); + fill_id(deep.recipe, 0); + fill_id(deep.output, 1); + fill_id(deep.root_config, 2); + fill_id(deep.argv, 3); + fill_id(deep.deepset, 4); + w = NULL; + EXPECT(kit_writer_mem(&g_u.heap, &w) == KIT_OK && w, "test deep writer"); + EXPECT(build_test_deep_emit(&deep, w, err, sizeof err) == BUILD_OK, + "emit test deep"); + bytes.data = kit_writer_mem_bytes(w, &bytes.len); + EXPECT(bytes_contains(bytes.data, bytes.len, "\nresult ") && + !bytes_contains(bytes.data, bytes.len, "\noutput "), + "test deep uses result field"); + memset(&parsed_deep, 0, sizeof parsed_deep); + EXPECT(build_test_deep_parse(bytes.data, bytes.len, &parsed_deep, err, + sizeof err) == BUILD_OK && + build_id_eq(parsed_deep.output, deep.output), + "parse test deep"); + EXPECT(build_deep_parse(bytes.data, bytes.len, &parsed_deep, err, + sizeof err) == BUILD_ERR, + "build deep rejects test magic"); + kit_writer_close(w); + memset(&rec, 0, sizeof rec); strcpy(rec.target, "//app:bin"); rec.rows = rows; @@ -349,7 +415,7 @@ static void test_bundle_manifest(void) { } int main(void) { - uint8_t key1[BUILD_HASH_LEN], key2[BUILD_HASH_LEN]; + uint8_t key1[BUILD_HASH_LEN], key2[BUILD_HASH_LEN], test_key[BUILD_HASH_LEN]; BuildPathBlob paths[1]; uint8_t glob_id[BUILD_HASH_LEN]; @@ -360,6 +426,10 @@ int main(void) { EXPECT(build_target_key(KIT_SLICE_LIT("//app:bin"), key2) == BUILD_OK && build_id_eq(key1, key2), "target key stable"); + EXPECT(build_test_target_key(KIT_SLICE_LIT("//app:bin"), test_key) == + BUILD_OK && + !build_id_eq(key1, test_key), + "test target key differs from build target key"); strcpy(paths[0].path, "src/a.c"); fill_id(paths[0].blob, 9); EXPECT(build_glob_result_hash(&g_u.heap, paths, 1, glob_id) == BUILD_OK, diff --git a/test/buildcoord/run.sh b/test/buildcoord/run.sh @@ -83,6 +83,16 @@ recipe recipes/stable_dep.sh recipe recipes/stable_parent.sh [target //submit:probe] recipe recipes/submit.sh +[target //test:fail] +recipe recipes/test_fail.sh +[target //test:nondet] +recipe recipes/test_nondet.sh +[target //test:parent] +recipe recipes/test_parent.sh +[target //test:pass] +recipe recipes/test_pass.sh +[target //test:separate] +recipe recipes/test_pass.sh EOF cat > "$ws/src/a.txt" <<'EOF' @@ -385,6 +395,70 @@ cat "$dep_dir/args.txt" > "$KIT_BUILD_OUT/submit.txt" dep_dir_again=$("$KIT" build need-await "$tok") cat "$dep_dir_again/args.txt" > "$KIT_BUILD_OUT/submit-again.txt" EOF + +cat > "$ws/recipes/test_pass.sh" <<'EOF' +#!/bin/sh +set -eu +count=0 +if [ -f test-pass.count ]; then + count=$(cat test-pass.count) +fi +count=$((count + 1)) +printf '%s\n' "$count" > test-pass.count +mkdir -p "$KIT_BUILD_OUT" +"$KIT" build source src/a.txt >/dev/null +printf 'pass-artifact\n' > "$KIT_BUILD_OUT/result.txt" +printf 'pass-stdout:%s\n' "$count" +printf 'pass-stderr:%s\n' "$count" >&2 +EOF + +cat > "$ws/recipes/test_parent.sh" <<'EOF' +#!/bin/sh +set -eu +count=0 +if [ -f test-parent.count ]; then + count=$(cat test-parent.count) +fi +count=$((count + 1)) +printf '%s\n' "$count" > test-parent.count +mkdir -p "$KIT_BUILD_OUT" +dep_dir=$("$KIT" build need //stable:dep) +cat "$dep_dir/stable.txt" > "$KIT_BUILD_OUT/stable.txt" +printf 'parent-pass\n' +EOF + +cat > "$ws/recipes/test_fail.sh" <<'EOF' +#!/bin/sh +set -eu +count=0 +if [ -f test-fail.count ]; then + count=$(cat test-fail.count) +fi +count=$((count + 1)) +printf '%s\n' "$count" > test-fail.count +mkdir -p "$KIT_BUILD_OUT" +printf 'fail-artifact:%s\n' "$count" > "$KIT_BUILD_OUT/fail.txt" +printf 'fail-stdout:%s\n' "$count" +printf 'fail-stderr:%s\n' "$count" >&2 +if [ -f src/test_fail_mode ] && grep -q '^pass$' src/test_fail_mode; then + exit 0 +fi +exit 7 +EOF + +cat > "$ws/recipes/test_nondet.sh" <<'EOF' +#!/bin/sh +set -eu +count=0 +if [ -f test-nondet.count ]; then + count=$(cat test-nondet.count) +fi +count=$((count + 1)) +printf '%s\n' "$count" > test-nondet.count +mkdir -p "$KIT_BUILD_OUT" +printf 'nondet-test:%s\n' "$count" > "$KIT_BUILD_OUT/nondet.txt" +printf 'nondet-stdout:%s\n' "$count" +EOF chmod +x "$ws"/recipes/*.sh run_build() { @@ -395,6 +469,14 @@ run_build() { > "$work/$name.out" 2> "$work/$name.err" } +run_test() { + name=$1 + shift + "$KIT" build test --store "$store" --root "$ws" --def BUILD.kit \ + --config "env.KIT=$KIT" "$@" \ + > "$work/$name.out" 2> "$work/$name.err" +} + build_assert_ok() { name=$1 shift @@ -405,10 +487,44 @@ build_assert_ok() { fi } +test_assert_pass() { + name=$1 + shift + if run_test "$name" "$@"; then + if awk 'NR == 1 && $1 == "PASS" {ok = 1} END {exit ok ? 0 : 1}' \ + "$work/$name.out"; then + ok "$name" + else + not_ok "$name" "$work/$name.out" + fi + else + not_ok "$name" "$work/$name.err" + fi +} + +test_assert_fail_result() { + name=$1 + shift + if run_test "$name" "$@"; then + not_ok "$name" "$work/$name.out" + else + if awk 'NR == 1 && $1 == "FAIL" {ok = 1} END {exit ok ? 0 : 1}' \ + "$work/$name.out"; then + ok "$name" + else + not_ok "$name" "$work/$name.err" + fi + fi +} + tree_path_from() { awk 'NF >= 2 {print $2; exit}' "$1" } +test_tree_path_from() { + awk 'NF >= 3 {print $3; exit}' "$1" +} + tree_id_from() { awk 'NF >= 1 {print $1; exit}' "$1" } @@ -852,6 +968,76 @@ contains "buildcoord-globstar-deep-hit" "$work/buildcoord-globstar-deep.err" \ contains "buildcoord-globstar-deep-no-run" "$work/buildcoord-globstar-deep.err" \ "recipes_run=0" +test_assert_pass buildcoord-test-pass-cold --stats //test:pass +contains "buildcoord-test-pass-cold-run" "$work/buildcoord-test-pass-cold.err" \ + "test_runs=1" +test_pass_path=$(test_tree_path_from "$work/buildcoord-test-pass-cold.out") +contains "buildcoord-test-pass-artifact" "$test_pass_path/result.txt" \ + "pass-artifact" +contains "buildcoord-test-pass-stdout" "$test_pass_path/stdout" \ + "pass-stdout:1" +contains "buildcoord-test-pass-stderr" "$test_pass_path/stderr" \ + "pass-stderr:1" + +test_assert_pass buildcoord-test-pass-deep --stats //test:pass +contains "buildcoord-test-pass-deep-hit" "$work/buildcoord-test-pass-deep.err" \ + "test_cache_hits=1" +contains "buildcoord-test-pass-deep-no-run" \ + "$work/buildcoord-test-pass-deep.err" "test_runs=0" +contains "buildcoord-test-pass-deep-count" "$ws/test-pass.count" "1" + +test_assert_pass buildcoord-test-parent-cold --stats //test:parent +contains "buildcoord-test-parent-cold-run" \ + "$work/buildcoord-test-parent-cold.err" "test_runs=1" +cat > "$ws/src/noop.txt" <<'EOF' +third +EOF +test_assert_pass buildcoord-test-parent-shallow --stats //test:parent +contains "buildcoord-test-parent-shallow-hit" \ + "$work/buildcoord-test-parent-shallow.err" "test_cache_hits=1" +contains "buildcoord-test-parent-shallow-no-run" \ + "$work/buildcoord-test-parent-shallow.err" "test_runs=0" +contains "buildcoord-test-parent-shallow-count" "$ws/test-parent.count" "1" + +test_assert_fail_result buildcoord-test-fail-first --stats //test:fail +test_fail_path=$(test_tree_path_from "$work/buildcoord-test-fail-first.out") +contains "buildcoord-test-fail-artifact" "$test_fail_path/fail.txt" \ + "fail-artifact:1" +contains "buildcoord-test-fail-stdout" "$test_fail_path/stdout" \ + "fail-stdout:1" +contains "buildcoord-test-fail-stderr" "$test_fail_path/stderr" \ + "fail-stderr:1" +contains "buildcoord-test-fail-stats" "$work/buildcoord-test-fail-first.err" \ + "test_failures=1" +test_assert_fail_result buildcoord-test-fail-reruns --stats //test:fail +contains "buildcoord-test-fail-rerun-count" "$ws/test-fail.count" "2" +contains "buildcoord-test-fail-rerun-no-cache" \ + "$work/buildcoord-test-fail-reruns.err" "test_cache_hits=0" +cat > "$ws/src/test_fail_mode" <<'EOF' +pass +EOF +test_assert_pass buildcoord-test-fail-then-pass --stats //test:fail +contains "buildcoord-test-fail-pass-run" \ + "$work/buildcoord-test-fail-then-pass.err" "test_runs=1" +test_assert_pass buildcoord-test-fail-pass-cached --stats //test:fail +contains "buildcoord-test-fail-pass-cached-hit" \ + "$work/buildcoord-test-fail-pass-cached.err" "test_cache_hits=1" + +build_assert_ok buildcoord-test-build-record-cold --stats //test:separate +cat > "$ws/test-pass.count" <<'EOF' +0 +EOF +test_assert_pass buildcoord-test-record-separate --stats //test:separate +contains "buildcoord-test-record-separate-runs" \ + "$work/buildcoord-test-record-separate.err" "test_runs=1" + +test_assert_pass buildcoord-test-nondet-cold //test:nondet +run_fail "buildcoord-test-verify-mismatch" \ + "$KIT" build test --store "$store" --root "$ws" --def BUILD.kit \ + --config "env.KIT=$KIT" --verify //test:nondet +contains "buildcoord-test-verify-diag" \ + "$work/buildcoord-test-verify-mismatch.err" "verify mismatch" + run_fail "buildcoord-failure-first-fails" \ "$KIT" build --store "$store" --root "$ws" --def BUILD.kit \ --config "env.KIT=$KIT" //fail:probe