test(pipeline): cover Python-Rust and Rust-TS-JS indexing

tests/test_pipeline.c lacked persisted CALLS coverage for a Python entry point importing PyO3 functions and lacked incremental-versus-fresh oracles for mixed Python/Rust and Rust/TypeScript/JavaScript repositories.

Add pipeline_python_pyo3_import_resolves_rust_function_calls plus two mixed-language incremental tests. The tests require Python edits to publish as CBM_PIPELINE_PUBLISH_INCREMENTAL_EXACT, require Rust and TypeScript scoped-LSP gaps to publish a full rebuild with reason scoped_lsp_gap, and compare each resulting graph with a fresh fast rebuild.

Route setup_lang_repo and setup_incremental_repo through cbm_resolve_cache_dir, reject truncated paths, and remove partially created fixture trees on setup errors. Assertion-aborted fixtures now remain inside the test runner cache lifecycle and are retained only through CBM_TEST_ARTIFACT_DIR.

Verification: all three focused tests passed with the ASan/UBSan runner and the current-source TSan runner. Apple leaks reported 0 leaks for 0 total leaked bytes for each focused test. A real MCP 2025-11-25 trace_path request against a mixed Python/Rust repository returned cli_main -> _run_cli_command and cli_main -> serve_mcp with isError=false and clean EOF shutdown.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-07-18 14:55:27 -04:00
parent f4dbb9a37b
commit ef5f2e3009
+222 -9
View File
@@ -2752,26 +2752,46 @@ TEST(usages_kotlin_no_duplicate_calls) {
static char g_lang_tmpdir[256];
static int setup_lang_repo(const char **filenames, const char **contents, int count) {
snprintf(g_lang_tmpdir, sizeof(g_lang_tmpdir), "/tmp/cbm_lang_XXXXXX");
if (!cbm_mkdtemp(g_lang_tmpdir))
const char *cache = cbm_resolve_cache_dir();
int n = snprintf(g_lang_tmpdir, sizeof(g_lang_tmpdir), "%s/cbm-lang-XXXXXX", cache);
if (n < 0 || (size_t)n >= sizeof(g_lang_tmpdir) || !cbm_mkdtemp(g_lang_tmpdir)) {
g_lang_tmpdir[0] = '\0';
return -1;
}
for (int i = 0; i < count; i++) {
char path[512];
snprintf(path, sizeof(path), "%s/%s", g_lang_tmpdir, filenames[i]);
n = snprintf(path, sizeof(path), "%s/%s", g_lang_tmpdir, filenames[i]);
if (n < 0 || (size_t)n >= sizeof(path)) {
rm_rf(g_lang_tmpdir);
g_lang_tmpdir[0] = '\0';
return -1;
}
/* Create parent directories */
char dir[512];
snprintf(dir, sizeof(dir), "%s", path);
n = snprintf(dir, sizeof(dir), "%s", path);
if (n < 0 || (size_t)n >= sizeof(dir)) {
rm_rf(g_lang_tmpdir);
g_lang_tmpdir[0] = '\0';
return -1;
}
char *slash = strrchr(dir, '/');
if (slash) {
*slash = '\0';
th_mkdir_p(dir);
if (th_mkdir_p(dir) != 0) {
rm_rf(g_lang_tmpdir);
g_lang_tmpdir[0] = '\0';
return -1;
}
}
FILE *f = fopen(path, "wb");
if (!f)
if (!f) {
rm_rf(g_lang_tmpdir);
g_lang_tmpdir[0] = '\0';
return -1;
}
fprintf(f, "%s", contents[i]);
fclose(f);
}
@@ -3059,6 +3079,41 @@ TEST(pipeline_typescript_barrel_reexport_call_resolves_implementation) {
PASS();
}
TEST(pipeline_python_pyo3_import_resolves_rust_function_calls) {
enum { FILE_COUNT = 2 };
const char *files[] = {"native_bridge/src/lib.rs", "python_package/entrypoint.py"};
const char *contents[] = {
"#[pyfunction]\nfn native_execute() -> i32 { 42 }\n\n"
"#[pyfunction]\nfn serve_protocol() {}\n",
"def cli_main():\n"
" from python_package._native import native_execute, serve_protocol\n"
" serve_protocol()\n"
" return native_execute()\n"};
if (setup_lang_repo(files, contents, FILE_COUNT) != 0) {
FAIL("tmpdir");
}
char db[CBM_SZ_512];
int n = snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir);
ASSERT_GT(n, 0);
ASSERT_LT((size_t)n, sizeof(db));
cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
cbm_store_t *s = cbm_store_open_path(db);
ASSERT_NOT_NULL(s);
const char *project = cbm_pipeline_project_name(p);
ASSERT_TRUE(cross_file_call_exists(s, project, "cli_main", "native_execute"));
ASSERT_TRUE(cross_file_call_exists(s, project, "cli_main", "serve_protocol"));
cbm_store_close(s);
cbm_pipeline_free(p);
teardown_lang_repo();
PASS();
}
TEST(pipeline_go_cross_package_call) {
/* Port of TestGoCrossPackageCallViaImport */
const char *files[] = {"main.go", "svc/handler.go"};
@@ -10073,11 +10128,19 @@ static char g_incr_tmpdir[256];
static char g_incr_dbpath[512];
static int setup_incremental_repo(void) {
snprintf(g_incr_tmpdir, sizeof(g_incr_tmpdir), "/tmp/cbm_incr_XXXXXX");
if (!cbm_mkdtemp(g_incr_tmpdir)) {
const char *cache = cbm_resolve_cache_dir();
int n = snprintf(g_incr_tmpdir, sizeof(g_incr_tmpdir), "%s/cbm-incr-XXXXXX", cache);
if (n < 0 || (size_t)n >= sizeof(g_incr_tmpdir) || !cbm_mkdtemp(g_incr_tmpdir)) {
g_incr_tmpdir[0] = '\0';
return -1;
}
n = snprintf(g_incr_dbpath, sizeof(g_incr_dbpath), "%s/test.db", g_incr_tmpdir);
if (n < 0 || (size_t)n >= sizeof(g_incr_dbpath)) {
rm_rf(g_incr_tmpdir);
g_incr_tmpdir[0] = '\0';
g_incr_dbpath[0] = '\0';
return -1;
}
snprintf(g_incr_dbpath, sizeof(g_incr_dbpath), "%s/test.db", g_incr_tmpdir);
char path[512];
FILE *f;
@@ -10086,6 +10149,9 @@ static int setup_incremental_repo(void) {
snprintf(path, sizeof(path), "%s/main.go", g_incr_tmpdir);
f = fopen(path, "w");
if (!f) {
rm_rf(g_incr_tmpdir);
g_incr_tmpdir[0] = '\0';
g_incr_dbpath[0] = '\0';
return -1;
}
fprintf(f, "package main\n\nfunc main() {\n\tHelper()\n}\n");
@@ -10095,6 +10161,9 @@ static int setup_incremental_repo(void) {
snprintf(path, sizeof(path), "%s/helper.go", g_incr_tmpdir);
f = fopen(path, "w");
if (!f) {
rm_rf(g_incr_tmpdir);
g_incr_tmpdir[0] = '\0';
g_incr_dbpath[0] = '\0';
return -1;
}
fprintf(f, "package main\n\nfunc Helper() string {\n\treturn \"hello\"\n}\n");
@@ -14002,6 +14071,147 @@ TEST(incremental_cross_lsp_language_matrix_matches_fresh_rebuild) {
PASS();
}
TEST(incremental_mixed_python_rust_edits_match_fresh_rebuild) {
if (setup_incremental_repo() != 0) {
FAIL("setup failed");
}
char rust_path[CBM_PATH_MAX];
char python_path[CBM_PATH_MAX];
int n = snprintf(rust_path, sizeof(rust_path), "%s/native_bridge.rs", g_incr_tmpdir);
ASSERT(n > 0 && (size_t)n < sizeof(rust_path));
n = snprintf(python_path, sizeof(python_path), "%s/entrypoint.py", g_incr_tmpdir);
ASSERT(n > 0 && (size_t)n < sizeof(python_path));
ASSERT_EQ(th_write_file(rust_path, "#[pyfunction]\nfn native_execute() -> i32 { 1 }\n"), 0);
ASSERT_EQ(th_write_file(python_path, "def cli_main():\n"
" from package._native import native_execute\n"
" return native_execute()\n"),
0);
cbm_config_t *cfg = incremental_test_config(g_incr_tmpdir);
ASSERT_NOT_NULL(cfg);
ASSERT_EQ(cbm_config_set(cfg, CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH,
CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH_EAGER),
0);
cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FAST);
ASSERT_NOT_NULL(p);
cbm_pipeline_apply_config(p, cfg);
ASSERT_EQ(cbm_pipeline_run(p), 0);
char *project = cbm_strdup(cbm_pipeline_project_name(p));
cbm_pipeline_free(p);
ASSERT_NOT_NULL(project);
ASSERT_EQ(th_write_file(python_path, "def python_helper():\n"
" return 2\n\n"
"def cli_main():\n"
" from package._native import native_execute\n"
" return native_execute() + python_helper()\n"),
0);
p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FAST);
ASSERT_NOT_NULL(p);
cbm_pipeline_apply_config(p, cfg);
ASSERT_EQ(cbm_pipeline_run(p), 0);
ASSERT_EQ(cbm_pipeline_publish_kind(p), CBM_PIPELINE_PUBLISH_INCREMENTAL_EXACT);
cbm_pipeline_free(p);
char diff_err[CBM_SZ_8K] = {0};
int diff_rc = pipeline_compare_current_db_to_fresh_fast_rebuild(
g_incr_tmpdir, g_incr_dbpath, project, cfg, diff_err, sizeof(diff_err));
if (diff_rc != 0) {
FAIL(diff_err[0] ? diff_err : "mixed Python/Rust Python edit differed from fresh rebuild");
}
ASSERT_EQ(th_write_file(rust_path, "#[pyfunction]\nfn native_execute() -> i32 { 2 }\n"
"#[pyfunction]\nfn native_extra() -> i32 { 3 }\n"),
0);
p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FAST);
ASSERT_NOT_NULL(p);
cbm_pipeline_apply_config(p, cfg);
ASSERT_EQ(cbm_pipeline_run(p), 0);
ASSERT_EQ(cbm_pipeline_publish_kind(p), CBM_PIPELINE_PUBLISH_FULL);
ASSERT_STR_EQ(cbm_pipeline_publish_reason(p), "scoped_lsp_gap");
cbm_pipeline_free(p);
diff_err[0] = '\0';
diff_rc = pipeline_compare_current_db_to_fresh_fast_rebuild(
g_incr_tmpdir, g_incr_dbpath, project, cfg, diff_err, sizeof(diff_err));
if (diff_rc != 0) {
FAIL(diff_err[0] ? diff_err : "mixed Python/Rust Rust edit differed from fresh rebuild");
}
free(project);
cbm_config_close(cfg);
cleanup_incremental_repo();
PASS();
}
TEST(incremental_mixed_rust_typescript_javascript_matches_fresh_rebuild) {
if (setup_incremental_repo() != 0) {
FAIL("setup failed");
}
char rust_path[CBM_PATH_MAX];
char bridge_path[CBM_PATH_MAX];
char caller_path[CBM_PATH_MAX];
int n = snprintf(rust_path, sizeof(rust_path), "%s/native_core.rs", g_incr_tmpdir);
ASSERT(n > 0 && (size_t)n < sizeof(rust_path));
n = snprintf(bridge_path, sizeof(bridge_path), "%s/bridge.ts", g_incr_tmpdir);
ASSERT(n > 0 && (size_t)n < sizeof(bridge_path));
n = snprintf(caller_path, sizeof(caller_path), "%s/caller.js", g_incr_tmpdir);
ASSERT(n > 0 && (size_t)n < sizeof(caller_path));
ASSERT_EQ(th_write_file(rust_path, "pub fn native_score() -> i32 { 1 }\n"), 0);
ASSERT_EQ(
th_write_file(bridge_path, "export function bridgeOperation(): number { return 1; }\n"), 0);
ASSERT_EQ(th_write_file(caller_path,
"import { bridgeOperation } from './bridge';\n"
"export function javascriptCaller() { return bridgeOperation(); }\n"),
0);
cbm_config_t *cfg = incremental_test_config(g_incr_tmpdir);
ASSERT_NOT_NULL(cfg);
ASSERT_EQ(cbm_config_set(cfg, CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH,
CBM_CONFIG_INCREMENTAL_DERIVED_REFRESH_EAGER),
0);
cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FAST);
ASSERT_NOT_NULL(p);
cbm_pipeline_apply_config(p, cfg);
ASSERT_EQ(cbm_pipeline_run(p), 0);
char *project = cbm_strdup(cbm_pipeline_project_name(p));
cbm_pipeline_free(p);
ASSERT_NOT_NULL(project);
cbm_store_t *store = cbm_store_open_path(g_incr_dbpath);
ASSERT_NOT_NULL(store);
ASSERT_TRUE(cross_file_call_exists(store, project, "javascriptCaller", "bridgeOperation"));
cbm_store_close(store);
ASSERT_EQ(
th_write_file(bridge_path,
"export function bridgeOperation(): number { return 2; }\n"
"export function bridgeExtra(): number { return bridgeOperation(); }\n"),
0);
p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FAST);
ASSERT_NOT_NULL(p);
cbm_pipeline_apply_config(p, cfg);
ASSERT_EQ(cbm_pipeline_run(p), 0);
ASSERT_EQ(cbm_pipeline_publish_kind(p), CBM_PIPELINE_PUBLISH_FULL);
ASSERT_STR_EQ(cbm_pipeline_publish_reason(p), "scoped_lsp_gap");
cbm_pipeline_free(p);
char diff_err[CBM_SZ_8K] = {0};
int diff_rc = pipeline_compare_current_db_to_fresh_fast_rebuild(
g_incr_tmpdir, g_incr_dbpath, project, cfg, diff_err, sizeof(diff_err));
if (diff_rc != 0) {
FAIL(diff_err[0] ? diff_err
: "mixed Rust/TypeScript/JavaScript edit differed from fresh rebuild");
}
free(project);
cbm_config_close(cfg);
cleanup_incremental_repo();
PASS();
}
TEST(incremental_exact_python_receiver_type_gap_matches_full_rebuild) {
enum { PIPELINE_EXACT_ONE_PATH = 1 };
if (setup_incremental_repo() != 0) {
@@ -17467,6 +17677,7 @@ SUITE(pipeline) {
RUN_TEST(pipeline_python_project);
RUN_TEST(pipeline_imports_multi_symbol_edges);
RUN_TEST(pipeline_typescript_barrel_reexport_call_resolves_implementation);
RUN_TEST(pipeline_python_pyo3_import_resolves_rust_function_calls);
RUN_TEST(pipeline_go_cross_package_call);
RUN_TEST(pipeline_python_cross_module_call);
RUN_TEST(pipeline_python_reexport_call_uses_resolved_import_edge);
@@ -17664,6 +17875,8 @@ SUITE(pipeline) {
RUN_TEST(incremental_exact_python_scoped_lsp_gap_matches_full_rebuild);
RUN_TEST(incremental_javascript_scoped_lsp_gap_reports_full_rebuild_not_cap_overflow);
RUN_TEST(incremental_cross_lsp_language_matrix_matches_fresh_rebuild);
RUN_TEST(incremental_mixed_python_rust_edits_match_fresh_rebuild);
RUN_TEST(incremental_mixed_rust_typescript_javascript_matches_fresh_rebuild);
RUN_TEST(incremental_exact_python_receiver_type_gap_matches_full_rebuild);
RUN_TEST(pipeline_persisted_python_defs_feed_scoped_cross_lsp);
RUN_TEST(pipeline_store_backed_lsp_cross_uses_import_scope_defs);