diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 8e0d9026..1e6ee13d 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -1869,6 +1869,35 @@ static const char **extract_decorators(CBMArena *a, TSNode node, const char *sou * first and one branch is lost (#495). Fold the cfg predicate into the QN so * each cfg-gated twin gets a DISTINCT, predicate-encoding QN. Returns the * (possibly suffixed) QN; the original QN when no cfg attribute is present. */ +/* Rust: mark a function as a test when it carries a test attribute (#855). + * cbm's test detection is otherwise file-path-based (cbm_is_test_file: + * *_test.rs / test_*), so inline #[test]/#[tokio::test] functions inside a + * regular .rs file are indexed as ordinary Functions (is_test=false) and leak + * past the store.c `is_test != 1` filter into graph/agent context. The + * attribute_item text extract_decorators stores is the bracketed form + * ("#[test]", "#[tokio::test]", "#[tokio::test(...)]", ...). */ +static bool rust_def_is_test(const char *const *decorators) { + if (!decorators) { + return false; + } + for (int i = 0; decorators[i]; i++) { + const char *d = decorators[i]; + /* Path-qualified async/param test macros (substring match, robust to the + * optional argument list and the surrounding #[ ]). */ + if (strstr(d, "tokio::test") || strstr(d, "async_std::test") || + strstr(d, "actix_rt::test") || strstr(d, "test_case::case")) { + return true; + } + /* Bare #[test] / #[test(...)]: match the bracketed path exactly so we do + * NOT match the unrelated #[test_case::case] (handled above) or a + * hypothetical #[test_crate]. */ + if (strstr(d, "#[test]") || strstr(d, "#[test(")) { + return true; + } + } + return false; +} + static const char *rust_cfg_qualified_name(CBMArena *a, const char *base_qn, const char *const *decorators) { if (!decorators) { @@ -3192,6 +3221,7 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec // predicate into the QN so both branches survive the graph upsert (#495). if (ctx->language == CBM_LANG_RUST) { def.qualified_name = rust_cfg_qualified_name(a, def.qualified_name, def.decorators); + def.is_test = rust_def_is_test(def.decorators); } // Docstring diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 3dd54acb..8a264f41 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -3350,6 +3350,46 @@ TEST(walk_defs_no_truncation_over_4096_issue668) { * Suite * ═══════════════════════════════════════════════════════════════════ */ +/* Rust: inline #[test]/#[tokio::test] functions must be marked is_test so the + * store.c `is_test != 1` filter excludes them from graph context. Detection is + * otherwise file-path-based (cbm_is_test_file), so test fns in a regular .rs + * file leak. (#855) */ +TEST(extract_rust_test_attr_marks_is_test_issue855) { + CBMFileResult *r = extract( + "pub fn real_fn() {}\n" + "\n" + "#[test]\n" + "fn sync_test() {}\n" + "\n" + "#[tokio::test]\n" + "async fn async_test() {}\n", + CBM_LANG_RUST, "t", "src/lib.rs"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + + int real = -1, sync = -1, asyn = -1; + for (int i = 0; i < r->defs.count; i++) { + const char *n = r->defs.items[i].name; + if (!n) { + continue; + } + if (strcmp(n, "real_fn") == 0) { + real = r->defs.items[i].is_test ? 1 : 0; + } else if (strcmp(n, "sync_test") == 0) { + sync = r->defs.items[i].is_test ? 1 : 0; + } else if (strcmp(n, "async_test") == 0) { + asyn = r->defs.items[i].is_test ? 1 : 0; + } + } + ASSERT(real >= 0 && sync >= 0 && asyn >= 0 && "all three fns extracted"); + ASSERT(real == 0 && "real_fn is NOT a test"); + ASSERT(sync == 1 && "#[test] fn is_test"); + ASSERT(asyn == 1 && "#[tokio::test] fn is_test"); + + cbm_free_result(r); + PASS(); +} + SUITE(extraction) { /* Initialize extraction library */ cbm_init(); @@ -3597,6 +3637,7 @@ SUITE(extraction) { RUN_TEST(complexity_go_method_receiver_self_recursion); RUN_TEST(complexity_access_depth_and_params); RUN_TEST(walk_defs_no_truncation_over_4096_issue668); + RUN_TEST(extract_rust_test_attr_marks_is_test_issue855); cbm_shutdown(); }