From c828105d1eafe377e00a465953613eb0e32c0a31 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Wed, 7 Oct 2026 22:16:36 +0200 Subject: [PATCH] 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 --- internal/cbm/cbm.c | 25 +++++++++++++++++++++---- internal/cbm/cbm.h | 4 +++- tests/test_extraction.c | 9 ++++++--- tests/test_pipeline.c | 15 ++++++++++----- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index f4ce1354f..366a71408 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -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 // INT_MAX #include // uint32_t, uint64_t, int64_t #include #include @@ -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 */ } diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index 598d10b56..1c3b0a64a 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -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 diff --git a/tests/test_extraction.c b/tests/test_extraction.c index c5057e7f7..5c85441ea 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -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); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 135560799..5aeb49c1a 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -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);