mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
A main commit can now publish its test-impact bundle — the commit's frozen graph, its per-test coverage map and receipt.json binding both — and a PR selection given the bundle of its merge base, verified by the caller, builds its graph incrementally from the bundle's graph and narrows suites by the admitted map. Without a verified bundle nothing changes: the graph is built from scratch and every reached suite runs whole. Producer and consumer: - `codebase-memory-mcp test-impact publish|select` (in process, no daemon): publish builds the bundle of the checked-out commit, from the previous bundle's graph when that bundle verifies; select is the selection with optional --artifact/--artifact-commit/--artifact-verified. The MCP surface is unchanged. - receipt.json binds the exact imported graph (graph digest), the graph's content and topology, the map's three files and a compatibility digest (platform label + the bytes of test_impact.coverage.compatibility_paths). - A bundle is used only when the caller verified its source, it is the merge base's own, and its imported graph is the one the receipt names; the map is admitted by the existing receipt policy (source, ancestry, content, age, compatibility, metadata). Refusals become rejection reasons in the answer's receipt. - C_eff: a changed function the map knows contributes its own id; one it does not know (new, platform-only, a type or macro) contributes its nearest mapped callers on the static inbound closure; deleted names contribute every mapped function of that name. Graph: - the frozen build accepts a base candidate and repairs it through the incremental closure route; any doubt declines to the full build. Pinned to one worker like the full frozen build. - incremental closure repair skipped the interprocedural complexity pass: re-parsed functions lost transitive_loop_depth/recursive and callers kept stale values. One traversal now serves the in-memory pass and a new store-level recompute that runs before the importance rescore. - test-impact's frozen builds use a fixed display Git context: the per-run pin path and the commit ids made two builds of one tree differ and made every commit a semantic-input change, so repair always declined. - graph content and topology digests (store_content_digest.c): the exact graph digest binds one database (ids, db_uid, generation), never equal across builds; the content digest keys rows by what they say, the topology digest by what selection reads. Measured on this repository (frozen publish, macOS arm64): a two-file C change repairs in 18-20 s instead of 75-80 s with the topology of the full build; a docs-only change repairs with identical content; selections with and without the bundle are identical on all seven replayed pairs (two repaired incrementally, five declined to the full build). Recorded, not fixed: closure repair still differs from a full build in edge PROPERTIES (argument quoting and key order, CONFIGURES args) and in the corpus-wide SIMILAR_TO/SEMANTICALLY_RELATED edges; selection reads neither, so the topology digest is the incremental-equals-full check and the receipt carries both. Tooling: scripts/test-impact/coverage-merge.py builds a push's incremental coverage map (re-run suites verbatim, the rest carried with ids remapped onto the new image's function table), with its contract as test.sh Step 0z7. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
704 lines
27 KiB
C
704 lines
27 KiB
C
/*
|
|
* test_impact_engine.c — detect_changes scope:"tests", end to end: git, the
|
|
* merge base's rules, classification of every changed path, the HEAD
|
|
* snapshot, seeding, the walk, the selection and the answer.
|
|
*/
|
|
#include "mcp/test_impact_engine_internal.h"
|
|
|
|
#include "foundation/compat.h"
|
|
#include "foundation/compat_fs.h"
|
|
#include "foundation/compat_thread.h"
|
|
#include "foundation/mem_core.h"
|
|
#include "foundation/platform.h"
|
|
#include "foundation/sha256.h"
|
|
#include "store/store_graph_digest.h"
|
|
#include "store/store_impact.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#ifndef _WIN32
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
/* ── Shared helpers ──────────────────────────────────────────────── */
|
|
|
|
bool te_grow(void **items, int *cap, int count, size_t size) {
|
|
if (count < *cap) {
|
|
return true;
|
|
}
|
|
int next = *cap ? *cap * 2 : 16;
|
|
void *grown = cbm_realloc(CBM_MEM_CLASS_EXTRACT, *items, (size_t)next * size);
|
|
if (!grown) {
|
|
return false;
|
|
}
|
|
*items = grown;
|
|
*cap = next;
|
|
return true;
|
|
}
|
|
|
|
bool te_oom(te_ctx_t *c) {
|
|
c->oom = true;
|
|
return false;
|
|
}
|
|
|
|
void te_note(te_ctx_t *c, const char *text) {
|
|
if (c->diagnostic && c->diagnostic_len && !c->diagnostic[0] && text) {
|
|
snprintf(c->diagnostic, c->diagnostic_len, "%s", text);
|
|
}
|
|
}
|
|
|
|
bool te_fallback(te_ctx_t *c, cbm_test_result_fallback_t code, const char *diagnostic) {
|
|
te_note(c, diagnostic);
|
|
for (int i = 0; i < c->fallback_count; i++) {
|
|
if (c->fallbacks[i] == code) {
|
|
return true;
|
|
}
|
|
}
|
|
if (!te_grow((void **)&c->fallbacks, &c->fallback_cap, c->fallback_count,
|
|
sizeof(*c->fallbacks))) {
|
|
return te_oom(c);
|
|
}
|
|
c->fallbacks[c->fallback_count++] = code;
|
|
return true;
|
|
}
|
|
|
|
/* ── git ─────────────────────────────────────────────────────────── */
|
|
|
|
bool cbm_test_impact_find_git(char *out, size_t out_len) {
|
|
const char *path = getenv("PATH");
|
|
#ifdef _WIN32
|
|
const char sep = ';';
|
|
const char *exe = "git.exe";
|
|
#else
|
|
const char sep = ':';
|
|
const char *exe = "git";
|
|
#endif
|
|
if (!path || !out || out_len == 0) {
|
|
return false;
|
|
}
|
|
const char *p = path;
|
|
while (*p) {
|
|
const char *e = strchr(p, sep);
|
|
size_t len = e ? (size_t)(e - p) : strlen(p);
|
|
bool absolute = len > 0 && (p[0] == '/' || (len > 2 && p[1] == ':'));
|
|
if (absolute && len + 1 + strlen(exe) < out_len) {
|
|
snprintf(out, out_len, "%.*s/%s", (int)len, p, exe);
|
|
struct stat st;
|
|
if (stat(out, &st) == 0 && S_ISREG(st.st_mode)
|
|
#ifndef _WIN32
|
|
&& access(out, X_OK) == 0
|
|
#endif
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
if (!e) {
|
|
break;
|
|
}
|
|
p = e + 1;
|
|
}
|
|
out[0] = '\0';
|
|
return false;
|
|
}
|
|
|
|
bool te_git(te_ctx_t *c) {
|
|
char git[4096];
|
|
if (!cbm_test_impact_find_git(git, sizeof(git))) {
|
|
return te_fallback(c, CBM_TEST_RESULT_FALLBACK_GIT_UNAVAILABLE, "no git on PATH") && false;
|
|
}
|
|
cbm_git_facts_options_t options = {.root = c->rq->repo_root,
|
|
.base_ref = c->rq->base_ref,
|
|
.git_executable = git,
|
|
.deadline_ms = c->rq->deadline_ms,
|
|
.command_limit = 4096,
|
|
.stdout_limit = (size_t)512 << 20,
|
|
.stderr_limit = 65536,
|
|
.total_output_limit = (size_t)2048 << 20};
|
|
cbm_git_facts_error_t error;
|
|
c->facts = cbm_git_facts_open(&options, &error);
|
|
if (!c->facts || !cbm_git_facts_diff(c->facts, &c->diff, &error)) {
|
|
char text[700];
|
|
snprintf(text, sizeof(text), "git: %s", error.diagnostic);
|
|
return te_fallback(c, CBM_TEST_RESULT_FALLBACK_GIT_UNAVAILABLE, text) && false;
|
|
}
|
|
c->id = cbm_git_facts_identity(c->facts);
|
|
cbm_changes_status_t status =
|
|
cbm_changes_parse(c->diff.name_status.data, c->diff.name_status.length, c->diff.patch.data,
|
|
c->diff.patch.length, &c->changes);
|
|
if (status == CBM_CHANGES_OOM) {
|
|
return te_oom(c);
|
|
}
|
|
cbm_changes_state_t state = cbm_changes_state(c->changes);
|
|
c->diff_complete = status == CBM_CHANGES_OK && state != CBM_CHANGES_UNKNOWN &&
|
|
cbm_changes_patch_reconciled(c->changes);
|
|
c->has_changes = state != CBM_CHANGES_EMPTY;
|
|
return true;
|
|
}
|
|
|
|
/* ── Query-time configuration: the merge base's (M-4) ────────────── */
|
|
|
|
bool te_config(te_ctx_t *c) {
|
|
const char *path = c->rq->config_path;
|
|
char file[4200] = "";
|
|
bool optional = false;
|
|
if (!path) {
|
|
snprintf(file, sizeof(file), "%s/merge-base-config.json", c->work_dir);
|
|
path = file;
|
|
optional = true;
|
|
cbm_git_blob_t blob;
|
|
cbm_git_facts_error_t error;
|
|
static const char name[] = ".codebase-memory.json";
|
|
cbm_git_blob_status_t found = cbm_git_facts_read_blob(
|
|
c->facts, CBM_GIT_REV_MERGE_BASE, name, sizeof(name) - 1, &blob, &error);
|
|
if (found == CBM_GIT_BLOB_ERROR) {
|
|
return te_fallback(c, CBM_TEST_RESULT_FALLBACK_CONFIG_INVALID,
|
|
"the merge base's .codebase-memory.json could not be read") &&
|
|
false;
|
|
}
|
|
if (found == CBM_GIT_BLOB_FOUND) {
|
|
FILE *f = cbm_fopen(file, "wb");
|
|
bool ok = f && fwrite(blob.bytes.data, 1, blob.bytes.length, f) == blob.bytes.length;
|
|
if (f) {
|
|
ok = fclose(f) == 0 && ok;
|
|
}
|
|
if (!ok) {
|
|
return te_fallback(c, CBM_TEST_RESULT_FALLBACK_CONFIG_INVALID,
|
|
"the merge base's configuration could not be staged") &&
|
|
false;
|
|
}
|
|
}
|
|
}
|
|
c->config = cbm_test_config_load(path, optional);
|
|
if (optional) {
|
|
(void)cbm_unlink(file); /* only the merge-base copy staged above */
|
|
}
|
|
if (!c->config) {
|
|
return te_fallback(c, CBM_TEST_RESULT_FALLBACK_CONFIG_INVALID,
|
|
"the test_impact configuration is invalid") &&
|
|
false;
|
|
}
|
|
c->policy = cbm_test_policy_new(c->config);
|
|
if (!c->policy) {
|
|
return te_fallback(c, CBM_TEST_RESULT_FALLBACK_POLICY_UNAVAILABLE,
|
|
"the test_impact rules are invalid") &&
|
|
false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* ── Classification of the changed paths ─────────────────────────── */
|
|
|
|
static bool te_match(te_ctx_t *c, int rule_index) {
|
|
for (int i = 0; i < c->match_count; i++) {
|
|
if (c->matches[i].rule_index == rule_index) {
|
|
return true;
|
|
}
|
|
}
|
|
if (!te_grow((void **)&c->matches, &c->match_cap, c->match_count, sizeof(*c->matches))) {
|
|
return te_oom(c);
|
|
}
|
|
c->matches[c->match_count++] = (te_match_t){.rule_index = rule_index, .resolved = true};
|
|
return true;
|
|
}
|
|
|
|
static bool te_fixture_path(te_ctx_t *c, const char *path, int rule_index) {
|
|
if (!te_grow((void **)&c->fixture_paths, &c->fixture_cap, c->fixture_count,
|
|
sizeof(*c->fixture_paths)) ||
|
|
!te_grow((void **)&c->fixture_rules, &c->fixture_rule_cap, c->fixture_count,
|
|
sizeof(*c->fixture_rules))) {
|
|
return te_oom(c);
|
|
}
|
|
c->fixture_paths[c->fixture_count] = path;
|
|
c->fixture_rules[c->fixture_count++] = rule_index;
|
|
return true;
|
|
}
|
|
|
|
static bool te_c_family(const char *path) {
|
|
static const char *const ext[] = {".c", ".h", ".cc", ".cpp", ".hpp",
|
|
".m", ".mm", ".cxx", ".hh"};
|
|
size_t n = strlen(path);
|
|
for (size_t i = 0; i < sizeof(ext) / sizeof(ext[0]); i++) {
|
|
size_t e = strlen(ext[i]);
|
|
if (n > e && strcmp(path + n - e, ext[i]) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* First pass: the rules. A path no rule matches is decided after the
|
|
* snapshot (te_classify_unmatched). Returns whether the snapshot is needed:
|
|
* a run-all rule decides everything without it. */
|
|
static bool te_classify_rules(te_ctx_t *c, bool *need_snapshot, bool **unmatched) {
|
|
size_t count = 0;
|
|
const cbm_change_file_t *files = cbm_changes_files(c->changes, &count);
|
|
int rule_count = 0;
|
|
const cbm_test_rule_t *rules = cbm_test_policy_rules(c->policy, &rule_count);
|
|
*unmatched = cbm_arena_calloc(&c->arena, count + 1);
|
|
if (!*unmatched) {
|
|
return te_oom(c);
|
|
}
|
|
bool run_all = false;
|
|
c->activation_complete = true;
|
|
for (size_t i = 0; i < count; i++) {
|
|
const char *path = (const char *)files[i].path;
|
|
const cbm_test_rule_t *rule = NULL;
|
|
if (!cbm_test_policy_match_path(c->policy, path, &rule)) {
|
|
c->activation_complete = false;
|
|
if (!te_fallback(c, CBM_TEST_RESULT_FALLBACK_POLICY_EVALUATION_FAILED, path)) {
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
if (!rule) {
|
|
(*unmatched)[i] = true;
|
|
continue;
|
|
}
|
|
int index = (int)(rule - rules);
|
|
if (!te_match(c, index)) {
|
|
return false;
|
|
}
|
|
run_all = run_all || rule->action == CBM_TEST_RULE_RUN_ALL;
|
|
if (rule->action == CBM_TEST_RULE_REFERENCING_TESTS && !te_fixture_path(c, path, index)) {
|
|
return false;
|
|
}
|
|
}
|
|
*need_snapshot = !run_all && c->fallback_count == 0;
|
|
return true;
|
|
}
|
|
|
|
static bool te_in_ledger(te_ctx_t *c, const char *path) {
|
|
const cbm_pinned_tree_view_t *view = cbm_pinned_tree_view(c->tree);
|
|
size_t n = 0;
|
|
const cbm_test_impact_classified_t *rows = cbm_test_impact_ledger_rows(c->ledger, &n);
|
|
size_t lo = 0;
|
|
size_t hi = n;
|
|
/* Ledger rows follow the tree's raw-path order. */
|
|
while (lo < hi) {
|
|
size_t mid = lo + (hi - lo) / 2;
|
|
int cmp = strcmp((const char *)view->files[rows[mid].file_index].path, path);
|
|
if (cmp == 0) {
|
|
return true;
|
|
}
|
|
if (cmp < 0) {
|
|
lo = mid + 1;
|
|
} else {
|
|
hi = mid;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool te_add_seed_change(te_ctx_t *c, const cbm_change_file_t *f) {
|
|
if (!te_grow((void **)&c->seed_changes, &c->seed_change_cap, c->seed_change_count,
|
|
sizeof(*c->seed_changes))) {
|
|
return te_oom(c);
|
|
}
|
|
cbm_ti_change_t change = {.path = (const char *)f->path};
|
|
if (f->status == 'D') {
|
|
change.deleted = true;
|
|
cbm_git_blob_t blob;
|
|
cbm_git_facts_error_t error;
|
|
cbm_git_blob_status_t found = cbm_git_facts_read_blob(
|
|
c->facts, CBM_GIT_REV_MERGE_BASE, (const char *)f->path, f->path_length, &blob, &error);
|
|
if (found != CBM_GIT_BLOB_FOUND) {
|
|
return te_fallback(c, CBM_TEST_RESULT_FALLBACK_GIT_UNAVAILABLE,
|
|
"a deleted file's base text could not be read");
|
|
}
|
|
change.base_text = (const char *)blob.bytes.data;
|
|
change.base_len = blob.bytes.length;
|
|
} else if (f->evidence == CBM_CHANGE_HUNKS && f->patch_file) {
|
|
change.hunks = f->patch_file->hunks;
|
|
change.hunk_count = f->patch_file->hunk_count;
|
|
} else {
|
|
change.whole_file = true;
|
|
}
|
|
c->seed_changes[c->seed_change_count++] = change;
|
|
return true;
|
|
}
|
|
|
|
/* Second pass: a path no rule matched is read through the graph when it is
|
|
* a C-family source of the snapshot (or deleted from it); anything else is a
|
|
* change the test-runner lane cannot reason about (M-10): run everything. */
|
|
static bool te_classify_unmatched(te_ctx_t *c, const bool *unmatched) {
|
|
size_t count = 0;
|
|
const cbm_change_file_t *files = cbm_changes_files(c->changes, &count);
|
|
for (size_t i = 0; i < count; i++) {
|
|
if (!unmatched[i]) {
|
|
continue;
|
|
}
|
|
const char *path = (const char *)files[i].path;
|
|
bool readable = te_c_family(path) && (files[i].status == 'D' || te_in_ledger(c, path));
|
|
bool ok = readable ? te_add_seed_change(c, &files[i])
|
|
: te_fallback(c, CBM_TEST_RESULT_FALLBACK_UNMAPPED_FILE, path);
|
|
if (!ok) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* ── Graph scope, seeding, selection ─────────────────────────────── */
|
|
|
|
static bool te_open_scope(te_ctx_t *c) {
|
|
if (cbm_store_read_scope_open(c->store, NULL, NULL, &c->scope) != CBM_STORE_OK) {
|
|
return te_fallback(c, CBM_TEST_RESULT_FALLBACK_GRAPH_UNAVAILABLE,
|
|
"the graph of HEAD could not be read") &&
|
|
false;
|
|
}
|
|
static const unsigned char project[] = "test-impact";
|
|
cbm_store_graph_digest_limits_t limits = {.max_rows = (uint64_t)1 << 32,
|
|
.max_framed_bytes = (uint64_t)1 << 40};
|
|
cbm_store_graph_digest_t digest;
|
|
if (cbm_store_graph_digest(c->scope, project, sizeof(project) - 1, &limits, &digest) !=
|
|
CBM_STORE_GRAPH_DIGEST_OK) {
|
|
return te_fallback(c, CBM_TEST_RESULT_FALLBACK_GRAPH_REJECTED,
|
|
"the graph of HEAD could not be digested") &&
|
|
false;
|
|
}
|
|
memcpy(c->graph_sha256, digest.sha256, sizeof(c->graph_sha256));
|
|
c->graph_digest = true;
|
|
return true;
|
|
}
|
|
|
|
static bool te_seed(te_ctx_t *c) {
|
|
cbm_ti_seed_input_t in = {.changes = c->seed_changes,
|
|
.change_count = c->seed_change_count,
|
|
.model = c->model,
|
|
.graph = c->graph,
|
|
.source = c->source};
|
|
cbm_ti_seed_status_t status = cbm_ti_seed(&in, &c->seeds);
|
|
if (status == CBM_TI_SEED_OOM) {
|
|
return te_oom(c);
|
|
}
|
|
if (status != CBM_TI_SEED_OK) {
|
|
return te_fallback(c, CBM_TEST_RESULT_FALLBACK_GRAPH_UNAVAILABLE,
|
|
"seeding the graph failed") &&
|
|
false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool te_select(te_ctx_t *c) {
|
|
cbm_test_selection_input_t input = {.model = c->model,
|
|
.reach = c->reach,
|
|
.reach_count = c->reach_count,
|
|
.suite_triggers = c->triggers,
|
|
.suite_trigger_count = c->trigger_count,
|
|
.has_changes = c->has_changes,
|
|
.diff_complete = c->diff_complete,
|
|
.inventory_complete = c->snapshot_ok,
|
|
.static_complete = c->static_complete,
|
|
.coverage = c->coverage_admitted ? c->coverage : NULL,
|
|
.changed_function_ids = c->cov_ids,
|
|
.changed_function_count = c->cov_count,
|
|
.coverage_admitted = c->coverage_admitted,
|
|
.coverage_changes_complete = c->cov_complete};
|
|
c->selection = cbm_test_select(&input);
|
|
if (!c->selection) {
|
|
return te_fallback(c, CBM_TEST_RESULT_FALLBACK_SELECTION_UNAVAILABLE,
|
|
"the selection could not be computed");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* ── The answer ──────────────────────────────────────────────────── */
|
|
|
|
static bool te_hex_bytes(const char *hex, unsigned char *out, size_t count) {
|
|
if (!hex || strlen(hex) != count * 2) {
|
|
return false;
|
|
}
|
|
for (size_t i = 0; i < count; i++) {
|
|
unsigned v = 0;
|
|
for (int k = 0; k < 2; k++) {
|
|
char ch = hex[i * 2 + (size_t)k];
|
|
unsigned d = ch >= '0' && ch <= '9' ? (unsigned)(ch - '0')
|
|
: ch >= 'a' && ch <= 'f' ? (unsigned)(ch - 'a' + 10)
|
|
: ch >= 'A' && ch <= 'F' ? (unsigned)(ch - 'A' + 10)
|
|
: 16U;
|
|
if (d > 15) {
|
|
return false;
|
|
}
|
|
v = v * 16 + d;
|
|
}
|
|
out[i] = (unsigned char)v;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static cbm_test_result_oid_t te_oid(const te_ctx_t *c, const char *hex) {
|
|
cbm_test_result_oid_t oid = {0};
|
|
if (!c->id || !hex || !hex[0]) {
|
|
return oid;
|
|
}
|
|
size_t bytes = c->id->oid_hex_length == 64 ? 32 : 20;
|
|
if (te_hex_bytes(hex, oid.bytes, bytes)) {
|
|
oid.format = bytes == 32 ? CBM_TEST_RESULT_OBJECT_SHA256 : CBM_TEST_RESULT_OBJECT_SHA1;
|
|
} else {
|
|
memset(oid.bytes, 0, sizeof(oid.bytes));
|
|
}
|
|
return oid;
|
|
}
|
|
|
|
static cbm_test_result_digest_t te_digest_hex(const char *hex) {
|
|
cbm_test_result_digest_t d = {0};
|
|
d.present = te_hex_bytes(hex, d.bytes, 32);
|
|
if (!d.present) {
|
|
memset(d.bytes, 0, sizeof(d.bytes));
|
|
}
|
|
return d;
|
|
}
|
|
|
|
static cbm_test_result_digest_t te_digest_of(const cbm_git_bytes_t *b) {
|
|
cbm_test_result_digest_t d = {.present = true};
|
|
cbm_sha256_ctx ctx;
|
|
cbm_sha256_init(&ctx);
|
|
cbm_sha256_update(&ctx, b->data ? b->data : (const unsigned char *)"", b->length);
|
|
cbm_sha256_final(&ctx, d.bytes);
|
|
return d;
|
|
}
|
|
|
|
static void te_receipt(te_ctx_t *c, cbm_test_result_receipt_t *r,
|
|
cbm_test_result_evidence_reason_t *graph_reason) {
|
|
memset(r, 0, sizeof(*r));
|
|
r->routes = CBM_TEST_RESULT_ROUTES_FOLLOWED;
|
|
if (c->id) {
|
|
r->object_format = c->id->oid_hex_length == 64 ? CBM_TEST_RESULT_OBJECT_SHA256
|
|
: CBM_TEST_RESULT_OBJECT_SHA1;
|
|
r->base = te_oid(c, c->id->base);
|
|
r->head = te_oid(c, c->id->head);
|
|
r->merge_base = te_oid(c, c->id->merge_base);
|
|
}
|
|
if (c->changes) {
|
|
r->diff_sha256 = te_digest_of(&c->diff.patch);
|
|
r->name_status_sha256 = te_digest_of(&c->diff.name_status);
|
|
}
|
|
if (c->config) {
|
|
r->config_sha256 = te_digest_hex(cbm_test_config_digest(c->config));
|
|
}
|
|
if (c->policy) {
|
|
r->policy_sha256 = te_digest_hex(cbm_test_policy_digest(c->policy));
|
|
}
|
|
if (c->graph_digest) {
|
|
r->graph.state = CBM_TEST_RESULT_GRAPH_CERTIFIED;
|
|
r->graph.commit = r->head;
|
|
r->graph.sha256.present = true;
|
|
memcpy(r->graph.sha256.bytes, c->graph_sha256, 32);
|
|
} else if (c->tree) {
|
|
r->graph.state = CBM_TEST_RESULT_GRAPH_REJECTED;
|
|
*graph_reason = CBM_TEST_RESULT_EVIDENCE_DIAGNOSTICS_INCOMPLETE;
|
|
r->graph.rejection_reasons = (cbm_test_result_evidence_reasons_t){graph_reason, 1};
|
|
}
|
|
te_artifact_receipt(c, &r->coverage, te_oid);
|
|
}
|
|
|
|
static cbm_test_impact_status_t te_answer(te_ctx_t *c, cbm_test_result_t **out) {
|
|
cbm_test_result_rule_match_t *matches =
|
|
cbm_arena_calloc(&c->arena, (size_t)(c->match_count + 1) * sizeof(*matches));
|
|
if (!matches) {
|
|
return CBM_TEST_IMPACT_OOM;
|
|
}
|
|
for (int i = 0; i < c->match_count; i++) {
|
|
matches[i] = (cbm_test_result_rule_match_t){.rule_index = c->matches[i].rule_index,
|
|
.target_resolved = c->matches[i].resolved};
|
|
}
|
|
cbm_test_result_receipt_t receipt;
|
|
cbm_test_result_evidence_reason_t graph_reason = CBM_TEST_RESULT_EVIDENCE_PROVIDER_UNAVAILABLE;
|
|
te_receipt(c, &receipt, &graph_reason);
|
|
cbm_test_result_comparison_t comparison = !c->changes || !c->diff_complete
|
|
? CBM_TEST_RESULT_COMPARISON_UNAVAILABLE
|
|
: c->has_changes ? CBM_TEST_RESULT_COMPARISON_CHANGED
|
|
: CBM_TEST_RESULT_COMPARISON_EMPTY;
|
|
if (comparison == CBM_TEST_RESULT_COMPARISON_UNAVAILABLE && c->fallback_count == 0 &&
|
|
!te_fallback(c, CBM_TEST_RESULT_FALLBACK_GIT_UNAVAILABLE, "the change is not complete")) {
|
|
return CBM_TEST_IMPACT_OOM;
|
|
}
|
|
bool with_selection = c->selection && comparison == CBM_TEST_RESULT_COMPARISON_CHANGED;
|
|
cbm_test_result_input_t input = {
|
|
.comparison = comparison,
|
|
.selection = with_selection ? c->selection : NULL,
|
|
.model = with_selection ? c->model : NULL,
|
|
.policy = comparison == CBM_TEST_RESULT_COMPARISON_EMPTY ? NULL : c->policy,
|
|
.inventory_complete = c->snapshot_ok,
|
|
.activation_complete = c->activation_complete,
|
|
.matched_rules = comparison == CBM_TEST_RESULT_COMPARISON_EMPTY ? NULL : matches,
|
|
.matched_rule_count =
|
|
comparison == CBM_TEST_RESULT_COMPARISON_EMPTY ? 0 : (size_t)c->match_count,
|
|
.fallbacks = c->fallbacks,
|
|
.fallback_count = (size_t)c->fallback_count,
|
|
.warnings = c->warnings,
|
|
.warning_count = (size_t)c->warning_count,
|
|
.receipt = &receipt};
|
|
cbm_test_result_limits_t limits = {.max_input_bytes = (uint64_t)1 << 30,
|
|
.max_items = (uint64_t)1 << 26,
|
|
.max_alloc_bytes = (size_t)1 << 30,
|
|
.max_output_bytes = (size_t)256 << 20};
|
|
cbm_test_result_status_t status = cbm_test_result_build(&input, &limits, NULL, NULL, out);
|
|
if (status == CBM_TEST_RESULT_OOM) {
|
|
return CBM_TEST_IMPACT_OOM;
|
|
}
|
|
if (status != CBM_TEST_RESULT_OK) {
|
|
char text[64];
|
|
snprintf(text, sizeof(text), "result codec status %d", (int)status);
|
|
te_note(c, text);
|
|
return CBM_TEST_IMPACT_INVALID;
|
|
}
|
|
return CBM_TEST_IMPACT_OK;
|
|
}
|
|
|
|
/* ── The request ─────────────────────────────────────────────────── */
|
|
|
|
static void te_free(te_ctx_t *c) {
|
|
cbm_test_selection_free(c->selection);
|
|
cbm_coverage_map_free(c->coverage);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, c->coverage_meta);
|
|
cbm_ti_seeds_free(c->seeds);
|
|
if (c->scope) {
|
|
(void)cbm_store_read_scope_close(c->scope);
|
|
}
|
|
te_snapshot_free(c);
|
|
cbm_test_policy_free(c->policy);
|
|
cbm_test_config_free(c->config);
|
|
cbm_changes_free(c->changes);
|
|
cbm_git_facts_free(c->facts);
|
|
void *arrays[] = {c->matches, c->fallbacks, c->warnings, c->seed_changes,
|
|
c->fixture_paths, c->fixture_rules, c->triggers};
|
|
for (size_t i = 0; i < sizeof(arrays) / sizeof(arrays[0]); i++) {
|
|
cbm_free(CBM_MEM_CLASS_EXTRACT, arrays[i]);
|
|
}
|
|
cbm_arena_destroy(&c->arena);
|
|
}
|
|
|
|
static bool te_valid(const cbm_test_impact_request_t *rq) {
|
|
return rq && rq->repo_root && rq->repo_root[0] && rq->base_ref && rq->base_ref[0] &&
|
|
rq->work_parent && rq->work_parent[0] && rq->deadline_ms > 0;
|
|
}
|
|
|
|
/* The evidence steps. Each false is either a recorded run-all reason (the
|
|
* answer still comes) or exhausted memory (c->oom). */
|
|
static void te_steps(te_ctx_t *c) {
|
|
if (!te_work_dir(c) || !te_git(c) || !te_config(c)) {
|
|
return;
|
|
}
|
|
if (!c->has_changes || !c->diff_complete) {
|
|
return; /* nothing changed, or the change is not known in full */
|
|
}
|
|
bool need_snapshot = false;
|
|
bool *unmatched = NULL;
|
|
if (!te_classify_rules(c, &need_snapshot, &unmatched) || !need_snapshot) {
|
|
return;
|
|
}
|
|
if (!te_artifact_graph(c) || !te_snapshot(c) || !te_artifact_coverage(c) ||
|
|
!te_classify_unmatched(c, unmatched) || c->fallback_count) {
|
|
return;
|
|
}
|
|
if (!te_open_scope(c) || !te_seed(c) || !te_reach(c) || !te_coverage_ids(c)) {
|
|
return;
|
|
}
|
|
(void)te_select(c);
|
|
}
|
|
|
|
typedef struct {
|
|
const cbm_test_impact_request_t *request;
|
|
cbm_test_result_t **out;
|
|
char *diagnostic;
|
|
size_t diagnostic_len;
|
|
cbm_test_impact_status_t status;
|
|
} te_job_t;
|
|
|
|
static void *te_job_main(void *arg) {
|
|
te_job_t *job = arg;
|
|
te_ctx_t c = {
|
|
.rq = job->request, .diagnostic = job->diagnostic, .diagnostic_len = job->diagnostic_len};
|
|
cbm_arena_init(&c.arena);
|
|
te_steps(&c);
|
|
cbm_test_impact_status_t status = c.oom ? CBM_TEST_IMPACT_OOM : te_answer(&c, job->out);
|
|
if (!c.oom && cbm_now_ms() > job->request->deadline_ms && status == CBM_TEST_IMPACT_OK) {
|
|
cbm_test_result_free(*job->out);
|
|
*job->out = NULL;
|
|
status = CBM_TEST_IMPACT_CANCELLED;
|
|
}
|
|
te_free(&c);
|
|
job->status = status;
|
|
return NULL;
|
|
}
|
|
|
|
cbm_test_impact_status_t cbm_test_impact_run(const cbm_test_impact_request_t *request,
|
|
cbm_test_result_t **out, char *diagnostic,
|
|
size_t diagnostic_len) {
|
|
if (diagnostic && diagnostic_len) {
|
|
diagnostic[0] = '\0';
|
|
}
|
|
if (!out) {
|
|
return CBM_TEST_IMPACT_INVALID;
|
|
}
|
|
*out = NULL;
|
|
if (!te_valid(request)) {
|
|
return CBM_TEST_IMPACT_INVALID;
|
|
}
|
|
te_job_t job = {.request = request,
|
|
.out = out,
|
|
.diagnostic = diagnostic,
|
|
.diagnostic_len = diagnostic_len,
|
|
.status = CBM_TEST_IMPACT_OOM};
|
|
/* The request runs a whole (frozen) index pipeline. Daemon tool calls run
|
|
* on 256 KiB application threads; the pipeline needs the stack every
|
|
* pipeline thread gets (cbm_thread_create's default), so the request runs
|
|
* on its own thread whoever calls it. On cbm, the in-daemon call died
|
|
* silently inside the pipeline without this. */
|
|
cbm_thread_t thread;
|
|
if (cbm_thread_create(&thread, 0, te_job_main, &job) != 0) {
|
|
return CBM_TEST_IMPACT_OOM;
|
|
}
|
|
(void)cbm_thread_join(&thread);
|
|
return job.status;
|
|
}
|
|
|
|
typedef struct {
|
|
const cbm_test_impact_publish_t *request;
|
|
char *diagnostic;
|
|
size_t diagnostic_len;
|
|
cbm_test_impact_status_t status;
|
|
} te_publish_job_t;
|
|
|
|
static void *te_publish_main(void *arg) {
|
|
te_publish_job_t *job = arg;
|
|
const cbm_test_impact_publish_t *p = job->request;
|
|
/* The steps read a selection request; publishing is the empty change of
|
|
* HEAD against itself. */
|
|
cbm_test_impact_request_t rq = {.repo_root = p->repo_root,
|
|
.base_ref = "HEAD",
|
|
.work_parent = p->work_parent,
|
|
.config_path = p->config_path,
|
|
.deadline_ms = p->deadline_ms};
|
|
te_ctx_t c = {.rq = &rq, .diagnostic = job->diagnostic, .diagnostic_len = job->diagnostic_len};
|
|
cbm_arena_init(&c.arena);
|
|
bool ok = te_publish(&c, p);
|
|
job->status = c.oom ? CBM_TEST_IMPACT_OOM : ok ? CBM_TEST_IMPACT_OK : CBM_TEST_IMPACT_INVALID;
|
|
te_free(&c);
|
|
return NULL;
|
|
}
|
|
|
|
cbm_test_impact_status_t cbm_test_impact_publish(const cbm_test_impact_publish_t *request,
|
|
char *diagnostic, size_t diagnostic_len) {
|
|
if (diagnostic && diagnostic_len) {
|
|
diagnostic[0] = '\0';
|
|
}
|
|
if (!request || !request->repo_root || !request->work_parent || !request->out_dir ||
|
|
!request->deadline_ms) {
|
|
return CBM_TEST_IMPACT_INVALID;
|
|
}
|
|
te_publish_job_t job = {.request = request,
|
|
.diagnostic = diagnostic,
|
|
.diagnostic_len = diagnostic_len,
|
|
.status = CBM_TEST_IMPACT_OOM};
|
|
cbm_thread_t thread; /* a whole frozen index: the default pipeline stack */
|
|
if (cbm_thread_create(&thread, 0, te_publish_main, &job) != 0) {
|
|
return CBM_TEST_IMPACT_OOM;
|
|
}
|
|
(void)cbm_thread_join(&thread);
|
|
return job.status;
|
|
}
|