fix(extract): write C variants in graph_buffer's schema

Main's graph_buffer.c ("Definition variants") records a definition's
variant spans as [{"file_path","start_line","end_line"}], and the
test-impact engine reads that schema to find every TEST(...) branch of one
test. This branch's extractor wrote [{"start","end"}]. On the merge with
main, graph_buffer's same-QN merge found a non-empty variants list it could
not read and dropped every span, the definition's own included. The engine
then no longer found the second #if branch of TEST(alpha_uses_lib), so
test_impact_engine_variant_tests_are_one_test reported suite alpha
UNMAPPED, in CI on macOS and Windows and locally.

The extractor now writes the shared schema, with the file path
JSON-escaped. The C-preprocessor-only gate and the one-carrier rule are
unchanged, and the variants assertions use the shared format.

test_impact_engine, extraction, pipeline, graph_buffer and
conditional_variants: 857 passed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-10-07 22:16:36 +02:00
parent c4d6453b6d
commit c828105d1e
4 changed files with 40 additions and 13 deletions
+21 -4
View File
@@ -26,12 +26,14 @@
#include "foundation/compat.h"
#include "foundation/compat_fs.h" // cbm_fopen — crash-supervisor per-file marker write
#include "foundation/hash_table.h" // CBMHashTable — crash-supervisor quarantine set
#include "foundation/str_util.h" // cbm_json_escape — variants file_path
#include "tree_sitter/api.h" // TSParser, TSNode, TSTree, TSInput, TSLanguage, TSPoint, TSParseOptions, TSParseState
#include "foundation/constants.h"
#include "mimalloc.h" // mi_malloc/mi_calloc/mi_realloc/mi_free/mi_usable_size — bind 3rd-party allocators (#424)
#if defined(CBM_BIND_TS_ALLOCATOR) && CBM_BIND_TS_ALLOCATOR
#include "sqlite3.h" // sqlite3_mem_methods, sqlite3_config, SQLITE_CONFIG_MALLOC — bind sqlite to mimalloc
#endif
#include <limits.h> // INT_MAX
#include <stdint.h> // uint32_t, uint64_t, int64_t
#include <stdlib.h>
#include <string.h>
@@ -2165,7 +2167,9 @@ static uint64_t cbm_variant_key(const char *qn, const char *label) {
/* refs[0..n) is one (QN, label) group in span order: give every member the
* list of the group's distinct spans. */
static void cbm_variant_assign(CBMFileResult *result, const cbm_variant_ref_t *refs, int n) {
enum { VARIANT_ENTRY_MAX = 40 }; /* ,{"start":4294967295,"end":4294967295} */
/* ,{"file_path":"","start_line":4294967295,"end_line":4294967295} + path;
* cbm_json_escape writes at most 6 bytes per source byte (\u00XX). */
enum { VARIANT_ENTRY_FIXED = 64, JSON_ESCAPE_GROWTH = 6 };
int distinct = 0;
for (int i = 0; i < n; i++) {
if (i == 0 || refs[i].start != refs[i - 1].start || refs[i].end != refs[i - 1].end) {
@@ -2175,7 +2179,17 @@ static void cbm_variant_assign(CBMFileResult *result, const cbm_variant_ref_t *r
if (distinct < 2) {
return;
}
size_t cap = (size_t)distinct * VARIANT_ENTRY_MAX + 3;
const char *path = result->defs.items[refs[0].idx].file_path;
size_t path_cap = (path ? strlen(path) : 0) * JSON_ESCAPE_GROWTH + 1;
if (path_cap > INT_MAX) {
return;
}
char *esc = (char *)cbm_arena_alloc(&result->arena, path_cap);
if (!esc) {
return;
}
(void)cbm_json_escape(esc, (int)path_cap, path ? path : "");
size_t cap = (size_t)distinct * (VARIANT_ENTRY_FIXED + strlen(esc)) + 3;
char *json = (char *)cbm_arena_alloc(&result->arena, cap);
if (!json) {
return;
@@ -2186,8 +2200,11 @@ static void cbm_variant_assign(CBMFileResult *result, const cbm_variant_ref_t *r
if (i > 0 && refs[i].start == refs[i - 1].start && refs[i].end == refs[i - 1].end) {
continue;
}
int w = snprintf(json + pos, cap - pos, "%s{\"start\":%u,\"end\":%u}", pos > 1 ? "," : "",
refs[i].start, refs[i].end);
/* graph_buffer.c's variants schema, so its same-QN merge and every
* reader of the node (the test-impact engine) see one format. */
int w = snprintf(json + pos, cap - pos,
"%s{\"file_path\":\"%s\",\"start_line\":%u,\"end_line\":%u}",
pos > 1 ? "," : "", esc, refs[i].start, refs[i].end);
if (w <= 0 || (size_t)w >= cap - pos) {
return; /* cannot happen: cap covers the longest entry */
}
+3 -1
View File
@@ -271,7 +271,9 @@ typedef struct {
* (`#if`/`#else` twins, a macro redefined per platform, overloads): the
* graph keeps one node per QN, and this lists every one of those
* definitions' line spans, the surviving one included, as a JSON array
* sorted by start line: [{"start":10,"end":14},{"start":20,"end":26}].
* sorted by start line, in graph_buffer.c's variants schema:
* [{"file_path":"a.c","start_line":10,"end_line":14},
* {"file_path":"a.c","start_line":20,"end_line":26}].
* Carried by the definition the graph keeps for the file (the last by
* start line), which makes it the node's `variants` property. NULL on
* every other definition, and in every language outside
+6 -3
View File
@@ -7743,15 +7743,18 @@ TEST(extract_c_variants_lists_same_file_duplicates_c1) {
ASSERT_NOT_NULL(kept);
ASSERT_NOT_NULL(other);
ASSERT_NOT_NULL(kept->variants);
ASSERT_STR_EQ(kept->variants, "[{\"start\":2,\"end\":2},{\"start\":4,\"end\":6}]");
ASSERT_STR_EQ(kept->variants, "[{\"file_path\":\"v.c\",\"start_line\":2,\"end_line\":2},"
"{\"file_path\":\"v.c\",\"start_line\":4,\"end_line\":6}]");
ASSERT_NULL(other->variants); /* one carrier per group: the node the graph keeps */
const CBMDefinition *lim_a = c1_def_at(r, "Macro", "LIMIT", 9);
const CBMDefinition *lim_b = c1_def_at(r, "Macro", "LIMIT", 11);
ASSERT_NOT_NULL(lim_a);
ASSERT_NOT_NULL(lim_b);
char want[128];
snprintf(want, sizeof(want), "[{\"start\":%u,\"end\":%u},{\"start\":%u,\"end\":%u}]",
char want[256];
snprintf(want, sizeof(want),
"[{\"file_path\":\"v.c\",\"start_line\":%u,\"end_line\":%u},"
"{\"file_path\":\"v.c\",\"start_line\":%u,\"end_line\":%u}]",
lim_a->start_line, lim_a->end_line, lim_b->start_line, lim_b->end_line);
ASSERT_NOT_NULL(lim_b->variants);
ASSERT_STR_EQ(lim_b->variants, want);
+10 -5
View File
@@ -10536,9 +10536,12 @@ static c1_variants_obs_t c1_observe_variants(const char *repo, const char *db_na
return obs;
}
obs.opened = true;
obs.pick_lists_both = c1_props_contain(
s, proj, "v.pick", "\"variants\":[{\"start\":2,\"end\":2},{\"start\":4,\"end\":6}]");
obs.limit_has_list = c1_props_contain(s, proj, "v.LIMIT#macro", "\"variants\":[{\"start\":9,");
obs.pick_lists_both =
c1_props_contain(s, proj, "v.pick",
"\"variants\":[{\"file_path\":\"v.c\",\"start_line\":2,\"end_line\":2},"
"{\"file_path\":\"v.c\",\"start_line\":4,\"end_line\":6}]");
obs.limit_has_list = c1_props_contain(s, proj, "v.LIMIT#macro",
"\"variants\":[{\"file_path\":\"v.c\",\"start_line\":9,");
obs.once_has_list = c1_props_contain(s, proj, "v.once", "\"variants\"");
cbm_node_t *nodes = NULL;
int count = 0;
@@ -10680,8 +10683,10 @@ TEST(pipeline_cpp_overloads_are_listed_as_variants_c1) {
int scale_nodes = n;
int kept_start = n > 0 ? nodes[0].start_line : -1;
cbm_store_free_nodes(nodes, n);
bool lists_both = c1_props_contain(
s, proj, "ov.scale", "\"variants\":[{\"start\":1,\"end\":1},{\"start\":2,\"end\":4}]");
bool lists_both =
c1_props_contain(s, proj, "ov.scale",
"\"variants\":[{\"file_path\":\"ov.cpp\",\"start_line\":1,\"end_line\":1},"
"{\"file_path\":\"ov.cpp\",\"start_line\":2,\"end_line\":4}]");
bool once_has_list = c1_props_contain(s, proj, "ov.once", "\"variants\"");
cbm_store_close(s);