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:
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