test(coverage): pin the truncation marker, the partial ceiling and the grammar limit (#963)

Phase 5. Four test groups, each checked RED before it was kept.

- The Studio Export range join puts ONE ",+<N>" marker at the end with the
  summed drop count. A marker left mid-string makes every reader stop there
  and silently lose the ranges after it. Reaching the join through the
  pipeline needs an export file with 256+ error regions across two <Class>
  elements, so it goes through a test seam, following the pattern already in
  this repo (CBM_COVERAGE_MARKER_TEST_API).
- check_index_coverage emits every range in front of a marker, never turns
  the marker's digits into a range, and reports "truncated" from BOTH caps —
  the producer's and its own 256 limit.
- test_index_resilience now has a ceiling beside its floor: exactly one of
  the two fixture files is flagged, the clean neighbour is absent, and the
  range does not cover the whole file.
- The three _Thread_local forms are pinned as measured. Only the array form
  fails today; the plan's Phase 0 also listed the pointer form, and that is
  wrong on the grammar shipped now.

Also fixes 13 clang-format violations the earlier commits on this branch left
in cbm.c, mcp.c and pass_definitions.c. `make -f Makefile.cbm lint-format`
would have failed CI. The changes are whitespace only — the two reflowed tool
descriptions concatenate byte-identically, so no output moved.

Full suite: 7735 passed, 28 failed, 7 skipped. The 28 are the pre-existing
cli install/uninstall failures, identical at clean HEAD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
This commit is contained in:
Joshua RichterandClaude Opus 5 committed 2026-09-05 16:02:17 -04:00
1 parent a953509275
commit 219b348263
8 files changed
+254 -15

No files matched your search

+3 -1
View File
@@ -98,9 +98,10 @@ endif
KOTLIN_DEDUP_TEST_DEFINE = -DCBM_KOTLIN_DEDUP_TEST_API=1
CALL_REFERENCE_LOOKUP_TEST_DEFINE = -DCBM_CALL_REFERENCE_LOOKUP_TEST_API=1
INCREMENTAL_TEST_DEFINE = -DCBM_INCREMENTAL_TEST_API=1
COVERAGE_MARKER_TEST_DEFINE = -DCBM_COVERAGE_MARKER_TEST_API=1
CFLAGS_TEST = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(SANITIZED_DEFINE) \
$(KOTLIN_DEDUP_TEST_DEFINE) $(CALL_REFERENCE_LOOKUP_TEST_DEFINE) \
$(INCREMENTAL_TEST_DEFINE) -g -O1 $(SANITIZE)
$(INCREMENTAL_TEST_DEFINE) $(COVERAGE_MARKER_TEST_DEFINE) -g -O1 $(SANITIZE)
CXXFLAGS_TEST = $(CXXFLAGS_COMMON) $(SANITIZED_DEFINE) -g -O1 $(SANITIZE) $(CXX_STDLIB_FLAGS)
# TSan (can't combine with ASan)
@@ -118,6 +119,7 @@ TSAN_SANITIZE = -fsanitize=thread -fno-omit-frame-pointer
# macro of ours.
CFLAGS_TSAN = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(KOTLIN_DEDUP_TEST_DEFINE) \
$(CALL_REFERENCE_LOOKUP_TEST_DEFINE) $(INCREMENTAL_TEST_DEFINE) \
$(COVERAGE_MARKER_TEST_DEFINE) \
-DCBM_SANITIZED_BUILD=1 -g -O1 $(TSAN_SANITIZE)
CXXFLAGS_TSAN = $(CXXFLAGS_COMMON) -DCBM_SANITIZED_BUILD=1 -g -O1 \
$(TSAN_SANITIZE)
+3 -4
View File
@@ -1801,16 +1801,15 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
orig_lines++;
}
}
uint8_t *map =
(uint8_t *)cbm_arena_alloc(a, (size_t)orig_lines + 2);
uint8_t *map = (uint8_t *)cbm_arena_alloc(a, (size_t)orig_lines + 2);
int exp_lines = preprocessed->expanded_line_count;
uint8_t *bad_rows =
exp_lines > 0 ? (uint8_t *)calloc((size_t)exp_lines + 2, 1) : NULL;
if (map && bad_rows) {
memset(map, 0, (size_t)orig_lines + 2);
cbm_mark_no_code_lines(source, source_len, map, orig_lines);
cbm_mark_pp_error_rows(pp_root, bad_rows, (uint32_t)exp_lines,
expanded, expanded_len);
cbm_mark_pp_error_rows(pp_root, bad_rows, (uint32_t)exp_lines, expanded,
expanded_len);
/* Walk the expanded text once. An expanded line
* only vouches for its original line when it
* actually HAS content: the preprocessor emits a
+17 -8
View File
@@ -561,9 +561,9 @@ static int objectscript_export_split_range_marker(const char *ranges, size_t *bo
if (i == len || i == 0 || ranges[i - 1] != '+') {
return 0;
}
size_t marker = i - 1; /* index of '+' */
size_t marker = i - 1; /* index of '+' */
if (marker > 0 && ranges[marker - 1] == ',') {
marker--; /* drop the separator too */
marker--; /* drop the separator too */
}
*body_len = marker;
return atoi(ranges + i);
@@ -612,8 +612,8 @@ static bool objectscript_export_append_error_ranges(CBMFileResult *aggregate,
return false;
}
if (dropped > 0) {
combined = cbm_arena_sprintf(&aggregate->arena, "%s%s+%d", combined,
combined[0] ? "," : "", dropped);
combined = cbm_arena_sprintf(&aggregate->arena, "%s%s+%d", combined, combined[0] ? "," : "",
dropped);
if (!combined) {
return false;
}
@@ -622,6 +622,16 @@ static bool objectscript_export_append_error_ranges(CBMFileResult *aggregate,
return true;
}
#if defined(CBM_COVERAGE_MARKER_TEST_API) && CBM_COVERAGE_MARKER_TEST_API
/* Test seam. This join only fires for a Studio Export file holding several
* <Class> elements where a class overruns the 256-region cap — hard to reach
* through the pipeline, easy to get wrong, and a wrong result hides ranges
* without saying so. Expose the join so the marker rules can be pinned. */
bool cbm_pipeline_coverage_marker_test_join(CBMFileResult *aggregate, const CBMFileResult *part) {
return objectscript_export_append_error_ranges(aggregate, part);
}
#endif
/* Studio Export files may contain multiple <Class> elements, while the
* pipeline cache has one slot per physical file. Extract each generated UDL
* class independently (preserving the upstream parser behavior), then compose
@@ -842,10 +852,9 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
} else if (result->parse_incomplete) {
/* Best-effort parse-coverage signal (#963): indexed, but with
* ERROR/MISSING regions — see pass_parallel.c (keep in sync). */
cbm_pipeline_add_file_error(ctx->pipeline, rel,
result->error_ranges ? result->error_ranges : "unknown",
result->parse_unusable ? "parse_unusable"
: "parse_partial");
cbm_pipeline_add_file_error(
ctx->pipeline, rel, result->error_ranges ? result->error_ranges : "unknown",
result->parse_unusable ? "parse_unusable" : "parse_partial");
}
/* Create nodes for each definition */
+6
View File
@@ -864,6 +864,12 @@ void cbm_pp_bp_nap_cycles_reset(void);
uint64_t cbm_pp_lsp_linear_fallback_rows(void);
void cbm_pp_lsp_linear_fallback_rows_reset(void);
#if defined(CBM_COVERAGE_MARKER_TEST_API) && CBM_COVERAGE_MARKER_TEST_API
/* Test-only view of the Studio Export range join, so the ",+<N>" truncation
* marker rules can be checked without building a 256-region export file. */
bool cbm_pipeline_coverage_marker_test_join(CBMFileResult *aggregate, const CBMFileResult *part);
#endif
#if defined(CBM_CALL_REFERENCE_LOOKUP_TEST_API) && CBM_CALL_REFERENCE_LOOKUP_TEST_API
/* Deterministic test-only operation count for the shared semantic-reference
* matcher used by both sequential and fused-parallel usage materialization. */
+15 -1
View File
@@ -337,8 +337,16 @@ TEST(index_parse_partial_reported) {
ASSERT_STR_EQ("indexed", status);
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(sc, "skipped_count")), 0);
/* The coverage signal is surfaced with ranges + the best-effort note. */
/* The coverage signal is surfaced with ranges + the best-effort note.
* Both bounds matter. The floor catches the signal going missing. The
* ceiling catches the opposite failure: exactly one of the two files has
* a gap, so a count above 1 means the clean Python neighbour got flagged
* as well, which is how over-flagging looks from the outside. */
ASSERT_GTE(yyjson_get_int(yyjson_obj_get(sc, "parse_partial_count")), 1);
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(sc, "parse_partial_count")), 1);
/* A local gap is not a whole-file failure, so the other coverage kind
* must stay empty here. */
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(sc, "parse_unusable_count")), 0);
yyjson_val *pp = yyjson_obj_get(sc, "parse_partial");
ASSERT_NOT_NULL(pp);
yyjson_val *files = yyjson_obj_get(pp, "files");
@@ -354,7 +362,13 @@ TEST(index_parse_partial_reported) {
found_split = 1;
ASSERT_NOT_NULL(ranges);
ASSERT_GT((int)strlen(ranges), 0);
/* The gap is the two-header block, not the whole file. An
* 8-line file reported as 1-8 would be the old whole-file
* blame coming back. */
ASSERT_NULL(strstr(ranges, "1-8"));
}
/* The clean file must not appear in the list at all. */
ASSERT_NULL(fp ? strstr(fp, "good.py") : NULL);
}
ASSERT_TRUE(found_split);
const char *note = yyjson_get_str(yyjson_obj_get(pp, "note"));
+81
View File
@@ -5425,6 +5425,86 @@ TEST(tool_check_index_coverage_pages_exact_paths_and_restores_raw_diagnostics) {
PASS();
}
/* The range string can carry a trailing ",+<N>" marker saying the producer hit
* its own cap and threw ranges away. The reader must emit every range in front
* of the marker, must not turn the marker itself into a range, and must say
* "ranges_truncated" so nobody reads a short list as a complete one. The reader has a
* second cap of its own, and that one must report itself the same way. */
TEST(tool_check_index_coverage_reports_truncation_marker_issue963) {
enum { WIDE_RANGE_COUNT = 300 };
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
cbm_store_t *st = cbm_mcp_server_store(srv);
ASSERT_NOT_NULL(st);
const char *project = "coverage-marker";
ASSERT_EQ(cbm_store_upsert_project(st, project, "/tmp/coverage-marker"), CBM_STORE_OK);
cbm_mcp_server_set_project(srv, project);
/* 300 one-line ranges — past the reader's own 256 limit. */
char *wide = calloc(1, WIDE_RANGE_COUNT * 12 + 1);
ASSERT_NOT_NULL(wide);
size_t off = 0;
for (int i = 0; i < WIDE_RANGE_COUNT; i++) {
off += (size_t)snprintf(wide + off, WIDE_RANGE_COUNT * 12 + 1 - off, "%s%d-%d",
i ? "," : "", i * 3 + 1, i * 3 + 1);
}
cbm_coverage_row_t rows[3] = {
{.rel_path = "src/marked.c", .kind = "parse_partial", .detail = "3-4,9-9,+12"},
{.rel_path = "src/plain.c", .kind = "parse_partial", .detail = "3-4,9-9"},
{.rel_path = "src/wide.c", .kind = "parse_partial", .detail = wide},
};
for (int i = 0; i < 3; i++) {
ASSERT_EQ(cbm_store_upsert_file_hash(st, project, rows[i].rel_path, "fixture", i + 1, 10),
CBM_STORE_OK);
}
ASSERT_EQ(cbm_store_coverage_replace(st, project, rows, 3), CBM_STORE_OK);
/* The marked file: both real ranges survive, the marker is flagged, and the
* "12" from the marker never becomes a range of its own. */
char *marked =
cbm_mcp_handle_tool(srv, "check_index_coverage",
"{\"project\":\"coverage-marker\",\"paths\":[\"src/marked.c\"],\"format\":\"json\"}");
ASSERT_NOT_NULL(marked);
char *marked_inner = extract_text_content(marked);
ASSERT_NOT_NULL(marked_inner);
ASSERT_NOT_NULL(strstr(marked_inner, "\"start\":3"));
ASSERT_NOT_NULL(strstr(marked_inner, "\"start\":9"));
ASSERT_NULL(strstr(marked_inner, "\"start\":12"));
ASSERT_NOT_NULL(strstr(marked_inner, "\"ranges_truncated\":true"));
free(marked_inner);
free(marked);
/* The same ranges without a marker must NOT be reported as truncated. */
char *plain =
cbm_mcp_handle_tool(srv, "check_index_coverage",
"{\"project\":\"coverage-marker\",\"paths\":[\"src/plain.c\"],\"format\":\"json\"}");
ASSERT_NOT_NULL(plain);
char *plain_inner = extract_text_content(plain);
ASSERT_NOT_NULL(plain_inner);
ASSERT_NOT_NULL(strstr(plain_inner, "\"start\":3"));
ASSERT_NULL(strstr(plain_inner, "\"ranges_truncated\":true"));
free(plain_inner);
free(plain);
/* The reader's own limit stops the list early, so it must say so even
* though the producer sent no marker. */
char *widest =
cbm_mcp_handle_tool(srv, "check_index_coverage",
"{\"project\":\"coverage-marker\",\"paths\":[\"src/wide.c\"],\"format\":\"json\"}");
ASSERT_NOT_NULL(widest);
char *wide_inner = extract_text_content(widest);
ASSERT_NOT_NULL(wide_inner);
ASSERT_NOT_NULL(strstr(wide_inner, "\"ranges_truncated\":true"));
free(wide_inner);
free(widest);
free(wide);
cbm_mcp_server_free(srv);
PASS();
}
TEST(tool_check_index_coverage_reports_paths_scopes_and_ranges) {
char tmp[256];
cbm_mcp_server_t *srv = setup_snippet_server(tmp, sizeof(tmp));
@@ -19846,6 +19926,7 @@ SUITE(mcp) {
RUN_TEST(tool_list_projects_preserves_root_beyond_one_kib);
RUN_TEST(tool_index_status_no_project);
RUN_TEST(tool_check_index_coverage_finds_path_beyond_status_cap);
RUN_TEST(tool_check_index_coverage_reports_truncation_marker_issue963);
RUN_TEST(tool_check_index_coverage_reports_paths_scopes_and_ranges);
RUN_TEST(tool_check_index_coverage_pages_exact_paths_and_restores_raw_diagnostics);
RUN_TEST(tool_check_index_coverage_preserves_multiple_scope_labels);
+45 -1
View File
@@ -20,7 +20,7 @@
* GREEN (fixed): cbm_extract_file sets parse_incomplete=true iff the tree
* contains ERROR/MISSING nodes, records the 1-based line
* ranges of the TOP-MOST error regions ("start-end,..."),
* bounded by the 64-region cap, and clean files stay
* bounded by the 256-region cap, and clean files stay
* completely unflagged (no false positives).
*
* BEST-EFFORT framing (must never be weakened the other way): a flag means
@@ -791,6 +791,49 @@ TEST(c_ifdef_split_is_partial_never_unusable) {
PASS();
}
/* Phase 0 finding 2, pinned so a tree-sitter bump cannot change it quietly.
*
* The C grammar handles `_Thread_local` unevenly, and these are the three
* forms measured on the grammar shipped today:
*
* static _Thread_local int x = 0; parses clean
* static _Thread_local int *p; parses clean
* static _Thread_local char b[8]; fails — flagged as range 1-1
*
* The array line really is missing from the graph, so flagging it is the
* honest answer, not a false positive. This test exists to make a grammar
* bump visible: if a newer grammar fixes the array form, this goes red and
* says so, instead of leaving a wrong note in the plan. (The plan's Phase 0
* also listed the pointer form as failing. It does not fail today.) */
TEST(c_thread_local_grammar_limit_is_pinned_issue963) {
CBMFileResult *ok = do_extract("static _Thread_local int x = 0;\n"
"void f(void) { x = 1; }\n",
CBM_LANG_C, "tls_init.c");
ASSERT_NOT_NULL(ok);
ASSERT_FALSE(ok->parse_incomplete);
cbm_free_result(ok);
CBMFileResult *ptr = do_extract("static _Thread_local int *p;\n"
"void f(void) { p = 0; }\n",
CBM_LANG_C, "tls_ptr.c");
ASSERT_NOT_NULL(ptr);
ASSERT_FALSE(ptr->parse_incomplete);
cbm_free_result(ptr);
CBMFileResult *arr = do_extract("static _Thread_local char b[8];\n"
"void f(void) { b[0] = 0; }\n",
CBM_LANG_C, "tls_arr.c");
ASSERT_NOT_NULL(arr);
ASSERT_TRUE(arr->parse_incomplete);
ASSERT_NOT_NULL(arr->error_ranges);
/* The range names the one broken line, not the whole file. */
ASSERT_STR_EQ("1-1", arr->error_ranges);
/* The clean function below it still reaches the graph. */
ASSERT_TRUE(has_def(arr, "f"));
cbm_free_result(arr);
PASS();
}
SUITE(parse_coverage) {
RUN_TEST(c_ifdef_split_brace_sets_parse_incomplete);
RUN_TEST(c_ifdef_split_brace_neighbors_still_extracted);
@@ -826,4 +869,5 @@ SUITE(parse_coverage) {
RUN_TEST(dockerfile_trailing_blank_at_eof_not_flagged_issue1746);
RUN_TEST(real_error_before_eof_still_flagged_with_trailing_blank_issue1746);
RUN_TEST(width_bearing_error_at_eof_still_flagged_with_trailing_blank_issue1746);
RUN_TEST(c_thread_local_grammar_limit_is_pinned_issue963);
}
+84
View File
@@ -13409,6 +13409,87 @@ TEST(pipeline_markdown_and_config_prose_reaches_fts_body) {
PASS();
}
#if defined(CBM_COVERAGE_MARKER_TEST_API) && CBM_COVERAGE_MARKER_TEST_API
/* Join two Studio Export range strings and hand back the result. The caller
* owns nothing: the string lives in the aggregate's arena, so copy it out
* before the arena goes away. */
static void join_export_ranges(const char *agg_ranges, int agg_regions, const char *part_ranges,
int part_regions, char *out, size_t out_size, int *out_regions) {
CBMFileResult aggregate;
CBMFileResult part;
memset(&aggregate, 0, sizeof(aggregate));
memset(&part, 0, sizeof(part));
cbm_arena_init(&aggregate.arena);
cbm_arena_init(&part.arena);
aggregate.error_ranges = agg_ranges;
aggregate.error_region_count = agg_regions;
aggregate.parse_incomplete = true;
part.error_ranges = part_ranges;
part.error_region_count = part_regions;
part.parse_incomplete = true;
out[0] = '\0';
*out_regions = 0;
if (cbm_pipeline_coverage_marker_test_join(&aggregate, &part)) {
snprintf(out, out_size, "%s", aggregate.error_ranges ? aggregate.error_ranges : "");
*out_regions = aggregate.error_region_count;
}
cbm_arena_destroy(&aggregate.arena);
cbm_arena_destroy(&part.arena);
}
/* Count the "+" characters in a range string. A truncation marker must appear
* once and only at the end: every reader stops at the first token that is not
* a range, so a marker in the middle silently hides every range after it. */
static int count_plus(const char *s) {
int n = 0;
for (const char *p = s; *p; p++) {
if (*p == '+') {
n++;
}
}
return n;
}
TEST(pipeline_objectscript_export_range_join_keeps_one_trailing_marker) {
char joined[256];
int regions = 0;
/* Neither side dropped anything, so nothing invents a marker. */
join_export_ranges("1-2,5-9", 2, "20-24", 1, joined, sizeof(joined), &regions);
ASSERT_STR_EQ("1-2,5-9,20-24", joined);
ASSERT_EQ(0, count_plus(joined));
ASSERT_EQ(3, regions);
/* The first class overran the cap. Its marker must move to the end, so the
* second class's ranges stay visible in front of it. */
join_export_ranges("1-2,5-9,+7", 2, "20-24", 1, joined, sizeof(joined), &regions);
ASSERT_STR_EQ("1-2,5-9,20-24,+7", joined);
ASSERT_EQ(1, count_plus(joined));
/* The second class overran the cap. Same single trailing marker. */
join_export_ranges("1-2", 1, "20-24,+3", 1, joined, sizeof(joined), &regions);
ASSERT_STR_EQ("1-2,20-24,+3", joined);
ASSERT_EQ(1, count_plus(joined));
/* Both overran. One marker, carrying the sum, or the report would
* under-count what it threw away. */
join_export_ranges("1-2,+7", 1, "20-24,+3", 1, joined, sizeof(joined), &regions);
ASSERT_STR_EQ("1-2,20-24,+10", joined);
ASSERT_EQ(1, count_plus(joined));
/* An empty aggregate is the first class in the file. No leading comma. */
join_export_ranges(NULL, 0, "20-24,+3", 1, joined, sizeof(joined), &regions);
ASSERT_STR_EQ("20-24,+3", joined);
ASSERT_EQ(1, regions);
/* A part with nothing to say leaves the aggregate exactly as it was. */
join_export_ranges("1-2,+7", 1, "", 0, joined, sizeof(joined), &regions);
ASSERT_STR_EQ("1-2,+7", joined);
PASS();
}
#endif
SUITE(pipeline) {
RUN_TEST(pipeline_lsp_surface_persisted_and_body_edit_invariant);
/* Index lock */
@@ -13453,6 +13534,9 @@ SUITE(pipeline) {
RUN_TEST(pipeline_objectscript_export_preserves_calls_sequential_parallel);
RUN_TEST(pipeline_objectscript_export_incremental_matches_full_relationships);
RUN_TEST(pipeline_objectscript_export_aggregate_exceeds_arena_block_table);
#if defined(CBM_COVERAGE_MARKER_TEST_API) && CBM_COVERAGE_MARKER_TEST_API
RUN_TEST(pipeline_objectscript_export_range_join_keeps_one_trailing_marker);
#endif
RUN_TEST(pipeline_env_access_configures_sequential_parallel_parity);
RUN_TEST(pipeline_call_reference_sequential_parallel_edge_set_parity);
RUN_TEST(pipeline_incremental_cross_file_call_reference_matches_fresh_full);