/* * test_pipeline.c — Integration tests for the indexing pipeline. * * Tests pipeline lifecycle, structure pass, and end-to-end indexing * on a temporary directory with known file layout. */ #include "../src/foundation/compat.h" #include "foundation/platform.h" // cbm_normalize_path_sep (drive-canonicalization regression) #include "test_framework.h" #include "test_helpers.h" #include "foundation/mem.h" // cbm_mem_init/budget (back-pressure futile-nap test) #include "pipeline/pipeline.h" #include "lsp/c_lsp.h" #include "lsp/py_lsp.h" #include "pipeline/lsp_surface.h" #include "pipeline/pass_lsp_cross.h" #include "pipeline/pipeline_internal.h" #include "pipeline/helm_template.h" #include "pipeline/artifact.h" #include "store/store.h" #include "semantic/semantic.h" #include "git/git_context.h" #include "foundation/dump_verify.h" #include "foundation/sha256.h" #include "foundation/compat_fs.h" #include "foundation/log.h" #include "foundation/win_utf8.h" // cbm_utf8_to_wide (Windows pipeline_test_set_mtime); no-op elsewhere #include "discover/userconfig.h" #include #include #include #include #include "foundation/compat_thread.h" #include #include #include #include "graph_buffer/graph_buffer.h" #include "yyjson/yyjson.h" #include "sqlite3.h" /* vendored/sqlite3 — PRAGMA integrity_check on dumped DBs */ /* ── Helper: create temp test repo with known layout ───────────── */ static char g_tmpdir[256]; /* Create: * /tmp/cbm_test_XXXXXX/ * main.go (empty) * pkg/ * service.go (empty) * util/ * helper.go (empty) */ static int setup_test_repo(void) { snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/cbm_test_XXXXXX"); if (!cbm_mkdtemp(g_tmpdir)) return -1; char path[512]; /* main.go — calls Serve() from pkg */ snprintf(path, sizeof(path), "%s/main.go", g_tmpdir); FILE *f = fopen(path, "w"); if (!f) return -1; fprintf(f, "package main\n\n" "import \"pkg\"\n\n" "func main() {\n" "\tpkg.Serve()\n" "}\n"); fclose(f); /* pkg/ */ snprintf(path, sizeof(path), "%s/pkg", g_tmpdir); cbm_mkdir(path); /* pkg/service.go — calls Help() from util */ snprintf(path, sizeof(path), "%s/pkg/service.go", g_tmpdir); f = fopen(path, "w"); if (!f) return -1; fprintf(f, "package pkg\n\n" "import \"pkg/util\"\n\n" "func Serve() {\n" "\tutil.Help()\n" "}\n"); fclose(f); /* pkg/util/ */ snprintf(path, sizeof(path), "%s/pkg/util", g_tmpdir); cbm_mkdir(path); /* pkg/util/helper.go */ snprintf(path, sizeof(path), "%s/pkg/util/helper.go", g_tmpdir); f = fopen(path, "w"); if (!f) return -1; fprintf(f, "package util\n\nfunc Help() {}\n"); fclose(f); return 0; } /* Recursive remove (simple, not production-grade) */ static void rm_rf(const char *path) { th_rmtree(path); } static void teardown_test_repo(void) { if (g_tmpdir[0]) rm_rf(g_tmpdir); g_tmpdir[0] = '\0'; } /* ── Lifecycle tests ─────────────────────────────────────────────── */ TEST(pipeline_create_free) { cbm_pipeline_t *p = cbm_pipeline_new("/some/path", NULL, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_STR_EQ(cbm_pipeline_project_name(p), "some-path"); cbm_pipeline_free(p); PASS(); } TEST(pipeline_null_repo) { cbm_pipeline_t *p = cbm_pipeline_new(NULL, NULL, CBM_MODE_FULL); ASSERT_NULL(p); PASS(); } TEST(pipeline_free_null) { cbm_pipeline_free(NULL); /* should not crash */ PASS(); } TEST(pipeline_cancel) { cbm_pipeline_t *p = cbm_pipeline_new("/some/path", NULL, CBM_MODE_FULL); ASSERT_NOT_NULL(p); cbm_pipeline_cancel(p); /* Running a cancelled pipeline should return -1 immediately */ int rc = cbm_pipeline_run(p); /* Note: it may fail because /some/path doesn't exist, not because of cancel. * This test just verifies cancel doesn't crash. */ (void)rc; cbm_pipeline_free(p); PASS(); } TEST(pipeline_cancel_null) { cbm_pipeline_cancel(NULL); /* should not crash */ PASS(); } TEST(pipeline_run_null) { int rc = cbm_pipeline_run(NULL); ASSERT_EQ(rc, -1); PASS(); } TEST(pipeline_discovery_limit_returns_exact_violation_without_publishing) { char *repo = th_mktempdir("cbm_pipeline_discovery_limit"); ASSERT_NOT_NULL(repo); ASSERT_EQ(th_write_file(TH_PATH(repo, "first.c"), "int first;\n"), 0); ASSERT_EQ(th_write_file(TH_PATH(repo, "second.py"), "second = 2\n"), 0); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/index.db", repo); cbm_pipeline_t *pipeline = cbm_pipeline_new(repo, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(pipeline); cbm_index_resource_policy_t policy; cbm_index_policy_init(&policy); policy.max_files = (cbm_index_limit_u64_t){.enabled = true, .value = 1}; cbm_pipeline_set_resource_policy(pipeline, &policy); ASSERT_EQ(cbm_pipeline_run(pipeline), CBM_PIPELINE_RESOURCE_LIMIT); cbm_index_resource_violation_t violation = {0}; cbm_pipeline_get_resource_violation(pipeline, &violation); ASSERT_EQ(violation.resource, CBM_INDEX_RESOURCE_FILES); ASSERT_EQ(violation.observed, 2); ASSERT_EQ(violation.limit, 1); ASSERT_FALSE(cbm_file_exists(db_path)); cbm_pipeline_free(pipeline); th_cleanup(repo); PASS(); } /* ── Focused: file-backed store persistence ─────────────────────── */ TEST(store_file_persistence) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/persist_test.db", g_tmpdir); /* Write data */ cbm_store_t *s1 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s1); cbm_store_upsert_project(s1, "proj", "/tmp"); cbm_node_t n = {.project = "proj", .label = "Function", .name = "foo", .qualified_name = "proj.foo", .file_path = "f.go"}; int64_t id = cbm_store_upsert_node(s1, &n); ASSERT_GT(id, 0); int cnt1 = cbm_store_count_nodes(s1, "proj"); ASSERT_EQ(cnt1, 1); cbm_store_checkpoint(s1); cbm_store_close(s1); /* Reopen and verify */ cbm_store_t *s2 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s2); int cnt2 = cbm_store_count_nodes(s2, "proj"); ASSERT_EQ(cnt2, 1); cbm_store_close(s2); teardown_test_repo(); PASS(); } TEST(store_bulk_persistence) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/bulk_test.db", g_tmpdir); /* Verify: begin_bulk + explicit txn + end_bulk persists to file */ cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); cbm_store_upsert_project(s, "proj", "/tmp"); cbm_store_begin_bulk(s); cbm_store_begin(s); cbm_node_t n = {.project = "proj", .label = "Function", .name = "foo", .qualified_name = "proj.foo", .file_path = "f.go"}; int64_t id = cbm_store_upsert_node(s, &n); ASSERT_GT(id, 0); ASSERT_EQ(cbm_store_commit(s), 0); cbm_store_end_bulk(s); cbm_store_checkpoint(s); cbm_store_close(s); /* Reopen and verify data survived */ cbm_store_t *s2 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s2); ASSERT_EQ(cbm_store_count_nodes(s2, "proj"), 1); cbm_store_close(s2); teardown_test_repo(); PASS(); } static void write_temp_file(const char *dir, const char *name, const char *content); /* EVERY proto service gets its Routes, not the first 64 the graph buffer * happened to hold. * * create_grpc_routes() collected services into a fixed `services[CBM_SZ_64]` * and stopped there, in graph-buffer insertion order — which is parallel * worker merge order. So on a repo with more than 64 services the extras were * silently dropped, and WHICH ones were dropped changed between runs of the * same binary: two kubernetes indexes differed by 5 Routes one way and 4 the * other, plus their HANDLES edges (2026-09-18). 70 services here, so the old * cap fails this test whatever order the workers produce. */ TEST(pipeline_grpc_routes_cover_every_service_past_the_old_cap) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_grpc_cap_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); enum { SERVICES = 70 }; /* > the 64 the fixed array used to hold */ for (int i = 0; i < SERVICES; i++) { char name[64]; char body[512]; snprintf(name, sizeof(name), "svc%02d.proto", i); snprintf(body, sizeof(body), "syntax = \"proto3\";\npackage demo%02d;\n\n" "message Req%02d { string id = 1; }\n" "message Resp%02d { string ok = 1; }\n\n" "service Svc%02d {\n" " rpc Ping (Req%02d) returns (Resp%02d);\n}\n", i, i, i, i, i, i); write_temp_file(tmp, name, body); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/grpc.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); cbm_node_t *routes = NULL; int route_count = 0; ASSERT_EQ(cbm_store_find_nodes_by_label(s, project, "Route", &routes, &route_count), CBM_STORE_OK); int grpc_routes = 0; for (int i = 0; i < route_count; i++) { if (routes[i].qualified_name && strncmp(routes[i].qualified_name, "__grpc__", strlen("__grpc__")) == 0) { grpc_routes++; } } cbm_store_free_nodes(routes, route_count); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); /* One rpc per service: every service must be represented. The old cap * produced at most 64 however the workers interleaved. */ ASSERT_EQ(grpc_routes, SERVICES); PASS(); } /* Pipeline-level: the doclinks pass runs as part of a real index and its * REFERENCES_FILE edges land in the store, not just in a unit test that * calls cbm_pipeline_pass_doclinks() directly. Registration-list gap: every * doclinks unit test calls the pass function by hand, so deleting its * {predump_doclinks, "doclinks", false} entry in pipeline.c would leave * every one of them green. */ TEST(pipeline_doclinks_edge_lands_in_store) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_doclinks_pipeline_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "README.md", "See [main](src/main.go) for the entry point.\n"); write_temp_file(tmp, "src/main.go", "package main\n\nfunc main() {}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/doclinks.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); ASSERT_GT(cbm_store_count_edges_by_type(s, project, "REFERENCES_FILE"), 0); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* #1735: every node and edge of a stored index, one sorted line each. */ static void graph_listing_append(sqlite3 *db, const char *sql, const char *project, char **buf, size_t *len) { sqlite3_stmt *stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { return; } sqlite3_bind_text(stmt, 1, project, -1, SQLITE_TRANSIENT); while (sqlite3_step(stmt) == SQLITE_ROW) { const char *row = (const char *)sqlite3_column_text(stmt, 0); size_t n = row ? strlen(row) : 0; char *grown = realloc(*buf, *len + n + 2); if (!grown) { break; } *buf = grown; memcpy(*buf + *len, row ? row : "", n); (*buf)[*len + n] = '\n'; *len += n + 1; (*buf)[*len] = '\0'; } sqlite3_finalize(stmt); } static char *graph_listing(const char *db_path, const char *project) { sqlite3 *db = NULL; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) { sqlite3_close(db); return NULL; } char *buf = calloc(1, 1); size_t len = 0; graph_listing_append(db, "SELECT label||'|'||name||'|'||qualified_name||'|'||file_path||'|'||" "start_line||'|'||end_line||'|'||properties FROM nodes " "WHERE project=?1 ORDER BY 1", project, &buf, &len); graph_listing_append(db, "SELECT e.type||'|'||s.qualified_name||'|'||t.qualified_name||'|'||" "e.properties FROM edges e JOIN nodes s ON s.id=e.source_id " "JOIN nodes t ON t.id=e.target_id WHERE e.project=?1 ORDER BY 1", project, &buf, &len); sqlite3_close(db); return buf; } static char *index_and_list(const char *repo, const char *db_name) { char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/%s", repo, db_name); cbm_pipeline_t *p = cbm_pipeline_new(repo, db_path, CBM_MODE_FULL); if (!p) { return NULL; } char *listing = cbm_pipeline_run(p) == 0 ? graph_listing(db_path, cbm_pipeline_project_name(p)) : NULL; cbm_pipeline_free(p); return listing; } /* True if every line of `sub` is a line of `super`. */ static bool listing_lines_within(const char *sub, const char *super) { for (const char *line = sub; *line;) { const char *nl = strchr(line, '\n'); size_t n = nl ? (size_t)(nl - line) : strlen(line); bool found = false; for (const char *p = super; *p && !found;) { const char *pnl = strchr(p, '\n'); size_t pn = pnl ? (size_t)(pnl - p) : strlen(p); found = pn == n && memcmp(p, line, n) == 0; p = pnl ? pnl + 1 : p + pn; } if (!found) { fprintf(stderr, " missing from the cut index: %.*s\n", (int)n, line); return false; } line = nl ? nl + 1 : line + n; } return true; } /* A small dump: two tables, a view, three INSERTs of 200 rows, each with one * subquery row and one function-call row among literal rows. */ static char *sql_dump_small(bool mysql_esc) { size_t cap = 256 * 1024; char *src = malloc(cap); if (!src) { return NULL; } const char *quoted = mysql_esc ? "(%d,'O\\'Brien',DEFAULT)" : "(%d,'O''Brien',DEFAULT)"; size_t len = (size_t)snprintf(src, cap, "CREATE TABLE city (ID int, Name char(35), Population int);\n" "CREATE TABLE country (Code char(3), Name char(52));\n" "CREATE VIEW big_cities AS SELECT Name FROM city " "WHERE Population > 1000000;\n"); for (int s = 0; s < 3; s++) { len += (size_t)snprintf(src + len, cap - len, "INSERT INTO `city` VALUES "); for (int r = 0; r < 200; r++) { if (r) { src[len++] = ','; } int id = s * 1000 + r; if (r == 100) { len += (size_t)snprintf(src + len, cap - len, "((SELECT MAX(Code) FROM country),'x',1)"); } else if (r == 101) { len += (size_t)snprintf(src + len, cap - len, "(%d,UPPER('y'),2)", id); } else if (r % 3) { len += (size_t)snprintf(src + len, cap - len, quoted, id); } else { len += (size_t)snprintf(src + len, cap - len, "(%d,_binary 'ab',NULL)", id); } } len += (size_t)snprintf(src + len, cap - len, ";\n"); } return src; } /* #1735: leaving a dump's literal INSERT rows out of the parse must not change * the graph. The same repository is indexed twice — once as shipped and once * with the full parse (test seam) — and the stored nodes and edges compared. * Standard '' escapes, which the SQL grammar reads correctly: identical. * MySQL \' escapes, which it misreads: the full parse's error recovery swallows * neighbouring rows, so the cut index must hold everything the full one has * (and may hold more — the rows the full parse lost). */ TEST(pipeline_sql_dump_graph_matches_the_full_parse_issue1735) { for (int esc = 0; esc < 2; esc++) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_sql_dump_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); char *src = sql_dump_small(esc == 1); ASSERT_NOT_NULL(src); write_temp_file(tmp, "dump.sql", src); free(src); char *cut = index_and_list(tmp, "cut.db"); cbm_setenv("CBM_TEST_SQL_FULL_PARSE_ON", "dump.sql", 1); char *full = index_and_list(tmp, "full.db"); cbm_unsetenv("CBM_TEST_SQL_FULL_PARSE_ON"); th_rmtree(tmp); ASSERT_NOT_NULL(cut); ASSERT_NOT_NULL(full); /* Not vacuous: the tables, the view and its lineage are there. */ ASSERT_NOT_NULL(strstr(cut, "Table|city|")); ASSERT_NOT_NULL(strstr(cut, "View|big_cities|")); ASSERT_NOT_NULL(strstr(cut, "USAGE|")); if (esc == 0 && strcmp(cut, full) != 0) { fprintf(stderr, "--- cut ---\n%s--- full ---\n%s", cut, full); } if (esc == 0) { ASSERT_STR_EQ(cut, full); } else { ASSERT_TRUE(listing_lines_within(full, cut)); } free(cut); free(full); } PASS(); } /* Spilling must be invisible in the OUTPUT: the same repository indexed with * results parked on disk must produce the same graph as one indexed entirely in * memory. It did not. The namespace map that `use`/`using`/package imports * resolve through was built from the in-memory result cache, where a PARKED * result is NULL, so every spilled file contributed no namespace and its * imports fell through to the looser fallback. Measured on the php corpus * (2026-09-18, reproducible 3/3 each way, and present on main): 57,182 edges in * memory against 59,379 while spilling — differing in BOTH directions, because * a file missing from that map does not fail to resolve, it resolves * differently. * * Why this needs its own fixture: test_parallel's spill-parity test uses the * compact parity harness, which omits the production pass that builds the map, * and its repo is Go+Java — neither reaches the namespace path. This one runs * the REAL pipeline over >MIN_FILES_FOR_PARALLEL files so the parallel route is * taken, with namespaced PHP that imports across files. */ TEST(pipeline_spill_resolves_namespace_imports_like_memory) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_spill_ns_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); /* 60 modules: each declares a namespace and the next one imports it, so * every file both contributes to the namespace map and depends on it. */ enum { NS_FILES = 60 }; for (int i = 0; i < NS_FILES; i++) { char name[64]; char body[512]; snprintf(name, sizeof(name), "Mod%02d.php", i); snprintf(body, sizeof(body), "run(); }\n}\n", i, (i + 1) % NS_FILES, (i + 1) % NS_FILES, i, (i + 1) % NS_FILES); write_temp_file(tmp, name, body); } int counts[2] = {0, 0}; int imports[2] = {0, 0}; for (int pass = 0; pass < 2; pass++) { char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/ns%d.db", tmp, pass); if (pass == 1) { cbm_setenv("CBM_MEM_SPILL", "1", 1); /* park every result on disk */ } cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); if (pass == 1) { cbm_unsetenv("CBM_MEM_SPILL"); } ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); counts[pass] = cbm_store_count_edges(s, project); imports[pass] = cbm_store_count_edges_by_type(s, project, "IMPORTS"); cbm_store_close(s); cbm_pipeline_free(p); } th_rmtree(tmp); /* The imports must actually have resolved, or this compares 0 == 0. */ ASSERT_GT(imports[0], 0); ASSERT_EQ(imports[0], imports[1]); ASSERT_EQ(counts[0], counts[1]); PASS(); } /* ── Integration: structure pass on temp repo ────────────────────── */ TEST(pipeline_structure_nodes) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); /* Verify results by opening the store */ cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Should have nodes: 1 Project + 2 Folders + 3 Files + N Functions */ int node_count = cbm_store_count_nodes(s, project); ASSERT_GTE(node_count, 9); /* 6 structure + at least 3 definitions */ /* Verify project node exists */ cbm_node_t proj_node = {0}; rc = cbm_store_find_node_by_qn(s, project, project, &proj_node); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_STR_EQ(proj_node.label, "Project"); cbm_node_free_fields(&proj_node); /* Verify folder nodes */ cbm_node_t *folders = NULL; int folder_count = 0; rc = cbm_store_find_nodes_by_label(s, project, "Folder", &folders, &folder_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_GTE(folder_count, 2); /* pkg, pkg/util */ cbm_store_free_nodes(folders, folder_count); /* Verify file nodes */ cbm_node_t *file_nodes = NULL; int file_count = 0; rc = cbm_store_find_nodes_by_label(s, project, "File", &file_nodes, &file_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_GTE(file_count, 3); /* main.go, service.go, helper.go */ cbm_store_free_nodes(file_nodes, file_count); /* Verify edges exist */ int edge_count = cbm_store_count_edges(s, project); ASSERT_GTE(edge_count, 5); /* CONTAINS_FOLDER + CONTAINS_FILE edges */ cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } /* Issue #516: an ADR stored via manage_adr (project_summaries) must survive a * full re-index. A full re-index deletes the DB and rebuilds it from the graph * buffer, which writes an empty project_summaries table; the fix captures the * ADR before the delete and restores it after the rebuild. Reproduce-first: * index, store an ADR, force a full re-index by adding files, assert the ADR * is still present and unchanged. */ /* #1665: a persistence artifact export failure must not be reported as a * repo_path problem. Create the artifact directory as a regular FILE so * cbm_mkdir_p(.codebase-memory) fails deterministically (works as root too), * run with persistence enabled, and assert: the run fails, the DB was * published (publish happens before export), the pipeline snapshots the export * error, and a clean re-run leaves the snapshot empty. */ TEST(pipeline_export_error_snapshot_on_artifact_failure) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_export1665_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("failed to create temp dir"); } char path[512]; snprintf(path, sizeof(path), "%s/main.py", tmp); FILE *f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "def foo():\n pass\n"); fclose(f); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test.db", tmp); /* Block the artifact directory: a FILE at .codebase-memory makes the * export's mkdir_p fail with a deterministic error on every platform. */ char art_block[512]; snprintf(art_block, sizeof(art_block), "%s/.codebase-memory", tmp); f = fopen(art_block, "w"); ASSERT_NOT_NULL(f); fprintf(f, "block\n"); fclose(f); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); cbm_pipeline_set_persistence(p, true); int rc = cbm_pipeline_run(p); ASSERT_NEQ(rc, 0); /* The export runs after publish, so the DB must exist despite the failure. */ struct stat db_st; ASSERT_EQ(stat(db_path, &db_st), 0); const char *export_error = cbm_pipeline_export_error(p); ASSERT_NOT_NULL(export_error); ASSERT_TRUE(export_error[0] != '\0'); ASSERT_NOT_NULL(strstr(export_error, "prepare_artifact_dir")); cbm_pipeline_free(p); /* Control: remove the blocker; a fresh run succeeds and leaves no snapshot. */ (void)cbm_unlink(art_block); cbm_pipeline_t *p2 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p2); cbm_pipeline_set_persistence(p2, true); rc = cbm_pipeline_run(p2); ASSERT_EQ(rc, 0); const char *export_error2 = cbm_pipeline_export_error(p2); ASSERT_NOT_NULL(export_error2); ASSERT_TRUE(export_error2[0] == '\0'); cbm_pipeline_free(p2); rm_rf(tmp); PASS(); } TEST(pipeline_adr_survives_full_reindex) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_adr_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test.db", tmp); /* Initial index with a single source file. */ char path[512]; snprintf(path, sizeof(path), "%s/main.py", tmp); FILE *f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "def foo():\n pass\n"); fclose(f); cbm_pipeline_t *p1 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p1); ASSERT_EQ(cbm_pipeline_run(p1), 0); const char *project = cbm_pipeline_project_name(p1); char project_copy[256]; snprintf(project_copy, sizeof(project_copy), "%s", project); cbm_pipeline_free(p1); /* Store an ADR. */ const char *adr_text = "# Decision\nWe chose X over Y."; cbm_store_t *s1 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s1); ASSERT_EQ(cbm_store_adr_store(s1, project_copy, adr_text), CBM_STORE_OK); cbm_store_close(s1); /* Force a full re-index: add enough files to exceed the incremental * threshold so the DB is deleted and rebuilt. */ for (int i = 0; i < 4; i++) { snprintf(path, sizeof(path), "%s/extra%d.py", tmp, i); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "def g%d():\n return %d\n", i, i); fclose(f); } cbm_pipeline_t *p2 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p2); ASSERT_EQ(cbm_pipeline_run(p2), 0); cbm_pipeline_free(p2); /* The ADR must still be present and unchanged. */ cbm_store_t *s2 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s2); cbm_adr_t adr = {0}; int rc = cbm_store_adr_get(s2, project_copy, &adr); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_NOT_NULL(adr.content); ASSERT_STR_EQ(adr.content, adr_text); cbm_store_adr_free(&adr); cbm_store_close(s2); rm_rf(tmp); PASS(); } TEST(pipeline_structure_edges) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Check CONTAINS_FILE edges */ int cf_count = cbm_store_count_edges_by_type(s, project, "CONTAINS_FILE"); /* Check CONTAINS_FOLDER edges */ int cd_count = cbm_store_count_edges_by_type(s, project, "CONTAINS_FOLDER"); /* Cleanup before assertions (so failures don't leak) */ cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); ASSERT_GTE(cf_count, 3); /* project->main.go, pkg->service.go, util->helper.go */ ASSERT_GTE(cd_count, 1); /* project->pkg (pkg->util may merge on some platforms) */ PASS(); } TEST(pipeline_branch_root_structure) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_branch_root.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); char branch_qn[1024]; snprintf(branch_qn, sizeof(branch_qn), "%s.__branch__.working-tree", project); cbm_node_t project_node = {0}; cbm_node_t branch_node = {0}; cbm_node_t root_file_node = {0}; cbm_node_t root_folder_node = {0}; rc = cbm_store_find_node_by_qn(s, project, project, &project_node); ASSERT_EQ(rc, CBM_STORE_OK); rc = cbm_store_find_node_by_qn(s, project, branch_qn, &branch_node); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_STR_EQ(branch_node.label, "Branch"); ASSERT_STR_EQ(branch_node.name, "working-tree"); ASSERT_NOT_NULL(strstr(branch_node.properties_json, "\"is_git\":false")); char *root_folder_qn = cbm_pipeline_fqn_folder(project, "pkg"); char *root_file_qn = cbm_pipeline_fqn_compute(project, "main.go", "__file__"); ASSERT_NOT_NULL(root_folder_qn); ASSERT_NOT_NULL(root_file_qn); rc = cbm_store_find_node_by_qn(s, project, root_folder_qn, &root_folder_node); ASSERT_EQ(rc, CBM_STORE_OK); rc = cbm_store_find_node_by_qn(s, project, root_file_qn, &root_file_node); ASSERT_EQ(rc, CBM_STORE_OK); cbm_edge_t *has_branch = NULL; int has_branch_count = 0; rc = cbm_store_find_edges_by_source_type(s, project_node.id, "HAS_BRANCH", &has_branch, &has_branch_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(has_branch_count, 1); ASSERT_EQ(has_branch[0].target_id, branch_node.id); cbm_edge_t *project_files = NULL; int project_file_count = 0; rc = cbm_store_find_edges_by_source_type(s, project_node.id, "CONTAINS_FILE", &project_files, &project_file_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(project_file_count, 0); cbm_edge_t *branch_files = NULL; int branch_file_count = 0; rc = cbm_store_find_edges_by_source_type(s, branch_node.id, "CONTAINS_FILE", &branch_files, &branch_file_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(branch_file_count, 1); ASSERT_EQ(branch_files[0].target_id, root_file_node.id); cbm_edge_t *branch_folders = NULL; int branch_folder_count = 0; rc = cbm_store_find_edges_by_source_type(s, branch_node.id, "CONTAINS_FOLDER", &branch_folders, &branch_folder_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_EQ(branch_folder_count, 1); ASSERT_EQ(branch_folders[0].target_id, root_folder_node.id); cbm_store_free_edges(has_branch, has_branch_count); cbm_store_free_edges(project_files, project_file_count); cbm_store_free_edges(branch_files, branch_file_count); cbm_store_free_edges(branch_folders, branch_folder_count); cbm_node_free_fields(&project_node); cbm_node_free_fields(&branch_node); cbm_node_free_fields(&root_file_node); cbm_node_free_fields(&root_folder_node); free(root_folder_qn); free(root_file_qn); cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } TEST(pipeline_project_name_derived) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, NULL, CBM_MODE_FULL); ASSERT_NOT_NULL(p); /* Project name should be derived from tmpdir path */ const char *name = cbm_pipeline_project_name(p); ASSERT_NOT_NULL(name); ASSERT_TRUE(strlen(name) > 0); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } TEST(pipeline_fast_mode) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_fast.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FAST); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); /* Just verify it completes without error in fast mode */ cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); int node_count = cbm_store_count_nodes(s, project); ASSERT_GT(node_count, 0); cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } /* ── Definitions pass tests ──────────────────────────────────────── */ TEST(pipeline_definitions_function_nodes) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_defs.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Verify Function nodes extracted from Go source files */ cbm_node_t *funcs = NULL; int func_count = 0; rc = cbm_store_find_nodes_by_label(s, project, "Function", &funcs, &func_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_GTE(func_count, 3); /* main, Serve, Help */ /* Verify each expected function exists */ int found_main = 0, found_serve = 0, found_help = 0; for (int i = 0; i < func_count; i++) { if (strcmp(funcs[i].name, "main") == 0) found_main = 1; else if (strcmp(funcs[i].name, "Serve") == 0) found_serve = 1; else if (strcmp(funcs[i].name, "Help") == 0) found_help = 1; } ASSERT_TRUE(found_main); ASSERT_TRUE(found_serve); ASSERT_TRUE(found_help); cbm_store_free_nodes(funcs, func_count); cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } TEST(pipeline_definitions_defines_edges) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_defs_edges.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* DEFINES edges: File → Function (one per extracted definition) */ int defines_count = cbm_store_count_edges_by_type(s, project, "DEFINES"); ASSERT_GTE(defines_count, 3); /* main.go→main, service.go→Serve, helper.go→Help */ /* CONTAINS_FILE edges should still exist from structure pass */ int cf_count = cbm_store_count_edges_by_type(s, project, "CONTAINS_FILE"); ASSERT_GTE(cf_count, 3); cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } TEST(pipeline_definitions_properties) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_defs_props.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Verify a function has valid properties (complexity, lines, etc.) */ cbm_node_t *funcs = NULL; int func_count = 0; rc = cbm_store_find_nodes_by_label(s, project, "Function", &funcs, &func_count); ASSERT_EQ(rc, CBM_STORE_OK); ASSERT_GT(func_count, 0); /* Check that Serve (exported) has is_exported:true in properties */ for (int i = 0; i < func_count; i++) { if (strcmp(funcs[i].name, "Serve") == 0) { ASSERT_NOT_NULL(funcs[i].properties_json); ASSERT_TRUE(strstr(funcs[i].properties_json, "\"is_exported\":true") != NULL); } /* All functions should have file_path set */ ASSERT_NOT_NULL(funcs[i].file_path); } cbm_store_free_nodes(funcs, func_count); cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } /* Node properties must remain VALID JSON even when a definition's serialized * properties exceed the fixed 2 KB build buffer. Found on the Linux kernel: * 135 nodes (50-param functions with struct-typed signatures) had properties * truncated mid-string at 2047 bytes — malformed JSON that aborts EVERY * json_extract()-based consumer (arch_entry_points, partial indexes, user * Cypher on properties). Oversized optional fields must be dropped whole, * never cut mid-value. */ TEST(pipeline_def_props_valid_json_when_oversized) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } /* Sweep of C functions with growing signatures. The corruption fires only * when the serialized position lands in a narrow window just under the * buffer margin as the param_types array starts — the array is then cut * mid-item (`"param_types":["enum`), exactly the kernel failure shape. * Sweeping 10..69 params deterministically hits the window (pre-fix: * sweep_fn_30 -> 2047-byte malformed properties). ONE FUNCTION PER FILE so * the file count exceeds MIN_FILES_FOR_PARALLEL (50) and the test covers * the PARALLEL pipeline's duplicated props builder (pass_parallel.c) — the * path large repos take; the serial twin lives in pass_definitions.c. */ for (int n = 10; n < 70; n++) { char path[512]; snprintf(path, sizeof(path), "%s/sweep_%02d.c", g_tmpdir, n); FILE *f = fopen(path, "w"); if (!f) { teardown_test_repo(); FAIL("failed to write sweep file"); } fprintf(f, "int sweep_fn_%02d(", n); for (int i = 0; i < n; i++) { fprintf(f, "%sstruct long_struct_type_name_padding_padding_%02d *par_%02d", i ? ", " : "", i, i); } fprintf(f, ") { return 0; }\n"); fclose(f); } /* Multi-line parameter declarations: param_types items then carry raw * newline/tab bytes from the source slice. The array appender's inline * escape loop only handled quote/backslash, so the raw control bytes made * the JSON invalid (118 Linux-kernel rows of this shape). */ { char path[512]; snprintf(path, sizeof(path), "%s/sweep_nl.c", g_tmpdir); FILE *f = fopen(path, "w"); if (!f) { teardown_test_repo(); FAIL("failed to write sweep_nl.c"); } fprintf(f, "int sweep_fn_nl(struct\n\t\t\t\treally_long_struct_name *a,\n" " enum\n\tweird_enum b) { return 0; }\n"); fclose(f); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_huge_props.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); cbm_node_t *funcs = NULL; int func_count = 0; ASSERT_EQ(cbm_store_find_nodes_by_label(s, project, "Function", &funcs, &func_count), CBM_STORE_OK); int checked = 0; for (int i = 0; i < func_count; i++) { if (strncmp(funcs[i].name, "sweep_fn_", 9) != 0) { continue; } ASSERT_NOT_NULL(funcs[i].properties_json); yyjson_doc *doc = yyjson_read(funcs[i].properties_json, strlen(funcs[i].properties_json), 0); if (!doc) { printf(" INVALID properties JSON for %s (%zu bytes): ...%s\n", funcs[i].name, strlen(funcs[i].properties_json), funcs[i].properties_json + (strlen(funcs[i].properties_json) > 60 ? strlen(funcs[i].properties_json) - 60 : 0)); } ASSERT_NOT_NULL(doc); /* valid JSON for EVERY sweep size */ yyjson_doc_free(doc); checked++; } ASSERT_EQ(checked, 61); /* all sweep functions present (60 sizes + nl case) */ cbm_store_free_nodes(funcs, func_count); cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } /* Edge properties must be VALID JSON. Decorator source text (quotes, raw * newlines: @register.tag("block"), multi-line @override_settings) was * interpolated raw into the DECORATES properties — django produced 3826 * malformed edges, and any json_extract-based consumer (including the * url_path_gen generated-column evaluation during PRAGMA integrity_check) * aborts on them. Usage/call emit sites had the same hole for sliced source * text. */ TEST(pipeline_edge_props_valid_json) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char path[512]; snprintf(path, sizeof(path), "%s/deco.py", g_tmpdir); FILE *f = fopen(path, "w"); if (!f) { teardown_test_repo(); FAIL("failed to write deco.py"); } fprintf(f, "from x import register\n" "@register.tag(\"block\")\n" "def do_block(parser):\n" " return parser\n" "@register.tag(\"extends\")\n" "def do_extends(parser):\n" " return parser\n"); fclose(f); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_edge_props.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); cbm_edge_t *edges = NULL; int edge_count = 0; ASSERT_EQ(cbm_store_find_edges_by_type(s, project, "DECORATES", &edges, &edge_count), CBM_STORE_OK); ASSERT_GT(edge_count, 0); /* the decorators must produce DECORATES edges */ for (int i = 0; i < edge_count; i++) { const char *pj = edges[i].properties_json; if (!pj) { continue; } yyjson_doc *doc = yyjson_read(pj, strlen(pj), 0); if (!doc) { printf(" INVALID edge properties JSON: %.80s\n", pj); } ASSERT_NOT_NULL(doc); yyjson_doc_free(doc); } cbm_store_free_edges(edges, edge_count); cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } /* ── Calls pass tests ──────────────────────────────────────────── */ TEST(pipeline_calls_resolution) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_calls.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* main() calls pkg.Serve(), Serve() calls util.Help() → at least 2 CALLS edges */ int calls_count = cbm_store_count_edges_by_type(s, project, "CALLS"); ASSERT_GTE(calls_count, 1); /* at least some calls resolved */ cbm_store_close(s); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } /* True iff a CALLS edge exists from a node named src_name to a node named * tgt_name. Used to assert cross-file call resolution survives a reindex. */ static bool cross_file_edge_exists(cbm_store_t *s, const char *project, const char *src_name, const char *tgt_name, const char *edge_type) { cbm_node_t *srcs = NULL; cbm_node_t *tgts = NULL; int sc = 0; int tc = 0; cbm_store_find_nodes_by_name(s, project, src_name, &srcs, &sc); cbm_store_find_nodes_by_name(s, project, tgt_name, &tgts, &tc); bool found = false; for (int i = 0; i < sc && !found; i++) { cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(s, srcs[i].id, edge_type, &edges, &ec); for (int j = 0; j < ec && !found; j++) { for (int k = 0; k < tc; k++) { if (edges[j].target_id == tgts[k].id) { found = true; break; } } } if (edges) { cbm_store_free_edges(edges, ec); } } if (srcs) { cbm_store_free_nodes(srcs, sc); } if (tgts) { cbm_store_free_nodes(tgts, tc); } return found; } static bool cross_file_call_exists(cbm_store_t *s, const char *project, const char *src_name, const char *tgt_name) { return cross_file_edge_exists(s, project, src_name, tgt_name, "CALLS"); } /* True iff the exact named CALLS edge exists and its serialized strategy * contains `strategy_fragment`. Parallel synthetic-carrier regressions use * this on a separate ordinary-call control: it proves the cross-file LSP ran * successfully for the language/target, rather than letting a registry-only * edge make a broken fixture look healthy. */ static bool cross_file_call_has_strategy(cbm_store_t *s, const char *project, const char *src_name, const char *tgt_name, const char *strategy_fragment) { cbm_node_t *srcs = NULL; cbm_node_t *tgts = NULL; int sc = 0; int tc = 0; cbm_store_find_nodes_by_name(s, project, src_name, &srcs, &sc); cbm_store_find_nodes_by_name(s, project, tgt_name, &tgts, &tc); bool found = false; for (int i = 0; i < sc && !found; i++) { cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(s, srcs[i].id, "CALLS", &edges, &ec); for (int j = 0; j < ec && !found; j++) { for (int k = 0; k < tc; k++) { if (edges[j].target_id == tgts[k].id && edges[j].properties_json && strstr(edges[j].properties_json, strategy_fragment)) { found = true; break; } } } if (edges) { cbm_store_free_edges(edges, ec); } } if (srcs) { cbm_store_free_nodes(srcs, sc); } if (tgts) { cbm_store_free_nodes(tgts, tc); } return found; } /* Nix attrpath qualification, end to end. A call inside a scoped binding must * source to the QUALIFIED definition. * * This has to be a pipeline test rather than an extraction one. Definition QNs * (extract_defs.c) and call-scope QNs (extract_unified.c) are computed by two * separate functions. If they disagree by even one segment, the CALLS edge names * a source node that was never minted and is dropped at write — with no error, * and with every extraction-level assertion still passing. Only the store sees it. * * Both callers below are scoped, one by an enclosing attrset and one by a dotted * attrpath, so this covers both routes into the qualified name. */ TEST(pipeline_nix_scoped_binding_calls_resolve) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char nix_path[512]; snprintf(nix_path, sizeof(nix_path), "%s/lib.nix", g_tmpdir); FILE *nf = fopen(nix_path, "w"); if (!nf) { teardown_test_repo(); FAIL("failed to write nix fixture"); } fprintf(nf, "{ prelude }:\n" "let\n" " nixTarget = t: t + 1;\n" "in\n" "{\n" " setA = { nixCaller = x: nixTarget x; };\n" " outer.inner.nixDeepCaller = y: nixTarget y;\n" "}\n"); fclose(nf); char nix_db[512]; snprintf(nix_db, sizeof(nix_db), "%s/test_nix_calls.db", g_tmpdir); cbm_pipeline_t *np = cbm_pipeline_new(g_tmpdir, nix_db, CBM_MODE_FULL); ASSERT_NOT_NULL(np); ASSERT_EQ(cbm_pipeline_run(np), 0); cbm_store_t *ns = cbm_store_open_path(nix_db); ASSERT_NOT_NULL(ns); const char *nix_project = cbm_pipeline_project_name(np); /* Scoped by an enclosing attrset: the def QN is proj.lib.setA.nixCaller. */ ASSERT(cross_file_call_exists(ns, nix_project, "nixCaller", "nixTarget")); /* Scoped by a dotted attrpath: proj.lib.outer.inner.nixDeepCaller. */ ASSERT(cross_file_call_exists(ns, nix_project, "nixDeepCaller", "nixTarget")); cbm_store_close(ns); cbm_pipeline_free(np); teardown_test_repo(); PASS(); } /* Terraform reference resolution, end to end. * * An HCL block names itself with its labels appended -- find_hcl_block_name * mints "resource.aws_instance.web" -- and the module resolver * (pass_terraform.c) finds the block a reference `aws_instance.web.id` names * by recomputing that QN from the declaring file and the name. The reference * USAGEs it from the block holding it, `resource.aws_eip.ip`; the file itself * USAGEs nothing (a .tf file is no longer served by the generic by-name index). * * This has to be a pipeline test: the extractor mints the name and the * resolver recomputes the QN, so the two halves can disagree with every * extraction-level assertion still green. */ TEST(pipeline_hcl_block_reference_resolves_to_its_block) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char tf_path[512]; snprintf(tf_path, sizeof(tf_path), "%s/main.tf", g_tmpdir); FILE *tf = fopen(tf_path, "w"); if (!tf) { teardown_test_repo(); FAIL("failed to write terraform fixture"); } fprintf(tf, "resource \"aws_instance\" \"web\" {\n" " ami = \"ami-0c55b159cbfafe1f0\"\n" " instance_type = \"t2.micro\"\n" "}\n" "\n" "resource \"aws_eip\" \"ip\" {\n" " instance = aws_instance.web.id\n" "}\n"); fclose(tf); char tf_db[512]; snprintf(tf_db, sizeof(tf_db), "%s/test_hcl_refs.db", g_tmpdir); cbm_pipeline_t *tp = cbm_pipeline_new(g_tmpdir, tf_db, CBM_MODE_FULL); ASSERT_NOT_NULL(tp); ASSERT_EQ(cbm_pipeline_run(tp), 0); cbm_store_t *ts = cbm_store_open_path(tf_db); ASSERT_NOT_NULL(ts); const char *tf_project = cbm_pipeline_project_name(tp); ASSERT(cross_file_edge_exists(ts, tf_project, "resource.aws_eip.ip", "resource.aws_instance.web", "USAGE")); ASSERT(!cross_file_edge_exists(ts, tf_project, "main", "resource.aws_instance.web", "USAGE")); cbm_store_close(ts); cbm_pipeline_free(tp); teardown_test_repo(); PASS(); } /* Regression: incremental re-index of an edited file must NOT drop inbound * cross-file CALLS edges whose source lives in an UNCHANGED file. * * Repro: Serve() (pkg/service.go) CALLS Help() (pkg/util/helper.go). Editing * helper.go purges Help's node; before the fix the cascade deleted the * Serve->Help edge and never regenerated it (helper.go's callers are not * re-parsed), so the edge silently vanished on every edit and the incremental * graph diverged from a full reindex. */ TEST(pipeline_incremental_preserves_cross_file_calls) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_incr_calls.db", g_tmpdir); /* 1. Full index. */ cbm_pipeline_t *p1 = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p1); ASSERT_EQ(cbm_pipeline_run(p1), 0); const char *project = cbm_pipeline_project_name(p1); cbm_store_t *s1 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s1); int calls_before = cbm_store_count_edges_by_type(s1, project, "CALLS"); ASSERT_GTE(calls_before, 2); /* main->Serve and Serve->Help at minimum */ ASSERT_TRUE(cross_file_call_exists(s1, project, "Serve", "Help")); cbm_store_close(s1); cbm_pipeline_free(p1); /* 2. Edit the callee's file so the incremental classifier marks it changed * (mtime+size differ). Help's symbol + qualified name are unchanged. */ char helper[512]; snprintf(helper, sizeof(helper), "%s/pkg/util/helper.go", g_tmpdir); ASSERT_EQ(th_append_file(helper, "\n// incremental regression marker\n"), 0); /* 3. Re-run on the SAME db_path → auto-routes to incremental re-index. */ cbm_pipeline_t *p2 = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p2); ASSERT_EQ(cbm_pipeline_run(p2), 0); /* 4. The inbound cross-file CALLS edge must survive and the total CALLS * count must not regress. (Before the fix: Serve->Help is dropped.) * NOTE: query with p2's project name — p1 (and the `project` pointer it * owned) was freed above; p1 and p2 derive the same name from g_tmpdir. */ const char *project2 = cbm_pipeline_project_name(p2); cbm_store_t *s2 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s2); int calls_after = cbm_store_count_edges_by_type(s2, project2, "CALLS"); ASSERT_EQ(calls_after, calls_before); ASSERT_TRUE(cross_file_call_exists(s2, project2, "Serve", "Help")); cbm_store_close(s2); cbm_pipeline_free(p2); teardown_test_repo(); PASS(); } static void write_temp_file(const char *dir, const char *name, const char *content); /* An Ansible task file included by an UNCHANGED file is edited, its body * only. The includer is not re-parsed, so its IMPORTS edge must survive the * edited file's re-index: an edge into a node the repair re-creates would be * lost (the Terraform layer needed closure_terraform_decline for that). */ TEST(pipeline_incremental_keeps_ansible_includes_of_an_edited_file) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } write_temp_file(g_tmpdir, "roles/app/tasks/main.yml", "- name: setup\n include_tasks: setup.yml\n"); write_temp_file(g_tmpdir, "roles/app/tasks/setup.yml", "- name: s\n debug:\n msg: one\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_incr_ansible.db", g_tmpdir); cbm_pipeline_t *p1 = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p1); ASSERT_EQ(cbm_pipeline_run(p1), 0); cbm_store_t *s1 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s1); ASSERT_TRUE(cross_file_edge_exists(s1, cbm_pipeline_project_name(p1), "main.yml", "roles/app/tasks/setup.yml", "IMPORTS")); cbm_store_close(s1); cbm_pipeline_free(p1); write_temp_file(g_tmpdir, "roles/app/tasks/setup.yml", "- name: s\n debug:\n msg: two words\n"); cbm_pipeline_t *p2 = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p2); ASSERT_EQ(cbm_pipeline_run(p2), 0); cbm_store_t *s2 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s2); ASSERT_TRUE(cross_file_edge_exists(s2, cbm_pipeline_project_name(p2), "main.yml", "roles/app/tasks/setup.yml", "IMPORTS")); cbm_store_close(s2); cbm_pipeline_free(p2); teardown_test_repo(); PASS(); } /* The CALLS edges an Ansible notify draws (via notify); -1 on a query error. */ static int ansible_notify_calls(cbm_store_t *s, const char *project) { sqlite3_stmt *st = NULL; int n = -1; if (sqlite3_prepare_v2(cbm_store_get_db(s), "SELECT COUNT(*) FROM edges WHERE project=?1 AND type='CALLS' AND " "json_extract(properties,'$.via')='notify'", -1, &st, NULL) == SQLITE_OK && sqlite3_bind_text(st, 1, project, -1, SQLITE_TRANSIENT) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW) { n = sqlite3_column_int(st, 0); } sqlite3_finalize(st); return n; } /* A task's notify CALLS the handlers of its role in another file. Editing * either file alone must land on the full graph: the handler file (a listen * topic renamed: one handler stops answering) re-creates its handlers, the * unchanged task file is not re-walked; the task file (a notifying task * added) is re-walked without the unchanged handler file. Both decline to * the full rebuild (closure_ansible_decline); the repair route kept a stale * edge in the first and lost every notify of the edited file in the second. */ TEST(pipeline_incremental_keeps_ansible_notifies_across_files) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } write_temp_file(g_tmpdir, "roles/app/tasks/main.yml", "- name: configure\n command: /bin/true\n notify: restart app\n"); write_temp_file(g_tmpdir, "roles/app/handlers/main.yml", "- name: h1\n command: one\n listen: restart app\n" "- name: h2\n command: two\n listen: restart app\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_incr_notify.db", g_tmpdir); int want[] = {2, 1, 2}; for (int step = 0; step < 3; step++) { if (step == 1) { write_temp_file(g_tmpdir, "roles/app/handlers/main.yml", "- name: h1\n command: one\n listen: restart app\n" "- name: h2\n command: two\n listen: something else\n"); } else if (step == 2) { write_temp_file(g_tmpdir, "roles/app/tasks/main.yml", "- name: configure\n command: /bin/true\n notify: restart app\n" "- name: reconfigure\n command: /bin/false\n notify: restart app\n"); } cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); int calls = ansible_notify_calls(s, cbm_pipeline_project_name(p)); cbm_store_close(s); cbm_pipeline_free(p); ASSERT_EQ(calls, want[step]); } teardown_test_repo(); PASS(); } /* The files Ansible tasks DEPLOY (via ansible), ordered, one per line, into * `out`; false on a query error. */ static bool ansible_deploy_targets(cbm_store_t *s, const char *project, char *out, size_t sz) { sqlite3_stmt *st = NULL; bool ok = false; if (sqlite3_prepare_v2(cbm_store_get_db(s), "SELECT coalesce(group_concat(p, char(10)), '') FROM (SELECT " "t.file_path AS p FROM edges e JOIN nodes t ON t.id=e.target_id WHERE " "e.project=?1 AND e.type='DEPLOYS' AND " "json_extract(e.properties,'$.via')='ansible' ORDER BY p)", -1, &st, NULL) == SQLITE_OK && sqlite3_bind_text(st, 1, project, -1, SQLITE_TRANSIENT) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW) { snprintf(out, sz, "%s", (const char *)sqlite3_column_text(st, 0)); ok = true; } sqlite3_finalize(st); return ok; } /* A kube task DEPLOYS the template its play rendered, read through a list of * the role's defaults/main.yml -- a file holding no task. Editing it (the * list names another template) or deleting it must land on the full graph, * and so must deleting the deployed template while a file of that name sits * in the task file's directory (the render falls back to it). The repair * did not re-walk the task file for the list edit -- it kept the edge to the * template the list no longer names -- and re-walked it for the deletion as * a dependent of the template, without the role's defaults: no edge at all. * Both decline to the full rebuild (closure_ansible_decline: a role's * defaults / vars; closure_ansible_dependent_decline: a task file joining * the repair as a dependent). A template body edit keeps the repair and the * edge. */ TEST(pipeline_incremental_keeps_ansible_deploys_across_files) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } write_temp_file(g_tmpdir, "roles/app/tasks/main.yml", "- name: Render\n template:\n src: \"{{ item.file }}.j2\"\n" " dest: \"/etc/k/{{ item.file }}\"\n loop: \"{{ app_list }}\"\n" " register: out\n" "- name: Apply\n kube:\n filename: \"/etc/k/{{ item.item.file }}\"\n" " loop: \"{{ out.results }}\"\n"); write_temp_file(g_tmpdir, "roles/app/defaults/main.yml", "app_list:\n - {file: a.yml}\n"); write_temp_file(g_tmpdir, "roles/app/templates/a.yml.j2", "a: 1\n"); write_temp_file(g_tmpdir, "roles/app/templates/b.yml.j2", "b: 1\n"); write_temp_file(g_tmpdir, "roles/app/tasks/b.yml.j2", "b: 2\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_incr_deploys.db", g_tmpdir); static const char *const want[] = { "roles/app/templates/a.yml.j2", /* the full index */ "roles/app/templates/a.yml.j2", /* a.yml.j2's body edited */ "roles/app/templates/b.yml.j2", /* the defaults list names b.yml */ "roles/app/tasks/b.yml.j2", /* templates/b.yml.j2 deleted */ "", /* the defaults deleted */ }; char path[1024]; for (int step = 0; step < (int)(sizeof(want) / sizeof(want[0])); step++) { if (step == 1) { snprintf(path, sizeof(path), "%s/roles/app/templates/a.yml.j2", g_tmpdir); ASSERT_EQ(th_append_file(path, "a2: 2\n"), 0); } else if (step == 2) { write_temp_file(g_tmpdir, "roles/app/defaults/main.yml", "app_list:\n - {file: b.yml}\n"); } else if (step == 3) { snprintf(path, sizeof(path), "%s/roles/app/templates/b.yml.j2", g_tmpdir); ASSERT_EQ(th_unlink_force(path), 0); } else if (step == 4) { snprintf(path, sizeof(path), "%s/roles/app/defaults/main.yml", g_tmpdir); ASSERT_EQ(th_unlink_force(path), 0); } cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); char got[2048]; bool ok = ansible_deploy_targets(s, cbm_pipeline_project_name(p), got, sizeof(got)); cbm_store_close(s); cbm_pipeline_free(p); ASSERT_TRUE(ok); if (strcmp(got, want[step]) != 0) { fprintf(stderr, "step %d deploys got [%s] want [%s]\n", step, got, want[step]); } ASSERT_STR_EQ(got, want[step]); } teardown_test_repo(); PASS(); } /* Regression: a Terraform module's output read lives in one file * (infra/outputs.tf: module.net.vpc_id) and the module's source in another * (infra/main.tf). Pointing the source elsewhere edits main.tf alone; the * repair route recomputed the changed file's edges only, so the read -- held * by two UNCHANGED files, outputs.tf and modules/net/main.tf -- stayed in the * graph, which a full reindex no longer has. A Terraform input change now * declines to the full rebuild (closure_terraform_decline). */ TEST(pipeline_incremental_drops_terraform_reads_of_a_moved_module) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } write_temp_file(g_tmpdir, "infra/main.tf", "module \"net\" {\n source = \"./modules/net\"\n}\n"); write_temp_file(g_tmpdir, "infra/outputs.tf", "output \"net_vpc\" {\n value = module.net.vpc_id\n}\n"); write_temp_file(g_tmpdir, "infra/modules/net/main.tf", "output \"vpc_id\" {\n value = \"x\"\n}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_incr_tf.db", g_tmpdir); cbm_pipeline_t *p1 = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p1); ASSERT_EQ(cbm_pipeline_run(p1), 0); cbm_store_t *s1 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s1); ASSERT_TRUE(cross_file_edge_exists(s1, cbm_pipeline_project_name(p1), "output.net_vpc", "output.vpc_id", "USAGE")); cbm_store_close(s1); cbm_pipeline_free(p1); /* the module's source moves to a directory that does not exist */ write_temp_file(g_tmpdir, "infra/main.tf", "module \"net\" {\n source = \"./modules/none\"\n}\n"); cbm_pipeline_t *p2 = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p2); ASSERT_EQ(cbm_pipeline_run(p2), 0); cbm_store_t *s2 = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s2); ASSERT_FALSE(cross_file_edge_exists(s2, cbm_pipeline_project_name(p2), "output.net_vpc", "output.vpc_id", "USAGE")); cbm_store_close(s2); cbm_pipeline_free(p2); teardown_test_repo(); PASS(); } /* TS/JS receiver-aware weak-strategy suppression (#592/#606 direction; Perl * precedent #477). A member call x.foo() whose receiver TYPE the TS-LSP cannot * resolve (a regex literal `re.test()`) must NOT be bound to a same-named * project method by a weak short-name strategy — that fabricates a CALLS edge. * Type-resolved receivers (`c.test()` on a typed SalesforceRestClient) and bare * local calls must still resolve. < 50 files → sequential path (pass_calls.c). * RED before the fix: checkFormat->test exists via unique_name/suffix_match. */ static void write_temp_file(const char *dir, const char *name, const char *content); typedef struct { const char *source_name; const char *target_name; } PreciseReferencePair; typedef struct { bool database_opened; bool query_ready; bool query_completed; int row_count; int valid_json_count; int exact_callee_count; } NamedEdgePropertyObservation; /* Read one named edge's persisted properties through SQLite's JSON functions. * The guarded json_extract is intentional: on the RED revision the parallel * usage emitter truncates the final `}` from a boundary-sized callee property. * json_valid must report that row without json_extract aborting the statement, * while the fixed revision must round-trip the complete callee value. */ static NamedEdgePropertyObservation observe_named_edge_callee_property( const char *db_path, const char *project, const char *edge_type, const char *source_name, const char *target_name, const char *expected_callee) { NamedEdgePropertyObservation observation = {0}; sqlite3 *db = NULL; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) { if (db) { sqlite3_close(db); } return observation; } observation.database_opened = true; static const char sql[] = "SELECT e.properties, json_valid(e.properties), " "CASE WHEN json_valid(e.properties) " "THEN json_extract(e.properties, '$.callee') END " "FROM edges e " "JOIN nodes src ON src.id=e.source_id AND src.project=e.project " "JOIN nodes tgt ON tgt.id=e.target_id AND tgt.project=e.project " "WHERE e.project=?1 AND e.type=?2 AND src.name=?3 AND tgt.name=?4;"; sqlite3_stmt *stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK || sqlite3_bind_text(stmt, 1, project, -1, SQLITE_TRANSIENT) != SQLITE_OK || sqlite3_bind_text(stmt, 2, edge_type, -1, SQLITE_TRANSIENT) != SQLITE_OK || sqlite3_bind_text(stmt, 3, source_name, -1, SQLITE_TRANSIENT) != SQLITE_OK || sqlite3_bind_text(stmt, 4, target_name, -1, SQLITE_TRANSIENT) != SQLITE_OK) { sqlite3_finalize(stmt); sqlite3_close(db); return observation; } observation.query_ready = true; int step_rc = SQLITE_OK; while ((step_rc = sqlite3_step(stmt)) == SQLITE_ROW) { observation.row_count++; if (sqlite3_column_int(stmt, 1) != 0) { observation.valid_json_count++; } const unsigned char *callee = sqlite3_column_text(stmt, 2); if (callee && strcmp((const char *)callee, expected_callee) == 0) { observation.exact_callee_count++; } } observation.query_completed = step_rc == SQLITE_DONE; sqlite3_finalize(stmt); sqlite3_close(db); return observation; } /* Count only edges of `edge_type` between the exact named source/target nodes. * The parity and incremental guards deliberately use unique symbol names, so * this canonicalizes across independent stores without depending on node IDs * or project-prefixed qualified names. */ static int named_edge_count(cbm_store_t *s, const char *project, const char *edge_type, const char *source_name, const char *target_name) { cbm_edge_t *edges = NULL; int edge_count = 0; if (cbm_store_find_edges_by_type(s, project, edge_type, &edges, &edge_count) != CBM_STORE_OK) { return -1; } int matches = 0; for (int i = 0; i < edge_count; i++) { cbm_node_t source = {0}; cbm_node_t target = {0}; int source_ok = cbm_store_find_node_by_id(s, edges[i].source_id, &source) == CBM_STORE_OK; int target_ok = cbm_store_find_node_by_id(s, edges[i].target_id, &target) == CBM_STORE_OK; if (source_ok && target_ok && source.name && target.name && strcmp(source.name, source_name) == 0 && strcmp(target.name, target_name) == 0) { matches++; } cbm_node_free_fields(&source); cbm_node_free_fields(&target); } if (edges) { cbm_store_free_edges(edges, edge_count); } return matches; } /* HTTP_CALLS edges out of the function `source_name` whose Route target's * qualified name contains `route_qn_part` (e.g. "__route__GET__"). */ static int http_calls_to_route(cbm_store_t *s, const char *project, const char *source_name, const char *route_qn_part) { cbm_edge_t *edges = NULL; int edge_count = 0; if (cbm_store_find_edges_by_type(s, project, "HTTP_CALLS", &edges, &edge_count) != CBM_STORE_OK) { return -1; } int matches = 0; for (int i = 0; i < edge_count; i++) { cbm_node_t source = {0}; cbm_node_t target = {0}; int source_ok = cbm_store_find_node_by_id(s, edges[i].source_id, &source) == CBM_STORE_OK; int target_ok = cbm_store_find_node_by_id(s, edges[i].target_id, &target) == CBM_STORE_OK; if (source_ok && target_ok && source.name && target.qualified_name && strcmp(source.name, source_name) == 0 && strstr(target.qualified_name, route_qn_part)) { matches++; } cbm_node_free_fields(&source); cbm_node_free_fields(&target); } if (edges) { cbm_store_free_edges(edges, edge_count); } return matches; } /* Like named_edge_count, but distinguish same-named targets by their source * file. Semantic-control fixtures intentionally keep the exported short name * identical in two modules so a project-wide unique-name fallback cannot make * an alias-mapping assertion pass accidentally. */ static int named_edge_to_file_count(cbm_store_t *s, const char *project, const char *edge_type, const char *source_name, const char *target_name, const char *target_file_path) { cbm_edge_t *edges = NULL; int edge_count = 0; if (cbm_store_find_edges_by_type(s, project, edge_type, &edges, &edge_count) != CBM_STORE_OK) { return -1; } int matches = 0; for (int i = 0; i < edge_count; i++) { cbm_node_t source = {0}; cbm_node_t target = {0}; int source_ok = cbm_store_find_node_by_id(s, edges[i].source_id, &source) == CBM_STORE_OK; int target_ok = cbm_store_find_node_by_id(s, edges[i].target_id, &target) == CBM_STORE_OK; if (source_ok && target_ok && source.name && target.name && target.file_path && strcmp(source.name, source_name) == 0 && strcmp(target.name, target_name) == 0 && strcmp(target.file_path, target_file_path) == 0) { matches++; } cbm_node_free_fields(&source); cbm_node_free_fields(&target); } if (edges) { cbm_store_free_edges(edges, edge_count); } return matches; } /* Count exact-name nodes without depending on project-prefixed qualified names. * Export-XML relationship tests use this as an anti-vacuous guard: the * transcoded methods must exist even when their extracted relationships were * accidentally discarded. */ static int named_node_count(cbm_store_t *s, const char *project, const char *name) { cbm_node_t *nodes = NULL; int count = 0; if (cbm_store_find_nodes_by_name(s, project, name, &nodes, &count) != CBM_STORE_OK) { return -1; } if (nodes) { cbm_store_free_nodes(nodes, count); } return count; } static int fixture_node_count(cbm_store_t *store, const char *project, const char *path, const char *name, const char *label) { cbm_node_t *nodes = NULL; int count = 0; if (cbm_store_find_nodes_by_name(store, project, name, &nodes, &count) != CBM_STORE_OK) { return -1; } int found = 0; for (int i = 0; i < count; i++) { if (nodes[i].file_path && nodes[i].label && strcmp(nodes[i].file_path, path) == 0 && strcmp(nodes[i].label, label) == 0) { found++; } } if (nodes) { cbm_store_free_nodes(nodes, count); } return found; } /* Edges of `edge_type` from the node named `source_name` to the node named * `target_name` that carries `target_label`. */ static int labeled_edge_count(cbm_store_t *s, const char *project, const char *edge_type, const char *source_name, const char *target_name, const char *target_label) { cbm_edge_t *edges = NULL; int edge_count = 0; if (cbm_store_find_edges_by_type(s, project, edge_type, &edges, &edge_count) != CBM_STORE_OK) { return -1; } int matches = 0; for (int i = 0; i < edge_count; i++) { cbm_node_t source = {0}; cbm_node_t target = {0}; int source_ok = cbm_store_find_node_by_id(s, edges[i].source_id, &source) == CBM_STORE_OK; int target_ok = cbm_store_find_node_by_id(s, edges[i].target_id, &target) == CBM_STORE_OK; if (source_ok && target_ok && source.name && target.name && target.label && strcmp(source.name, source_name) == 0 && strcmp(target.name, target_name) == 0 && strcmp(target.label, target_label) == 0) { matches++; } cbm_node_free_fields(&source); cbm_node_free_fields(&target); } if (edges) { cbm_store_free_edges(edges, edge_count); } return matches; } /* A Java field and a same-named method of one class keep their own nodes (the * field is fenced `.#field`), and a write or a read of the field * reaches the field, never the method; the method keeps its calls. The setter * writing its own namesake was a self-edge before, so it was dropped. */ TEST(pipeline_field_named_like_method_keeps_its_reads_and_writes) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_fieldfence_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); th_write_file(TH_PATH(tmp, "src/demo/Starter.java"), "package demo;\n\npublic class Starter {\n" " private String executable;\n\n" " public Starter executable(String path) {\n" " this.executable = path;\n" " return this;\n }\n\n" " public String show() {\n return executable;\n }\n\n" " public static void run() {\n" " new Starter().executable(\"x\").show();\n }\n}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/fence.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *store = cbm_store_open_path(db_path); ASSERT_NOT_NULL(store); ASSERT_EQ(named_node_count(store, project, "executable"), 2); ASSERT_EQ(labeled_edge_count(store, project, "WRITES", "executable", "executable", "Field"), 1); ASSERT_EQ(labeled_edge_count(store, project, "WRITES", "executable", "executable", "Method"), 0); ASSERT_EQ(labeled_edge_count(store, project, "USAGE", "show", "executable", "Field"), 1); ASSERT_EQ(labeled_edge_count(store, project, "USAGE", "show", "executable", "Method"), 0); ASSERT_EQ(labeled_edge_count(store, project, "CALLS", "run", "executable", "Method"), 1); cbm_store_close(store); cbm_pipeline_free(p); th_cleanup(tmp); PASS(); } typedef struct { int run_rc; bool store_opened; int run_nodes; int execute_nodes; int run_execute_calls; int shared_payload_nodes; int run_shared_payload_usages; } ObjectScriptExportObservation; static ObjectScriptExportObservation observe_objectscript_export(const char *repo_path, const char *db_name) { ObjectScriptExportObservation observation = { .run_rc = -1, .store_opened = false, .run_nodes = -1, .execute_nodes = -1, .run_execute_calls = -1, .shared_payload_nodes = -1, .run_shared_payload_usages = -1, }; char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/%s", repo_path, db_name); cbm_pipeline_t *pipeline = cbm_pipeline_new(repo_path, db_path, CBM_MODE_FULL); if (!pipeline) { return observation; } observation.run_rc = cbm_pipeline_run(pipeline); const char *project = cbm_pipeline_project_name(pipeline); cbm_store_t *store = cbm_store_open_path(db_path); observation.store_opened = store != NULL; if (store && project) { observation.run_nodes = named_node_count(store, project, "Run"); observation.execute_nodes = named_node_count(store, project, "Execute"); observation.run_execute_calls = named_edge_count(store, project, "CALLS", "Run", "Execute"); observation.shared_payload_nodes = named_node_count(store, project, "SharedPayload"); observation.run_shared_payload_usages = named_edge_count(store, project, "USAGE", "Run", "SharedPayload"); } if (store) { cbm_store_close(store); } cbm_pipeline_free(pipeline); return observation; } /* Studio Export XML is transformed into ObjectScript UDL during extraction. * The transformed definitions already survive, but the old sequential and * fused-parallel special cases freed each transformed result after inserting * only its definitions. Calls/usages therefore vanished even though direct * extraction of the same generated UDL found them. * * One shared multi-class fixture is indexed through both execution paths. The * 50 filler files only select the fused path; CBM_INDEX_SINGLE_THREAD forces * the first run through the sequential path without duplicating setup. * RED before the fix: Run and Execute each exist once, Run -> Execute is 0 in * both stores. GREEN requires exactly one CALLS edge in each store. */ TEST(pipeline_objectscript_export_preserves_calls_sequential_parallel) { static const char export_xml[] = "\n" "\n" "\n" "\n" "1\n" "pValue:%String\n" "%Status\n" "\n" "\n" "\n" "\n" "\n" "1\n" "pPayload:%String\n" "%Status\n" "\n" "\n" "\n" "\n"; char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_export_rel_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } char export_path[512]; snprintf(export_path, sizeof(export_path), "%s/studio-export.xml", tmp); if (th_write_file(export_path, export_xml) != 0) { th_rmtree(tmp); FAIL("failed to write Studio Export fixture"); } /* Export XML plus 50 trackable fillers exceeds MIN_FILES_FOR_PARALLEL. */ for (int i = 0; i < 50; i++) { char filename[64]; char path[512]; char source[128]; snprintf(filename, sizeof(filename), "export_pad_%02d.ts", i); snprintf(path, sizeof(path), "%s/%s", tmp, filename); snprintf(source, sizeof(source), "export function exportPad%02d(): number { return %d; }\n", i, i); if (th_write_file(path, source) != 0) { th_rmtree(tmp); FAIL("failed to write parallel-selection fixture"); } } char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1); ObjectScriptExportObservation sequential = observe_objectscript_export(tmp, "export-sequential.db"); cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); cbm_setenv("CBM_WORKERS", "4", 1); ObjectScriptExportObservation parallel = observe_objectscript_export(tmp, "export-parallel.db"); if (saved_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); free(saved_workers); } else { cbm_unsetenv("CBM_WORKERS"); } if (saved_single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); free(saved_single); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } th_rmtree(tmp); ASSERT_EQ(sequential.run_rc, 0); ASSERT_TRUE(sequential.store_opened); ASSERT_EQ(sequential.run_nodes, 1); ASSERT_EQ(sequential.execute_nodes, 1); ASSERT_EQ(parallel.run_rc, 0); ASSERT_TRUE(parallel.store_opened); ASSERT_EQ(parallel.run_nodes, 1); ASSERT_EQ(parallel.execute_nodes, 1); if (sequential.run_execute_calls != 1 || parallel.run_execute_calls != 1) { fprintf(stderr, " [objectscript-export] invariant=transformed_calls_preserved " "expected=1/1 actual=%d/%d\n", sequential.run_execute_calls, parallel.run_execute_calls); } ASSERT_TRUE(sequential.run_execute_calls == 1 && parallel.run_execute_calls == 1); PASS(); } static int format_objectscript_export_lifecycle_fixture(char *xml, size_t xml_capacity, const char *default_value) { int written = snprintf(xml, xml_capacity, "\n" "\n" "\n" "\n" "1\n" "pValue:%%String\n" "%%Status\n" "\n" "\n" "\n" "\n" "%s\n" "\n" "1\n" "watched:%%String\n" "%%Status\n" "\n" "\n" "\n" "\n", default_value); if (written <= 0 || (size_t)written >= xml_capacity) { return -1; } return written; } static int write_objectscript_export_lifecycle_fixture(const char *path, const char *default_value) { char xml[4096]; int written = format_objectscript_export_lifecycle_fixture(xml, sizeof(xml), default_value); if (written < 0) { return -1; } return th_write_file(path, xml); } /* A changed Studio Export file must keep its transformed carriers alive for * every incremental resolution pass. Compare the incrementally rebuilt graph * with a clean full sequential build of the same replacement XML. Direct * extraction separately proves that the transformed aggregate retains the * method-argument USAGE carrier; formal parameters are not graph targets, so * graph resolution of that carrier is outside this lifecycle regression. * ObjectScript has no per-file or cross-file LSP dispatcher, so it cannot * produce a resolved_calls semantic carrier to assert here. * * RED before the incremental cache lifecycle fix: definitions survived, but * the changed Export was re-read as raw XML by later passes, so Run->Execute * CALLS was absent from the incremental graph. */ TEST(pipeline_objectscript_export_incremental_matches_full_relationships) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_export_incr_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } char export_path[512]; snprintf(export_path, sizeof(export_path), "%s/studio-export.xml", tmp); if (write_objectscript_export_lifecycle_fixture(export_path, "initial") != 0) { th_rmtree(tmp); FAIL("failed to write initial Studio Export fixture"); } char carrier_xml[4096]; int carrier_xml_len = format_objectscript_export_lifecycle_fixture(carrier_xml, sizeof(carrier_xml), "initial"); if (carrier_xml_len < 0) { th_rmtree(tmp); FAIL("failed to format Studio Export carrier fixture"); } cbm_init(); CBMFileResult *carrier_result = cbm_pipeline_extract_objectscript_export( carrier_xml, carrier_xml_len, "export-lifecycle", "studio-export.xml", NULL, NULL); if (!carrier_result) { th_rmtree(tmp); FAIL("failed to extract Studio Export carrier fixture"); } bool found_argument_usage = false; for (int i = 0; i < carrier_result->usages.count; i++) { const CBMUsage *usage = &carrier_result->usages.items[i]; if (usage->ref_name && strcmp(usage->ref_name, "watched") == 0 && usage->enclosing_func_qn && strstr(usage->enclosing_func_qn, ".Run")) { found_argument_usage = true; break; } } cbm_free_result(carrier_result); if (!found_argument_usage) { th_rmtree(tmp); FAIL("Studio Export aggregate lost method-argument USAGE carrier"); } char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1); ObjectScriptExportObservation initial = observe_objectscript_export(tmp, "export-incremental.db"); if (write_objectscript_export_lifecycle_fixture(export_path, "replacement-with-a-different-size") != 0) { if (saved_single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); free(saved_single); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } th_rmtree(tmp); FAIL("failed to write replacement Studio Export fixture"); } /* Same DB routes through incremental; a fresh DB is the full reference. */ ObjectScriptExportObservation incremental = observe_objectscript_export(tmp, "export-incremental.db"); ObjectScriptExportObservation full = observe_objectscript_export(tmp, "export-full-reference.db"); if (saved_single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); free(saved_single); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } th_rmtree(tmp); ASSERT_EQ(initial.run_rc, 0); ASSERT_TRUE(initial.store_opened); ASSERT_EQ(initial.run_execute_calls, 1); ASSERT_EQ(incremental.run_rc, 0); ASSERT_TRUE(incremental.store_opened); ASSERT_EQ(full.run_rc, 0); ASSERT_TRUE(full.store_opened); ASSERT_EQ(incremental.run_nodes, full.run_nodes); ASSERT_EQ(incremental.execute_nodes, full.execute_nodes); ASSERT_EQ(incremental.shared_payload_nodes, full.shared_payload_nodes); ASSERT_EQ(incremental.run_execute_calls, full.run_execute_calls); ASSERT_EQ(incremental.run_shared_payload_usages, full.run_shared_payload_usages); ASSERT_EQ(full.run_nodes, 1); ASSERT_EQ(full.execute_nodes, 1); ASSERT_EQ(full.shared_payload_nodes, 1); ASSERT_EQ(full.run_execute_calls, 1); PASS(); } /* Cross-language unique_name leak. Every Python file injects a few builtin * stub definitions (builtins.dict.get, builtins.len, ...; see * internal/cbm/lsp/py_builtins.c) into the project registry. A JS Express * registration `app.get('/users', listUsers)` has no LSP target, falls * through to the registry, and its short name "get" had exactly one * candidate project-wide: Python's builtins.dict.get (strategy unique_name). * The non-empty resolution skipped the route-registration fallback (it runs * only on an EMPTY resolution), and the weak-member guard then dropped the * plain CALLS edge, so the Route node and its HANDLES edge vanished -- on the * sequential and the parallel path alike, and on an incremental reindex whose * registry is re-seeded from stored nodes (the builtin stubs survive there * even after the last .py file is gone). * * The same leak fabricates CALLS edges on both paths: a bare JS `print(msg)` * bound to Python's builtins.print by unique_name. `cache.get(key)` guards the * other side of the veto: an unresolved route verb without a route path must * not become a CALLS self-loop on the parallel path. * * The Python control proves the builtins stay reachable from Python itself: * lookup() -> builtins.dict.get (lsp_generic_method) and -> builtins.len. */ typedef struct { int run_rc; bool store_opened; int routes; /* Route node "/users" */ int handles; /* listUsers -[HANDLES]-> "/users" */ int py_dict_get; /* lookup -[CALLS]-> get */ int py_len; /* lookup -[CALLS]-> len */ int js_print; /* report -[CALLS]-> print (JS global, never Python's) */ int self_loop; /* readCache -[CALLS]-> readCache (unresolved route verb) */ } XlangRouteObservation; static XlangRouteObservation observe_xlang_route(const char *repo_path, const char *db_name) { XlangRouteObservation o = {.run_rc = -1, .routes = -1, .handles = -1, .py_dict_get = -1, .py_len = -1, .js_print = -1, .self_loop = -1}; char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/%s", repo_path, db_name); cbm_pipeline_t *pipeline = cbm_pipeline_new(repo_path, db_path, CBM_MODE_FULL); if (!pipeline) { return o; } o.run_rc = cbm_pipeline_run(pipeline); const char *project = cbm_pipeline_project_name(pipeline); cbm_store_t *store = cbm_store_open_path(db_path); o.store_opened = store != NULL; if (store && project) { o.routes = named_node_count(store, project, "/users"); o.handles = named_edge_count(store, project, "HANDLES", "listUsers", "/users"); o.py_dict_get = named_edge_count(store, project, "CALLS", "lookup", "get"); o.py_len = named_edge_count(store, project, "CALLS", "lookup", "len"); o.js_print = named_edge_count(store, project, "CALLS", "report", "print"); o.self_loop = named_edge_count(store, project, "CALLS", "readCache", "readCache"); } if (store) { cbm_store_close(store); } cbm_pipeline_free(pipeline); return o; } static const char XLANG_APP_JS[] = "const express = require('express');\n" "const app = express();\n" "\n" "function listUsers(req, res) {\n" " res.json([]);\n" "}\n" "\n" "app.get('/users', listUsers);\n" "\n" "function report(msg) {\n" " print(msg);\n" "}\n" "\n" "function readCache(cache, key) {\n" " return cache.get(key);\n" "}\n" "module.exports = { app, report, readCache };\n"; static const char XLANG_TOOL_PY[] = "def lookup(xs):\n" " d = {\"k\": 1}\n" " n = len(xs)\n" " return d.get(\"k\"), n\n"; /* Writes app.js + tool.py + `pad` JS filler files (the fillers only select the * parallel path: more than MIN_FILES_FOR_PARALLEL files). 0 on success. */ static int write_xlang_route_fixture(const char *dir, int pad) { char path[512]; snprintf(path, sizeof(path), "%s/app.js", dir); if (th_write_file(path, XLANG_APP_JS) != 0) { return -1; } snprintf(path, sizeof(path), "%s/tool.py", dir); if (th_write_file(path, XLANG_TOOL_PY) != 0) { return -1; } for (int i = 0; i < pad; i++) { char source[128]; snprintf(path, sizeof(path), "%s/xlang_pad_%02d.js", dir, i); snprintf(source, sizeof(source), "function xlangPad%02d() { return %d; }\n", i, i); if (th_write_file(path, source) != 0) { return -1; } } return 0; } /* Saves and overrides the two worker-selection variables. */ typedef struct { char *workers; char *single; } XlangEnvSave; static XlangEnvSave xlang_env_select(bool parallel) { XlangEnvSave save = {0}; const char *w = getenv("CBM_WORKERS"); const char *s = getenv("CBM_INDEX_SINGLE_THREAD"); save.workers = w ? strdup(w) : NULL; save.single = s ? strdup(s) : NULL; if (parallel) { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); cbm_setenv("CBM_WORKERS", "4", 1); } else { cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1); } return save; } static void xlang_env_restore(XlangEnvSave *save) { if (save->workers) { cbm_setenv("CBM_WORKERS", save->workers, 1); free(save->workers); } else { cbm_unsetenv("CBM_WORKERS"); } if (save->single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", save->single, 1); free(save->single); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } save->workers = NULL; save->single = NULL; } static XlangRouteObservation run_xlang_route_case(bool parallel, int pad) { XlangRouteObservation o = {.run_rc = -1}; char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_xlang_route_XXXXXX"); if (!cbm_mkdtemp(tmp)) { return o; } if (write_xlang_route_fixture(tmp, pad) == 0) { XlangEnvSave save = xlang_env_select(parallel); o = observe_xlang_route(tmp, "xlang.db"); xlang_env_restore(&save); } th_rmtree(tmp); return o; } TEST(pipeline_js_route_not_bound_to_python_builtin_sequential) { XlangRouteObservation o = run_xlang_route_case(false, 0); ASSERT_EQ(o.run_rc, 0); ASSERT_TRUE(o.store_opened); ASSERT_EQ(o.routes, 1); ASSERT_EQ(o.handles, 1); ASSERT_EQ(o.py_dict_get, 1); ASSERT_EQ(o.py_len, 1); ASSERT_EQ(o.js_print, 0); ASSERT_EQ(o.self_loop, 0); PASS(); } TEST(pipeline_js_route_not_bound_to_python_builtin_parallel) { XlangRouteObservation o = run_xlang_route_case(true, 55); ASSERT_EQ(o.run_rc, 0); ASSERT_TRUE(o.store_opened); ASSERT_EQ(o.routes, 1); ASSERT_EQ(o.handles, 1); ASSERT_EQ(o.py_dict_get, 1); ASSERT_EQ(o.py_len, 1); ASSERT_EQ(o.js_print, 0); ASSERT_EQ(o.self_loop, 0); PASS(); } /* Incremental: the registry is re-seeded from stored nodes, where the Python * builtin stubs outlive the last .py file (their file_path is synthetic). */ TEST(pipeline_js_route_not_bound_to_python_builtin_incremental) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_xlang_incr_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } if (write_xlang_route_fixture(tmp, 0) != 0) { th_rmtree(tmp); FAIL("fixture"); } XlangEnvSave save = xlang_env_select(false); XlangRouteObservation initial = observe_xlang_route(tmp, "xlang-incr.db"); char path[512]; snprintf(path, sizeof(path), "%s/tool.py", tmp); int rm_rc = remove(path); snprintf(path, sizeof(path), "%s/app.js", tmp); int append_rc = th_append_file(path, "// edited: re-resolve this file\n"); XlangRouteObservation incremental = observe_xlang_route(tmp, "xlang-incr.db"); xlang_env_restore(&save); th_rmtree(tmp); ASSERT_EQ(rm_rc, 0); ASSERT_EQ(append_rc, 0); ASSERT_EQ(initial.run_rc, 0); ASSERT_EQ(incremental.run_rc, 0); ASSERT_TRUE(incremental.store_opened); ASSERT_EQ(incremental.routes, 1); ASSERT_EQ(incremental.handles, 1); ASSERT_EQ(incremental.js_print, 0); ASSERT_EQ(incremental.self_loop, 0); PASS(); } /* FD1: a method defined in another file than its type must get its * DEFINES_METHOD edge on every venue. The sequential definitions pass linked * the edge while it was still registering files, so a method whose type lived * in a file the directory walk returns LATER found no parent: on helm and * prometheus the sequential venue (CBM_WORKERS=1) lost 236 DEFINES_METHOD * edges, and the IMPLEMENTS / OVERRIDE edges derived from them, that the * parallel venue keeps. Each of the two files holds one type and the method of * the OTHER file's type, so whichever order the walk returns them in, one * method precedes its type: the case fails without depending on directory * order. */ typedef struct { int run_rc; bool store_opened; int alpha_method; /* FdmAlpha -[DEFINES_METHOD]-> FdmAlphaGet */ int beta_method; /* FdmBeta -[DEFINES_METHOD]-> FdmBetaGet */ } CrossFileMethodObservation; static const char FDM_X_GO[] = "package fdm\n" "\n" "type FdmBeta struct{ n int }\n" "\n" "func (a *FdmAlpha) FdmAlphaGet() int { return a.n }\n"; static const char FDM_Y_GO[] = "package fdm\n" "\n" "type FdmAlpha struct{ n int }\n" "\n" "func (b *FdmBeta) FdmBetaGet() int { return b.n }\n"; static int write_cross_file_method_fixture(const char *dir, int pad) { char path[512]; snprintf(path, sizeof(path), "%s/x.go", dir); if (th_write_file(path, FDM_X_GO) != 0) { return -1; } snprintf(path, sizeof(path), "%s/y.go", dir); if (th_write_file(path, FDM_Y_GO) != 0) { return -1; } for (int i = 0; i < pad; i++) { char source[128]; snprintf(path, sizeof(path), "%s/fdm_pad_%02d.go", dir, i); snprintf(source, sizeof(source), "package fdm\n\nfunc fdmPad%02d() int { return %d }\n", i, i); if (th_write_file(path, source) != 0) { return -1; } } return 0; } static CrossFileMethodObservation run_cross_file_method_case(bool parallel, int pad) { CrossFileMethodObservation o = {.run_rc = -1, .alpha_method = -1, .beta_method = -1}; char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_fd1_method_XXXXXX"); if (!cbm_mkdtemp(tmp)) { return o; } if (write_cross_file_method_fixture(tmp, pad) == 0) { XlangEnvSave save = xlang_env_select(parallel); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/fdm.db", tmp); cbm_pipeline_t *pipeline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); if (pipeline) { o.run_rc = cbm_pipeline_run(pipeline); const char *project = cbm_pipeline_project_name(pipeline); cbm_store_t *store = cbm_store_open_path(db_path); o.store_opened = store != NULL; if (store && project) { o.alpha_method = named_edge_count(store, project, "DEFINES_METHOD", "FdmAlpha", "FdmAlphaGet"); o.beta_method = named_edge_count(store, project, "DEFINES_METHOD", "FdmBeta", "FdmBetaGet"); } if (store) { cbm_store_close(store); } cbm_pipeline_free(pipeline); } xlang_env_restore(&save); } th_rmtree(tmp); return o; } TEST(pipeline_cross_file_method_defines_method_sequential) { CrossFileMethodObservation o = run_cross_file_method_case(false, 0); ASSERT_EQ(o.run_rc, 0); ASSERT_TRUE(o.store_opened); ASSERT_EQ(o.alpha_method, 1); ASSERT_EQ(o.beta_method, 1); PASS(); } /* The same input through both resolvers: 55 fillers exceed * MIN_FILES_FOR_PARALLEL, CBM_INDEX_SINGLE_THREAD keeps the first run * sequential and CBM_WORKERS sends the second through pass_parallel.c. */ TEST(pipeline_cross_file_method_defines_method_venue_parity) { CrossFileMethodObservation seq = run_cross_file_method_case(false, 55); CrossFileMethodObservation par = run_cross_file_method_case(true, 55); ASSERT_EQ(seq.run_rc, 0); ASSERT_EQ(par.run_rc, 0); ASSERT_TRUE(seq.store_opened); ASSERT_TRUE(par.store_opened); ASSERT_EQ(par.alpha_method, 1); ASSERT_EQ(par.beta_method, 1); ASSERT_EQ(seq.alpha_method, par.alpha_method); ASSERT_EQ(seq.beta_method, par.beta_method); PASS(); } static bool test_buffer_appendf(char *buffer, size_t capacity, size_t *used, const char *format, ...) { if (!buffer || !used || *used >= capacity) { return false; } va_list args; va_start(args, format); int written = vsnprintf(buffer + *used, capacity - *used, format, args); va_end(args); if (written < 0 || (size_t)written >= capacity - *used) { return false; } *used += (size_t)written; return true; } static CBM_TLS int objectscript_timeout_calls; static CBM_TLS int64_t objectscript_expected_timeout; static CBM_TLS bool objectscript_timeout_mismatch; static CBMFileResult *test_objectscript_timeout_extractor( const char *source, int source_len, CBMLanguage language, const char *project, const char *rel_path, int64_t timeout_micros, const char **extra_defines, const char **include_paths, const CBMMacroTable *macro_table, const CBMReturnTypeTable *return_type_table) { (void)source; (void)source_len; (void)language; (void)project; (void)rel_path; (void)extra_defines; (void)include_paths; (void)macro_table; (void)return_type_table; objectscript_timeout_calls++; objectscript_timeout_mismatch |= timeout_micros != objectscript_expected_timeout; CBMFileResult *result = calloc(1, sizeof(*result)); if (result) { cbm_arena_init(&result->arena); } return result; } TEST(pipeline_objectscript_export_forwards_parse_timeout) { const char *xml = "" "1" "" "" "1" ""; const int64_t timeouts[] = {0, 17, 5000000}; for (size_t i = 0; i < sizeof(timeouts) / sizeof(timeouts[0]); i++) { objectscript_timeout_calls = 0; objectscript_expected_timeout = timeouts[i]; objectscript_timeout_mismatch = false; cbm_pipeline_set_objectscript_export_extractor_for_tests( test_objectscript_timeout_extractor); CBMFileResult *result = i == 2 ? cbm_pipeline_extract_objectscript_export(xml, (int)strlen(xml), "timeout", "export.xml", NULL, NULL) : cbm_pipeline_extract_objectscript_export_with_timeout( xml, (int)strlen(xml), "timeout", "export.xml", NULL, NULL, timeouts[i]); cbm_pipeline_set_objectscript_export_extractor_for_tests(NULL); bool complete = result && result->owned_result_count == 2; cbm_free_result(result); ASSERT_TRUE(complete); ASSERT_EQ(objectscript_timeout_calls, 2); ASSERT_FALSE(objectscript_timeout_mismatch); } PASS(); } SUITE(pipeline_objectscript_timeout) { RUN_TEST(pipeline_objectscript_export_forwards_parse_timeout); } /* Each extracted class owns a separate arena. A CBMCall includes eight argument * slots, so growing its call array through the 513th item requires at least four * arena blocks per class. Sixty-four classes therefore exceed the aggregate's * fixed 256-block adoption table even before aggregate-array growth is counted, * while staying within the Studio Export transcoder's existing 64-class cap. * * RED with fixed-block ownership transfer: aggregation returns NULL before all * 64 classes can be composed. GREEN requires one result containing every class, * method, and call carrier without raising the class limit. */ TEST(pipeline_objectscript_export_aggregate_exceeds_arena_block_table) { enum { STRESS_CLASS_COUNT = 64, STRESS_CALLS_PER_CLASS = 513, STRESS_XML_CAPACITY = 4 * 1024 * 1024, }; char *xml = (char *)malloc(STRESS_XML_CAPACITY); if (!xml) { FAIL("stress XML allocation"); } size_t used = 0; bool complete = test_buffer_appendf(xml, STRESS_XML_CAPACITY, &used, "\n" "\n"); for (int ci = 0; ci < STRESS_CLASS_COUNT && complete; ci++) { complete = test_buffer_appendf(xml, STRESS_XML_CAPACITY, &used, "\n" "\n" "1\n" "\n" "\n" "\n"); } complete = complete && test_buffer_appendf(xml, STRESS_XML_CAPACITY, &used, "\n"); if (!complete) { free(xml); FAIL("stress XML capacity"); } cbm_init(); CBMFileResult *aggregate = cbm_pipeline_extract_objectscript_export( xml, (int)used, "arena-stress", "studio-export.xml", NULL, NULL); free(xml); ASSERT_NOT_NULL(aggregate); bool found_last_method = false; for (int i = 0; i < aggregate->defs.count; i++) { const CBMDefinition *def = &aggregate->defs.items[i]; if (def->name && strcmp(def->name, "Run63") == 0) { found_last_method = true; break; } } int expected_calls = STRESS_CLASS_COUNT * STRESS_CALLS_PER_CLASS; int actual_calls = aggregate->calls.count; int actual_defs = aggregate->defs.count; cbm_free_result(aggregate); ASSERT_TRUE(found_last_method); ASSERT_EQ(actual_calls, expected_calls); ASSERT_GTE(actual_defs, STRESS_CLASS_COUNT * 2); PASS(); } typedef struct { int run_rc; bool store_opened; int source_nodes; int target_nodes; int edge_count; } NamedEdgeObservation; static NamedEdgeObservation observe_named_edge(const char *repo_path, const char *db_name, const char *edge_type, const char *source_name, const char *target_name) { NamedEdgeObservation observation = { .run_rc = -1, .store_opened = false, .source_nodes = -1, .target_nodes = -1, .edge_count = -1, }; char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/%s", repo_path, db_name); cbm_pipeline_t *pipeline = cbm_pipeline_new(repo_path, db_path, CBM_MODE_FULL); if (!pipeline) { return observation; } observation.run_rc = cbm_pipeline_run(pipeline); const char *project = cbm_pipeline_project_name(pipeline); cbm_store_t *store = cbm_store_open_path(db_path); observation.store_opened = store != NULL; if (store && project) { observation.source_nodes = named_node_count(store, project, source_name); observation.target_nodes = named_node_count(store, project, target_name); observation.edge_count = named_edge_count(store, project, edge_type, source_name, target_name); } if (store) { cbm_store_close(store); } cbm_pipeline_free(pipeline); return observation; } /* env_accesses are extracted independently from calls and must materialize the * same EnvVar + CONFIGURES relationship in both pipeline implementations. * * RED before the fused-path consumer is added: sequential definition insertion * creates envParityRead -> CBM_PARITY_TOKEN, while cbm_build_registry_from_cache * ignores result->env_accesses and the parallel graph contains neither target * nor edge. */ TEST(pipeline_env_access_configures_sequential_parallel_parity) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_env_parity_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } char source_path[512]; snprintf(source_path, sizeof(source_path), "%s/env.go", tmp); if (th_write_file(source_path, "package envparity\n\n" "import \"os\"\n\n" "func envParityRead() string {\n" " return os.Getenv(\"CBM_PARITY_TOKEN\")\n" "}\n") != 0) { th_rmtree(tmp); FAIL("failed to write env-access fixture"); } for (int i = 0; i < 50; i++) { char filename[64]; char path[512]; char source[128]; snprintf(filename, sizeof(filename), "env_pad_%02d.ts", i); snprintf(path, sizeof(path), "%s/%s", tmp, filename); snprintf(source, sizeof(source), "export function envPad%02d(): number { return %d; }\n", i, i); if (th_write_file(path, source) != 0) { th_rmtree(tmp); FAIL("failed to write env parity filler"); } } char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1); NamedEdgeObservation sequential = observe_named_edge(tmp, "env-sequential.db", "CONFIGURES", "envParityRead", "CBM_PARITY_TOKEN"); cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); cbm_setenv("CBM_WORKERS", "4", 1); NamedEdgeObservation parallel = observe_named_edge(tmp, "env-parallel.db", "CONFIGURES", "envParityRead", "CBM_PARITY_TOKEN"); if (saved_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); free(saved_workers); } else { cbm_unsetenv("CBM_WORKERS"); } if (saved_single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); free(saved_single); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } th_rmtree(tmp); ASSERT_EQ(sequential.run_rc, 0); ASSERT_TRUE(sequential.store_opened); ASSERT_EQ(sequential.source_nodes, 1); ASSERT_EQ(sequential.target_nodes, 1); ASSERT_EQ(sequential.edge_count, 1); ASSERT_EQ(parallel.run_rc, 0); ASSERT_TRUE(parallel.store_opened); ASSERT_EQ(parallel.source_nodes, sequential.source_nodes); ASSERT_EQ(parallel.target_nodes, sequential.target_nodes); ASSERT_EQ(parallel.edge_count, sequential.edge_count); PASS(); } /* Sequential and fused-parallel resolution must materialize the same precise * CALL_REFERENCE edge set. The table covers each independently implemented * resolver family involved in callable-value/reference semantics; sibling * frontends share those resolvers (JS/TSX -> TS, C++/CUDA -> C). * * Every pair uses a globally unique source and target name. That lets the test * compare the semantic edge multiset across stores while ignoring unstable DB * IDs and project-name prefixes. The fixture exceeds MIN_FILES_FOR_PARALLEL; * CBM_INDEX_SINGLE_THREAD forces the first run through the sequential path and * CBM_WORKERS forces the second through pass_parallel.c. */ TEST(pipeline_call_reference_sequential_parallel_edge_set_parity) { static const PreciseReferencePair pairs[] = { {"goReferenceSite", "goReferenceTarget"}, {"pythonReferenceSite", "pythonReferenceTarget"}, {"cReferenceSite", "cReferenceTarget"}, {"rustReferenceSite", "rustReferenceTarget"}, {"csharpReferenceSite", "csharpReferenceTarget"}, {"javascriptReferenceSite", "javascriptReferenceTarget"}, {"typescriptReferenceSite", "typescriptReferenceTarget"}, {"tsxReferenceSite", "tsxReferenceTarget"}, {"cppReferenceSite", "cppReferenceTarget"}, {"cudaReferenceSite", "cudaReferenceTarget"}, {"javaReferenceSite", "javaReferenceTarget"}, {"kotlinReferenceSite", "kotlinReferenceTarget"}, {"phpReferenceSite", "phpReferenceTarget"}, {"perlReferenceSite", "perlReferenceTarget"}, }; enum { pair_count = (int)(sizeof(pairs) / sizeof(pairs[0])) }; static const PreciseReferencePair shadow_pairs[] = { {"pythonShadowSite", "pythonShadowTarget"}, {"javascriptShadowSite", "javascriptShadowTarget"}, {"typescriptShadowSite", "typescriptShadowTarget"}, }; static const PreciseReferencePair shadow_controls[] = { {"pythonShadowSite", "pythonShadowAccept"}, {"javascriptShadowSite", "javascriptShadowAccept"}, {"typescriptShadowSite", "typescriptShadowAccept"}, }; enum { shadow_count = (int)(sizeof(shadow_pairs) / sizeof(shadow_pairs[0])) }; char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_ref_parity_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "parity.go", "package parity\n" "func goReferenceTarget() {}\n" "func goReferenceAccept(callback func()) {}\n" "func goReferenceSite() { goReferenceAccept(goReferenceTarget) }\n"); /* `{"callee":""}` contributes 13 bytes. A 243-byte identifier therefore * produces 256 bytes of JSON content: it fits the usage emitter's escaped * value buffer but needs 257 bytes including NUL for the wrapper. The * sequential 512-byte wrapper is valid; the parallel 256-byte wrapper is * RED because snprintf drops the closing brace. */ enum { LONG_REFERENCE_NAME_LEN = 243 }; char long_reference_name[LONG_REFERENCE_NAME_LEN + 1]; memset(long_reference_name, 'x', LONG_REFERENCE_NAME_LEN); long_reference_name[0] = 'l'; long_reference_name[LONG_REFERENCE_NAME_LEN] = '\0'; char long_reference_source[1024]; int long_reference_source_len = snprintf(long_reference_source, sizeof(long_reference_source), "package parity\n" "func %s() {}\n" "func longPropertiesReferenceAccept(callback func()) {}\n" "func longPropertiesReferenceSite() { longPropertiesReferenceAccept(%s) }\n", long_reference_name, long_reference_name); if (long_reference_source_len <= 0 || (size_t)long_reference_source_len >= sizeof(long_reference_source)) { th_rmtree(tmp); FAIL("long reference source buffer"); } write_temp_file(tmp, "long_properties.go", long_reference_source); write_temp_file(tmp, "python_target.py", "def pythonReferenceTarget():\n" " pass\n"); write_temp_file(tmp, "parity.py", "from python_target import pythonReferenceTarget as pythonReferenceAlias\n" "def pythonReferenceAccept(callback):\n" " pass\n" "def pythonReferenceSite():\n" " pythonReferenceAccept(pythonReferenceAlias)\n"); write_temp_file(tmp, "parity.c", "typedef void (*c_reference_callback_t)(void);\n" "void cReferenceTarget(void) {}\n" "void cReferenceAccept(c_reference_callback_t callback) {}\n" "void cReferenceSite(void) { cReferenceAccept(cReferenceTarget); }\n"); write_temp_file(tmp, "parity.rs", "fn rustReferenceTarget() {}\n" "fn rustReferenceAccept(_callback: fn()) {}\n" "fn rustReferenceSite() { rustReferenceAccept(rustReferenceTarget); }\n"); write_temp_file(tmp, "Parity.cs", "using System;\n" "class CSharpParity {\n" " static void csharpReferenceTarget() {}\n" " static void csharpReferenceAccept(Action callback) {}\n" " static void csharpReferenceSite() { " "csharpReferenceAccept(csharpReferenceTarget); }\n" "}\n"); write_temp_file(tmp, "parity.js", "function javascriptReferenceTarget() {}\n" "function javascriptReferenceAccept(callback) {}\n" "function javascriptReferenceSite() {\n" " javascriptReferenceAccept(javascriptReferenceTarget);\n" "}\n"); write_temp_file(tmp, "parity.ts", "class TypeScriptParity { typescriptReferenceTarget(): void {} }\n" "function typescriptReferenceAccept(callback: () => void): void {}\n" "function typescriptReferenceSite(service: TypeScriptParity): void {\n" " typescriptReferenceAccept(service.typescriptReferenceTarget);\n" "}\n"); write_temp_file(tmp, "parity.tsx", "function tsxReferenceTarget(): void {}\n" "function tsxReferenceAccept(callback: () => void): void {}\n" "function tsxReferenceSite(): void {\n" " tsxReferenceAccept(tsxReferenceTarget);\n" "}\n"); write_temp_file(tmp, "parity.cpp", "using cpp_reference_callback_t = void (*)();\n" "void cppReferenceTarget() {}\n" "void cppReferenceAccept(cpp_reference_callback_t callback) {}\n" "void cppReferenceSite() { cppReferenceAccept(cppReferenceTarget); }\n"); write_temp_file(tmp, "parity.cu", "typedef void (*cuda_reference_callback_t)(void);\n" "__device__ void cudaReferenceTarget(void) {}\n" "__device__ void cudaReferenceAccept(cuda_reference_callback_t callback) {}\n" "__device__ void cudaReferenceSite(void) {\n" " cudaReferenceAccept(cudaReferenceTarget);\n" "}\n"); write_temp_file(tmp, "JavaParity.java", "interface JavaParityTask { void run(); }\n" "class JavaParity {\n" " static void javaReferenceTarget() {}\n" " static void javaReferenceAccept(JavaParityTask callback) {}\n" " static void javaReferenceSite() { " "javaReferenceAccept(JavaParity::javaReferenceTarget); }\n" "}\n"); write_temp_file(tmp, "parity.kt", "fun kotlinReferenceTarget(): Unit {}\n" "fun kotlinReferenceAccept(callback: () -> Unit): Unit {}\n" "fun kotlinReferenceSite(): Unit { " "kotlinReferenceAccept(::kotlinReferenceTarget) }\n"); write_temp_file(tmp, "parity.php", " {};\n" "}\n"); write_temp_file(tmp, "shadow.ts", "function typescriptShadowTarget(): void {}\n" "function typescriptShadowAccept(callback: () => void): void {}\n" "function typescriptShadowSite(): void {\n" " typescriptShadowAccept(typescriptShadowTarget);\n" " const typescriptShadowTarget = (): void => {};\n" "}\n"); /* Fifteen positive and three shadow fixtures plus 34 source fillers exceed * the 50-file fused threshold without making the test larger than needed. */ for (int i = 0; i < 34; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "parity_pad_%02d.ts", i); snprintf(body, sizeof(body), "export function parityPad%02d(): number { return %d; }\n", i, i); write_temp_file(tmp, name, body); } int sequential_reference[pair_count]; int sequential_usage[pair_count]; int sequential_calls[pair_count]; int parallel_reference[pair_count]; int parallel_usage[pair_count]; int parallel_calls[pair_count]; int sequential_shadow_leaks[shadow_count]; int parallel_shadow_leaks[shadow_count]; int sequential_shadow_controls[shadow_count]; int parallel_shadow_controls[shadow_count]; for (int i = 0; i < pair_count; i++) { sequential_reference[i] = sequential_usage[i] = sequential_calls[i] = -1; parallel_reference[i] = parallel_usage[i] = parallel_calls[i] = -1; } for (int i = 0; i < shadow_count; i++) { sequential_shadow_leaks[i] = parallel_shadow_leaks[i] = -1; sequential_shadow_controls[i] = parallel_shadow_controls[i] = -1; } int sequential_total = -1; int parallel_total = -1; int sequential_long_reference = -1; int sequential_long_usage = -1; int sequential_long_calls = -1; int parallel_long_reference = -1; int parallel_long_usage = -1; int parallel_long_calls = -1; NamedEdgePropertyObservation sequential_long_property = {0}; NamedEdgePropertyObservation parallel_long_property = {0}; char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1); char sequential_db_path[512]; snprintf(sequential_db_path, sizeof(sequential_db_path), "%s/reference_sequential.db", tmp); cbm_pipeline_t *sequential = cbm_pipeline_new(tmp, sequential_db_path, CBM_MODE_FULL); int sequential_run_rc = sequential ? cbm_pipeline_run(sequential) : -1; const char *sequential_project = sequential ? cbm_pipeline_project_name(sequential) : NULL; cbm_store_t *sequential_store = cbm_store_open_path(sequential_db_path); bool sequential_store_opened = sequential_store != NULL; if (sequential_store && sequential_project) { sequential_total = cbm_store_count_edges_by_type(sequential_store, sequential_project, "CALL_REFERENCE"); for (int i = 0; i < pair_count; i++) { sequential_reference[i] = named_edge_count(sequential_store, sequential_project, "CALL_REFERENCE", pairs[i].source_name, pairs[i].target_name); sequential_usage[i] = named_edge_count(sequential_store, sequential_project, "USAGE", pairs[i].source_name, pairs[i].target_name); sequential_calls[i] = named_edge_count(sequential_store, sequential_project, "CALLS", pairs[i].source_name, pairs[i].target_name); } for (int i = 0; i < shadow_count; i++) { sequential_shadow_leaks[i] = named_edge_count(sequential_store, sequential_project, "CALL_REFERENCE", shadow_pairs[i].source_name, shadow_pairs[i].target_name) + named_edge_count(sequential_store, sequential_project, "USAGE", shadow_pairs[i].source_name, shadow_pairs[i].target_name) + named_edge_count(sequential_store, sequential_project, "CALLS", shadow_pairs[i].source_name, shadow_pairs[i].target_name); sequential_shadow_controls[i] = named_edge_count(sequential_store, sequential_project, "CALLS", shadow_controls[i].source_name, shadow_controls[i].target_name); } sequential_long_reference = named_edge_count(sequential_store, sequential_project, "CALL_REFERENCE", "longPropertiesReferenceSite", long_reference_name); sequential_long_usage = named_edge_count(sequential_store, sequential_project, "USAGE", "longPropertiesReferenceSite", long_reference_name); sequential_long_calls = named_edge_count(sequential_store, sequential_project, "CALLS", "longPropertiesReferenceSite", long_reference_name); sequential_long_property = observe_named_edge_callee_property( sequential_db_path, sequential_project, "CALL_REFERENCE", "longPropertiesReferenceSite", long_reference_name, long_reference_name); cbm_store_close(sequential_store); } cbm_pipeline_free(sequential); cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); cbm_setenv("CBM_WORKERS", "4", 1); char parallel_db_path[512]; snprintf(parallel_db_path, sizeof(parallel_db_path), "%s/reference_parallel.db", tmp); cbm_pipeline_t *parallel = cbm_pipeline_new(tmp, parallel_db_path, CBM_MODE_FULL); int parallel_run_rc = parallel ? cbm_pipeline_run(parallel) : -1; const char *parallel_project = parallel ? cbm_pipeline_project_name(parallel) : NULL; cbm_store_t *parallel_store = cbm_store_open_path(parallel_db_path); bool parallel_store_opened = parallel_store != NULL; if (parallel_store && parallel_project) { parallel_total = cbm_store_count_edges_by_type(parallel_store, parallel_project, "CALL_REFERENCE"); for (int i = 0; i < pair_count; i++) { parallel_reference[i] = named_edge_count(parallel_store, parallel_project, "CALL_REFERENCE", pairs[i].source_name, pairs[i].target_name); parallel_usage[i] = named_edge_count(parallel_store, parallel_project, "USAGE", pairs[i].source_name, pairs[i].target_name); parallel_calls[i] = named_edge_count(parallel_store, parallel_project, "CALLS", pairs[i].source_name, pairs[i].target_name); } for (int i = 0; i < shadow_count; i++) { parallel_shadow_leaks[i] = named_edge_count(parallel_store, parallel_project, "CALL_REFERENCE", shadow_pairs[i].source_name, shadow_pairs[i].target_name) + named_edge_count(parallel_store, parallel_project, "USAGE", shadow_pairs[i].source_name, shadow_pairs[i].target_name) + named_edge_count(parallel_store, parallel_project, "CALLS", shadow_pairs[i].source_name, shadow_pairs[i].target_name); parallel_shadow_controls[i] = named_edge_count(parallel_store, parallel_project, "CALLS", shadow_controls[i].source_name, shadow_controls[i].target_name); } parallel_long_reference = named_edge_count(parallel_store, parallel_project, "CALL_REFERENCE", "longPropertiesReferenceSite", long_reference_name); parallel_long_usage = named_edge_count(parallel_store, parallel_project, "USAGE", "longPropertiesReferenceSite", long_reference_name); parallel_long_calls = named_edge_count(parallel_store, parallel_project, "CALLS", "longPropertiesReferenceSite", long_reference_name); parallel_long_property = observe_named_edge_callee_property( parallel_db_path, parallel_project, "CALL_REFERENCE", "longPropertiesReferenceSite", long_reference_name, long_reference_name); cbm_store_close(parallel_store); } cbm_pipeline_free(parallel); if (saved_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); free(saved_workers); } else { cbm_unsetenv("CBM_WORKERS"); } if (saved_single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); free(saved_single); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } th_rmtree(tmp); ASSERT_EQ(sequential_run_rc, 0); ASSERT_TRUE(sequential_store_opened); ASSERT_EQ(parallel_run_rc, 0); ASSERT_TRUE(parallel_store_opened); ASSERT_EQ(sequential_long_reference, 1); ASSERT_EQ(sequential_long_usage, 0); ASSERT_EQ(sequential_long_calls, 0); ASSERT_TRUE(sequential_long_property.database_opened); ASSERT_TRUE(sequential_long_property.query_ready); ASSERT_TRUE(sequential_long_property.query_completed); ASSERT_EQ(sequential_long_property.row_count, 1); ASSERT_EQ(sequential_long_property.valid_json_count, 1); ASSERT_EQ(sequential_long_property.exact_callee_count, 1); ASSERT_EQ(parallel_long_reference, 1); ASSERT_EQ(parallel_long_usage, 0); ASSERT_EQ(parallel_long_calls, 0); ASSERT_TRUE(parallel_long_property.database_opened); ASSERT_TRUE(parallel_long_property.query_ready); ASSERT_TRUE(parallel_long_property.query_completed); ASSERT_EQ(parallel_long_property.row_count, 1); ASSERT_EQ(parallel_long_property.valid_json_count, 1); ASSERT_EQ(parallel_long_property.exact_callee_count, 1); ASSERT_EQ(parallel_total, sequential_total); int expected_total = 0; for (int i = 0; i < pair_count; i++) { ASSERT_GT(sequential_reference[i], 0); ASSERT_EQ(parallel_reference[i], sequential_reference[i]); ASSERT_EQ(sequential_usage[i], 0); ASSERT_EQ(parallel_usage[i], 0); ASSERT_EQ(sequential_calls[i], 0); ASSERT_EQ(parallel_calls[i], 0); expected_total += sequential_reference[i]; } expected_total += sequential_long_reference; for (int i = 0; i < shadow_count; i++) { ASSERT_EQ(sequential_shadow_controls[i], 1); ASSERT_EQ(parallel_shadow_controls[i], 1); ASSERT_EQ(sequential_shadow_leaks[i], 0); ASSERT_EQ(parallel_shadow_leaks[i], 0); } ASSERT_EQ(sequential_total, expected_total); PASS(); } /* Reproduce-first for the multi-worker determinism gap distilled from #1925: * the complexity pass walked Function/Method nodes and their CALLS targets in * temp-id order. Extract workers draw ids from one shared counter, so that * order is worker-scheduling order, and the cycle guard flags whichever member * of a mutual-recursion cycle the DFS enters first. Run to run, `recursive` * therefore flipped between the members of a cycle while the CALLS edge set * stayed identical -- a violation of the MT-byte-identical invariant. The * fixture holds 24 two-member and 6 three-member cycles, one function per * file, whose members are equal-sized (adjacent in the size-ordered work * queue), which is what makes the flip likely. The * fixture exceeds MIN_FILES_FOR_PARALLEL; CBM_INDEX_SINGLE_THREAD forces the * reference run through the sequential path and CBM_WORKERS forces the * repeated runs through pass_parallel.c, exactly like the parity test above. * Every multi-worker run must match the sequential one byte for byte. */ enum { CX_ORDER_MT_RUNS = 6, CX_ORDER_LINE_MAX = 512 }; static const char *cx_order_fixture_dir(void) { return "tests/fixtures/complexity_pass_cycle_order"; } /* Copy the regular files of a flat fixture directory into dst_dir. Returns the * number of files copied, or -1 when one could not be read whole. */ static int cx_order_copy_fixture(const char *src_dir, const char *dst_dir) { cbm_dir_t *d = cbm_opendir(src_dir); if (!d) { return -1; } int copied = 0; cbm_dirent_t *entry; while ((entry = cbm_readdir(d)) != NULL) { if (entry->name[0] == '.' || entry->is_dir) { continue; } char src[CBM_SZ_1K]; snprintf(src, sizeof(src), "%s/%s", src_dir, entry->name); FILE *in = cbm_fopen(src, "rb"); if (!in) { copied = -1; break; } char buf[CBM_SZ_4K]; size_t n = fread(buf, 1, sizeof(buf) - 1, in); fclose(in); if (n == sizeof(buf) - 1) { copied = -1; /* fixture files are tiny; a full buffer means truncation */ break; } buf[n] = '\0'; write_temp_file(dst_dir, entry->name, buf); copied++; } cbm_closedir(d); return copied; } static int cx_order_cmp_node_qn(const void *pa, const void *pb) { const cbm_node_t *a = *(const cbm_node_t *const *)pa; const cbm_node_t *b = *(const cbm_node_t *const *)pb; return strcmp(a->qualified_name ? a->qualified_name : "", b->qualified_name ? b->qualified_name : ""); } /* One line per Function in qualified-name order: " ". * Sorting by name keeps DB ids and row order out of the comparison. Returns a * heap string, or NULL when the store cannot be read. */ static char *cx_order_signature(const char *db_path, const char *project, int *func_count) { cbm_store_t *s = cbm_store_open_path(db_path); if (!s) { return NULL; } cbm_node_t *funcs = NULL; int count = 0; if (cbm_store_find_nodes_by_label(s, project, "Function", &funcs, &count) != CBM_STORE_OK) { cbm_store_close(s); return NULL; } const cbm_node_t **sorted = calloc((size_t)count + 1, sizeof(*sorted)); char *sig = calloc((size_t)count + 1, CX_ORDER_LINE_MAX); if (sorted && sig) { for (int i = 0; i < count; i++) { sorted[i] = &funcs[i]; } qsort(sorted, (size_t)count, sizeof(*sorted), cx_order_cmp_node_qn); size_t used = 0; for (int i = 0; i < count; i++) { const char *props = sorted[i]->properties_json ? sorted[i]->properties_json : "{}"; const char *tld = strstr(props, "\"transitive_loop_depth\":"); const char *rec = strstr(props, "\"recursive\":"); int w = snprintf(sig + used, CX_ORDER_LINE_MAX, "%s %.*s %.*s\n", sorted[i]->qualified_name ? sorted[i]->qualified_name : "", tld ? (int)strcspn(tld, ",}") : 0, tld ? tld : "", rec ? (int)strcspn(rec, ",}") : 0, rec ? rec : ""); if (w < 0) { w = 0; } else if (w >= CX_ORDER_LINE_MAX) { w = CX_ORDER_LINE_MAX - 1; } used += (size_t)w; } } else { free(sig); sig = NULL; } free(sorted); cbm_store_free_nodes(funcs, count); cbm_store_close(s); *func_count = count; return sig; } /* First line of `got` that differs from `want`, copied into out (for the * failure diagnostic). Returns false when the strings are identical. */ static bool cx_order_first_diff(const char *want, const char *got, char *out, size_t cap) { if (strcmp(want, got) == 0) { return false; } while (*want && *got) { size_t lw = strcspn(want, "\n"); size_t lg = strcspn(got, "\n"); if (lw != lg || memcmp(want, got, lw) != 0) { snprintf(out, cap, "want '%.*s' got '%.*s'", (int)lw, want, (int)lg, got); return true; } want += lw + (want[lw] == '\n'); got += lg + (got[lg] == '\n'); } snprintf(out, cap, "line count differs"); return true; } TEST(pipeline_complexity_props_independent_of_worker_order) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_cx_order_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } int copied = cx_order_copy_fixture(cx_order_fixture_dir(), tmp); if (copied <= 0) { th_rmtree(tmp); FAIL("fixture copy"); } char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/cx_sequential.db", tmp); cbm_pipeline_t *sequential = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); int sequential_rc = sequential ? cbm_pipeline_run(sequential) : -1; int sequential_funcs = 0; char *sequential_sig = NULL; if (sequential && sequential_rc == 0) { sequential_sig = cx_order_signature(db_path, cbm_pipeline_project_name(sequential), &sequential_funcs); } cbm_pipeline_free(sequential); cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); cbm_setenv("CBM_WORKERS", "4", 1); int parallel_rc[CX_ORDER_MT_RUNS]; char *parallel_sig[CX_ORDER_MT_RUNS]; for (int r = 0; r < CX_ORDER_MT_RUNS; r++) { snprintf(db_path, sizeof(db_path), "%s/cx_parallel_%d.db", tmp, r); cbm_pipeline_t *parallel = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); parallel_rc[r] = parallel ? cbm_pipeline_run(parallel) : -1; parallel_sig[r] = NULL; if (parallel && parallel_rc[r] == 0) { int funcs = 0; parallel_sig[r] = cx_order_signature(db_path, cbm_pipeline_project_name(parallel), &funcs); } cbm_pipeline_free(parallel); } if (saved_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); free(saved_workers); } else { cbm_unsetenv("CBM_WORKERS"); } if (saved_single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); free(saved_single); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } th_rmtree(tmp); /* Verdicts first, then release, then assert: the assertions must not leak. */ bool cycles_detected = sequential_sig && strstr(sequential_sig, "\"recursive\":true") != NULL; int mismatch_run = -1; char diff[CBM_SZ_1K] = ""; for (int r = 0; r < CX_ORDER_MT_RUNS && mismatch_run < 0; r++) { if (parallel_rc[r] != 0 || !parallel_sig[r]) { mismatch_run = r; snprintf(diff, sizeof(diff), "run %d rc=%d", r, parallel_rc[r]); } else if (sequential_sig && cx_order_first_diff(sequential_sig, parallel_sig[r], diff, sizeof(diff))) { mismatch_run = r; } } free(sequential_sig); for (int r = 0; r < CX_ORDER_MT_RUNS; r++) { free(parallel_sig[r]); } ASSERT_EQ(sequential_rc, 0); ASSERT_NOT_NULL(sequential_sig); ASSERT_GTE(sequential_funcs, copied); /* at least the one function per fixture file */ ASSERT_TRUE(cycles_detected); /* the cycles must reach the pass at all */ if (mismatch_run >= 0) { printf("\n parallel run %d diverges from sequential: %s\n", mismatch_run, diff); FAIL("complexity props depend on worker id order"); } PASS(); } #ifdef _WIN32 /* utimensat/AT_FDCWD do not exist on Windows. Set the same instant through * SetFileTime: FILETIME is 100ns ticks since 1601, the same representation * cbm_path_info_utf8 reads back, so the round-trip loses nothing the * incremental pipeline can observe. */ static int pipeline_test_set_mtime(const char *path, time_t seconds, long nanoseconds) { uint64_t ticks = (uint64_t)seconds * UINT64_C(10000000) + (uint64_t)nanoseconds / 100U + UINT64_C(116444736000000000); FILETIME ft = {.dwLowDateTime = (DWORD)(ticks & 0xFFFFFFFFU), .dwHighDateTime = (DWORD)(ticks >> 32)}; wchar_t *wpath = cbm_utf8_to_wide(path); if (!wpath) { return -1; } HANDLE h = CreateFileW(wpath, FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); free(wpath); if (h == INVALID_HANDLE_VALUE) { return -1; } BOOL ok = SetFileTime(h, NULL, &ft, &ft); CloseHandle(h); return ok ? 0 : -1; } #else static int pipeline_test_set_mtime(const char *path, time_t seconds, long nanoseconds) { struct timespec times[2] = {{.tv_sec = seconds, .tv_nsec = nanoseconds}, {.tv_sec = seconds, .tv_nsec = nanoseconds}}; return utimensat(AT_FDCWD, path, times, 0); } #endif /* Reindexing a changed callable-value occurrence must replace its exact target, * not preserve the old CALL_REFERENCE alongside the new one or regress the new * occurrence to USAGE/CALLS. Equal-length source generations use the exact same * mtime so metadata heuristics cannot make the test pass: byte hashes must route * the semantic change to a full rebuild. A metadata-only mtime change is a true * no-op and must not rebuild. */ TEST(pipeline_incremental_repoints_call_reference_without_stale_edge) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_ref_incr_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } static const char initial_source[] = "package refs\n" "func alphaReferenceTarget() {}\n" "func bravoReferenceTarget() {}\n" "func incrementalReferenceAccept(callback func()) {}\n" "func incrementalReferenceSite() { " "incrementalReferenceAccept(alphaReferenceTarget) }\n"; static const char replacement_source[] = "package refs\n" "func alphaReferenceTarget() {}\n" "func bravoReferenceTarget() {}\n" "func incrementalReferenceAccept(callback func()) {}\n" "func incrementalReferenceSite() { " "incrementalReferenceAccept(bravoReferenceTarget) }\n"; ASSERT_EQ(sizeof(initial_source), sizeof(replacement_source)); write_temp_file(tmp, "refs.go", initial_source); char source_path[512]; snprintf(source_path, sizeof(source_path), "%s/refs.go", tmp); const time_t fixed_mtime = 1700000000; ASSERT_EQ(pipeline_test_set_mtime(source_path, fixed_mtime, 123456789L), 0); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/reference_incremental.db", tmp); cbm_pipeline_t *first = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(first); ASSERT_EQ(cbm_pipeline_run(first), 0); const char *first_project = cbm_pipeline_project_name(first); cbm_store_t *first_store = cbm_store_open_path(db_path); ASSERT_NOT_NULL(first_store); ASSERT_EQ(named_edge_count(first_store, first_project, "CALL_REFERENCE", "incrementalReferenceSite", "alphaReferenceTarget"), 1); ASSERT_EQ(named_edge_count(first_store, first_project, "CALL_REFERENCE", "incrementalReferenceSite", "bravoReferenceTarget"), 0); ASSERT_EQ(named_edge_count(first_store, first_project, "USAGE", "incrementalReferenceSite", "alphaReferenceTarget"), 0); ASSERT_EQ(named_edge_count(first_store, first_project, "CALLS", "incrementalReferenceSite", "alphaReferenceTarget"), 0); cbm_file_hash_t stored_hash = {0}; ASSERT_EQ(cbm_store_get_file_hash(first_store, first_project, "refs.go", &stored_hash), CBM_STORE_OK); char expected_hash[CBM_SHA256_HEX_LEN + 1]; cbm_sha256_hex(initial_source, strlen(initial_source), expected_hash); ASSERT_NOT_NULL(stored_hash.sha256); ASSERT_STR_EQ(stored_hash.sha256, expected_hash); cbm_store_clear_file_hash(&stored_hash); cbm_coverage_meta_t baseline_meta = {0}; ASSERT_EQ(cbm_store_coverage_meta_get(first_store, first_project, &baseline_meta), CBM_STORE_OK); ASSERT_EQ(baseline_meta.coverage_version, CBM_SEMANTIC_INDEX_VERSION); ASSERT_TRUE(baseline_meta.hash_records_complete); cbm_store_coverage_meta_clear(&baseline_meta); cbm_store_close(first_store); cbm_pipeline_free(first); /* Same bytes, metadata only: exact manifest equality is a no-op. */ ASSERT_EQ(pipeline_test_set_mtime(source_path, fixed_mtime + 1, 123456789L), 0); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *metadata_only = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(metadata_only); ASSERT_EQ(cbm_pipeline_run(metadata_only), 0); ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_NOOP); cbm_pipeline_free(metadata_only); write_temp_file(tmp, "refs.go", replacement_source); ASSERT_EQ(pipeline_test_set_mtime(source_path, fixed_mtime, 123456789L), 0); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *second = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(second); ASSERT_EQ(cbm_pipeline_run(second), 0); /* Since closure repair landed, a single-file content change with no * added names routes to CLOSURE_REPAIR instead of a forced full * rebuild; the convergence assertions below are unchanged and now pin * the repaired graph against the same expectations. */ ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); const char *second_project = cbm_pipeline_project_name(second); cbm_store_t *second_store = cbm_store_open_path(db_path); ASSERT_NOT_NULL(second_store); ASSERT_EQ(named_edge_count(second_store, second_project, "CALL_REFERENCE", "incrementalReferenceSite", "alphaReferenceTarget"), 0); ASSERT_EQ(named_edge_count(second_store, second_project, "CALL_REFERENCE", "incrementalReferenceSite", "bravoReferenceTarget"), 1); ASSERT_EQ(named_edge_count(second_store, second_project, "USAGE", "incrementalReferenceSite", "bravoReferenceTarget"), 0); ASSERT_EQ(named_edge_count(second_store, second_project, "CALLS", "incrementalReferenceSite", "bravoReferenceTarget"), 0); cbm_store_close(second_store); cbm_pipeline_free(second); th_rmtree(tmp); PASS(); } /* #1419: every index publishes through a private staging DB in WAL mode that * cbm_store_seal_for_atomic_publish() checkpoints at synchronous=FULL and * switches to DELETE mode before the atomic rename. A crash before that rename * discards the stage, so a WAL commit at FULL only adds an fsync per * transaction without protecting anything; SQLite documents NORMAL as * corruption-safe under WAL. The probe reads the EFFECTIVE synchronous level * of the committing handle from a WAL hook on every connection the run opens. * * SQLite installs its default autocheckpoint WAL hook after auto-extensions * run, so the auto-extension installs a statement trace and the trace installs * the probe once the connection is fully open. The probe keeps SQLite's * default 1000-frame autocheckpoint, so it does not change what the run * writes. */ enum { I1419_SYNC_FULL = 2, I1419_DEFAULT_AUTOCHECKPOINT = 1000 }; static atomic_int g_i1419_wal_commits; static atomic_int g_i1419_full_sync_commits; static int i1419_wal_probe(void *arg, sqlite3 *db, const char *db_name, int wal_frames) { (void)arg; atomic_fetch_add(&g_i1419_wal_commits, 1); sqlite3_stmt *stmt = NULL; if (sqlite3_prepare_v2(db, "PRAGMA synchronous;", -1, &stmt, NULL) == SQLITE_OK && sqlite3_step(stmt) == SQLITE_ROW && sqlite3_column_int(stmt, 0) >= I1419_SYNC_FULL) { atomic_fetch_add(&g_i1419_full_sync_commits, 1); } sqlite3_finalize(stmt); if (wal_frames >= I1419_DEFAULT_AUTOCHECKPOINT) { (void)sqlite3_wal_checkpoint(db, db_name); } return SQLITE_OK; } static int i1419_trace(unsigned type, void *ctx, void *stmt, void *sql) { (void)type; (void)ctx; (void)sql; (void)sqlite3_wal_hook(sqlite3_db_handle((sqlite3_stmt *)stmt), i1419_wal_probe, NULL); return 0; } static int i1419_install_probe(sqlite3 *db, char **errmsg, const sqlite3_api_routines *api) { (void)errmsg; (void)api; return sqlite3_trace_v2(db, SQLITE_TRACE_STMT, i1419_trace, NULL); } static int i1419_index(const char *repo, const char *db_path) { cbm_pipeline_t *p = cbm_pipeline_new(repo, db_path, CBM_MODE_FULL); if (!p) { return -1; } int rc = cbm_pipeline_run(p); cbm_pipeline_free(p); return rc; } TEST(pipeline_staged_publish_commits_below_full_sync_issue1419) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_i1419_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } /* Equal lengths + equal mtimes: the second run must route on content * bytes to the closure-repair (delta) publish, not a full rebuild. */ static const char initial_source[] = "package syncprobe\n" "func alphaSyncTarget() {}\n" "func bravoSyncTarget() {}\n" "func syncProbeAccept(callback func()) {}\n" "func syncProbeSite() { syncProbeAccept(alphaSyncTarget) }\n"; static const char replacement_source[] = "package syncprobe\n" "func alphaSyncTarget() {}\n" "func bravoSyncTarget() {}\n" "func syncProbeAccept(callback func()) {}\n" "func syncProbeSite() { syncProbeAccept(bravoSyncTarget) }\n"; ASSERT_EQ(sizeof(initial_source), sizeof(replacement_source)); write_temp_file(tmp, "probe.go", initial_source); char source_path[512]; snprintf(source_path, sizeof(source_path), "%s/probe.go", tmp); const time_t fixed_mtime = 1700000000; ASSERT_EQ(pipeline_test_set_mtime(source_path, fixed_mtime, 123456789L), 0); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/i1419.db", tmp); atomic_store(&g_i1419_wal_commits, 0); atomic_store(&g_i1419_full_sync_commits, 0); ASSERT_EQ(sqlite3_auto_extension((void (*)(void))i1419_install_probe), SQLITE_OK); /* Both publish routes run before any assertion, so the probe is always * unregistered again even when a run fails. */ int full_rc = i1419_index(tmp, db_path); write_temp_file(tmp, "probe.go", replacement_source); int mtime_rc = pipeline_test_set_mtime(source_path, fixed_mtime, 123456789L); cbm_pipeline_incremental_test_reset_faults(); int delta_rc = i1419_index(tmp, db_path); cbm_incremental_route_t delta_route = cbm_pipeline_incremental_test_last_route(); (void)sqlite3_cancel_auto_extension((void (*)(void))i1419_install_probe); th_rmtree(tmp); ASSERT_EQ(full_rc, 0); ASSERT_EQ(mtime_rc, 0); ASSERT_EQ(delta_rc, 0); ASSERT_EQ(delta_route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); /* Non-vacuous: both publishes commit through the WAL. */ ASSERT_GT(atomic_load(&g_i1419_wal_commits), 0); ASSERT_EQ(atomic_load(&g_i1419_full_sync_commits), 0); PASS(); } /* Kubernetes inputs reach past their own file: a Service selects a workload * in another file of its bundle, and a kustomization decides the namespace * of every object it builds. The closure repair re-runs the K8s pass over * its plan only, so editing either must still leave the graph a fresh index * builds. */ static int k8s_inc_index(const char *repo, const char *db) { cbm_pipeline_t *p = cbm_pipeline_new(repo, db, CBM_MODE_FULL); if (!p) { return -1; } (void)cbm_pipeline_set_project_name(p, "k8sinc"); cbm_pipeline_set_persistence(p, false); int rc = cbm_pipeline_run(p); cbm_pipeline_free(p); return rc; } static int k8s_inc_count(const char *db_path, const char *sql) { sqlite3 *db = NULL; int n = -1; if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, NULL) == SQLITE_OK) { sqlite3_stmt *stmt = NULL; if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK && sqlite3_step(stmt) == SQLITE_ROW) { n = sqlite3_column_int(stmt, 0); } sqlite3_finalize(stmt); } sqlite3_close(db); return n; } TEST(incremental_k8s_edits_reach_other_files) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_k8sinc_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "kustomization.yaml", "resources:\n - svc.yaml\n - deploy.yaml\n"); write_temp_file(tmp, "svc.yaml", "apiVersion: v1\nkind: Service\nmetadata:\n name: db\nspec:\n" " selector:\n app: db\n"); write_temp_file(tmp, "deploy.yaml", "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: db\nspec:\n" " template:\n metadata:\n labels:\n app: db\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/k8sinc.db", tmp); static const char links[] = "SELECT COUNT(*) FROM edges WHERE type='INFRA_MAPS'"; static const char shop[] = "SELECT COUNT(*) FROM nodes WHERE label='Resource' AND " "json_extract(properties,'$.obj_key')='apps/Deployment/shop/db'"; int rc_full = k8s_inc_index(tmp, db_path); int links_full = k8s_inc_count(db_path, links); /* the Service's file changes, the Deployment's does not */ write_temp_file(tmp, "svc.yaml", "apiVersion: v1\nkind: Service\nmetadata:\n name: db\nspec:\n" " selector:\n app: db\n# edited\n"); int rc_svc = k8s_inc_index(tmp, db_path); int links_after_svc = k8s_inc_count(db_path, links); /* the build gains a namespace: deploy.yaml's object lives in it now */ write_temp_file(tmp, "kustomization.yaml", "namespace: shop\nresources:\n - svc.yaml\n - deploy.yaml\n"); int rc_kust = k8s_inc_index(tmp, db_path); int shop_after_kust = k8s_inc_count(db_path, shop); th_rmtree(tmp); ASSERT_EQ(rc_full, 0); ASSERT_EQ(links_full, 1); ASSERT_EQ(rc_svc, 0); ASSERT_EQ(links_after_svc, 1); ASSERT_EQ(rc_kust, 0); ASSERT_EQ(shop_after_kust, 1); PASS(); } /* A script's deploy commands link nodes of other files (pass_shell.c): the * closure repair re-reads the changed script alone, so editing it must still * leave the edges a fresh index builds. */ TEST(incremental_shell_deploy_edit_keeps_its_edges) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_shinc_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "manifests/app.yaml", "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: app\n"); write_temp_file(tmp, "hack/deploy.sh", "#!/bin/sh\ncd \"$(dirname \"$0\")/..\"\nkubectl apply -f manifests/app.yaml\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/shinc.db", tmp); static const char deploys[] = "SELECT COUNT(*) FROM edges WHERE type='DEPLOYS'"; int rc_full = k8s_inc_index(tmp, db_path); int deploys_full = k8s_inc_count(db_path, deploys); write_temp_file(tmp, "hack/deploy.sh", "#!/bin/sh\ncd \"$(dirname \"$0\")/..\"\nkubectl apply -f manifests/app.yaml\n" "# edited\n"); int rc_edit = k8s_inc_index(tmp, db_path); cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); int deploys_after = k8s_inc_count(db_path, deploys); th_rmtree(tmp); ASSERT_EQ(rc_full, 0); ASSERT_EQ(deploys_full, 1); ASSERT_EQ(rc_edit, 0); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); ASSERT_EQ(deploys_after, 1); PASS(); } /* Deleting the only file that starts a program leaves no Process node * behind -- a full index never makes one without a SPAWNS edge. One script * (pass_shell.c) and one Python call (pass_spawns.c) start programs; the * program a kept script starts stays. * - `legacy`: both go at once, through the full-load route. * - otherwise in two steps: the script alone goes through closure repair, * then the Python file, whose deletion the Python namespace admission * declines (a deleted module's names leave its importers' namespaces), so * it takes the full route. * Returns the Process count after the last deletion, or -1. *route: the route * of the first (or only) step; *after_script and *python_route: the count and * the route after the script-only step and the Python step (two-step mode). */ static int incremental_spawner_delete(bool legacy, cbm_incremental_route_t *route, int *after_script, cbm_incremental_route_t *python_route) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_spawngc_XXXXXX"); if (!cbm_mkdtemp(tmp)) { return -1; } write_temp_file(tmp, "hack/a.sh", "#!/bin/sh\n./bin/tool-a --x\n"); write_temp_file(tmp, "hack/b.sh", "#!/bin/sh\n./bin/tool-b\n"); write_temp_file(tmp, "app.py", "import subprocess\n\n\ndef run():\n subprocess.run([\"tool-c\"])\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/spawngc.db", tmp); static const char processes[] = "SELECT COUNT(*) FROM nodes WHERE label='Process'"; int full = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, processes) : -1; char path[512]; snprintf(path, sizeof(path), "%s/hack/a.sh", tmp); (void)remove(path); if (!legacy) { *after_script = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, processes) : -1; *route = cbm_pipeline_incremental_test_last_route(); } snprintf(path, sizeof(path), "%s/app.py", tmp); (void)remove(path); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API if (legacy) { cbm_pipeline_incremental_test_force_legacy_partial_once(); } #endif int after = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, processes) : -1; *(legacy ? route : python_route) = cbm_pipeline_incremental_test_last_route(); th_rmtree(tmp); return full == 3 ? after : -1; } TEST(incremental_deleted_spawner_drops_its_process) { cbm_incremental_route_t route = CBM_INCREMENTAL_ROUTE_NONE; cbm_incremental_route_t python_route = CBM_INCREMENTAL_ROUTE_NONE; int after_script = -1; int left = incremental_spawner_delete(false, &route, &after_script, &python_route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); ASSERT_EQ(after_script, 2); /* tool-a gone; tool-b and tool-c stay */ ASSERT_EQ(python_route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(left, 1); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API left = incremental_spawner_delete(true, &route, &after_script, &python_route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); ASSERT_EQ(left, 1); #endif PASS(); } /* Deleting the only script requesting a URL leaves no Route behind -- a full * index never makes one without an edge; the Route a kept script requests * stays. `legacy`: through the full-load route, else closure repair. Returns * the Route count after the deletion, or -1; *route: the route taken. */ static int incremental_caller_delete(bool legacy, cbm_incremental_route_t *route) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_routegc_XXXXXX"); if (!cbm_mkdtemp(tmp)) { return -1; } write_temp_file(tmp, "hack/a.sh", "#!/bin/sh\ncurl -fsSL https://dl.example.com/a.tgz\n"); write_temp_file(tmp, "hack/b.sh", "#!/bin/sh\nwget https://dl.example.com/b.tgz\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/routegc.db", tmp); static const char routes[] = "SELECT COUNT(*) FROM nodes WHERE label='Route'"; int full = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, routes) : -1; char path[512]; snprintf(path, sizeof(path), "%s/hack/a.sh", tmp); (void)remove(path); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API if (legacy) { cbm_pipeline_incremental_test_force_legacy_partial_once(); } #endif int after = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, routes) : -1; *route = cbm_pipeline_incremental_test_last_route(); th_rmtree(tmp); return full == 2 ? after : -1; } TEST(incremental_deleted_caller_drops_its_route) { cbm_incremental_route_t route = CBM_INCREMENTAL_ROUTE_NONE; int left = incremental_caller_delete(false, &route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); ASSERT_EQ(left, 1); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API left = incremental_caller_delete(true, &route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); ASSERT_EQ(left, 1); #endif PASS(); } /* Deleting the only Dockerfile naming an image leaves no Image node behind; * the image another Dockerfile still names stays. `legacy`: through the * full-load route, else the closure-repair route. Returns the Image count * after the deletion, or -1; *route: the route taken. */ static int incremental_dockerfile_delete(bool legacy, cbm_incremental_route_t *route) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_imagegc_XXXXXX"); if (!cbm_mkdtemp(tmp)) { return -1; } write_temp_file(tmp, "a/Dockerfile", "FROM alpine:3.20\nCOPY --from=docker.io/bitnami/kubectl:1.30 /k /k\n"); write_temp_file(tmp, "b/Dockerfile", "FROM alpine:3.19\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/imagegc.db", tmp); static const char images[] = "SELECT COUNT(*) FROM nodes WHERE label='Image'"; int full = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, images) : -1; char path[512]; snprintf(path, sizeof(path), "%s/a/Dockerfile", tmp); (void)remove(path); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API if (legacy) { cbm_pipeline_incremental_test_force_legacy_partial_once(); } #else (void)legacy; #endif int after = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, images) : -1; *route = cbm_pipeline_incremental_test_last_route(); th_rmtree(tmp); return full == 2 ? after : -1; } /* Editing one Dockerfile keeps every image's build edge -- also the one * between nodes that both pre-existed (Image -> the unchanged Dockerfile), * which the incremental routes recompute whole. `legacy`: through the * full-load route, else the closure-repair route. Returns the build edge * count before / after as "full=N inc=M", or NULL; *route: the route taken. */ static char *incremental_build_keep(bool legacy, cbm_incremental_route_t *route) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_buildkeep_XXXXXX"); if (!cbm_mkdtemp(tmp)) { return NULL; } write_temp_file(tmp, "src/web/Dockerfile", "FROM nginx:1.27\n"); write_temp_file(tmp, "src/api/Dockerfile", "FROM golang:1.22\n"); write_temp_file(tmp, "skaffold.yaml", "apiVersion: skaffold/v4beta6\nkind: Config\nbuild:\n artifacts:\n" " - image: web\n context: src/web\n" " - image: api\n context: src/api\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/buildkeep.db", tmp); static const char builds[] = "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id WHERE e.type='DEPENDS_ON' " "AND s.label='Image' AND json_extract(e.properties,'$.via')='build'"; int full = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, builds) : -1; write_temp_file(tmp, "src/web/Dockerfile", "FROM nginx:1.27\n# edited\n"); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API if (legacy) { cbm_pipeline_incremental_test_force_legacy_partial_once(); } #else (void)legacy; #endif char *out = NULL; if (k8s_inc_index(tmp, db_path) == 0) { char buf[64]; snprintf(buf, sizeof(buf), "full=%d inc=%d", full, k8s_inc_count(db_path, builds)); out = strdup(buf); } *route = cbm_pipeline_incremental_test_last_route(); th_rmtree(tmp); return out; } TEST(incremental_dockerfile_edit_keeps_build_edges) { cbm_incremental_route_t route = CBM_INCREMENTAL_ROUTE_NONE; char *got = incremental_build_keep(false, &route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, "full=2 inc=2"); free(got); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API got = incremental_build_keep(true, &route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, "full=2 inc=2"); free(got); #endif PASS(); } /* A compose file's edit replaces its build edges: the renamed image keeps * no edge (nor node), the new one has its edge. `legacy`: through the * full-load route, else the closure-repair route. Returns "old=N new=M" * after the edit, or NULL; *route: the route taken. */ static char *incremental_compose_edit(bool legacy, cbm_incremental_route_t *route) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_composeedit_XXXXXX"); if (!cbm_mkdtemp(tmp)) { return NULL; } write_temp_file(tmp, "src/web/Dockerfile", "FROM nginx:1.27\n"); write_temp_file(tmp, "src/api/Dockerfile", "FROM golang:1.22\n"); write_temp_file(tmp, "compose.yaml", "services:\n web:\n image: acme/web-old\n build: ./src/web\n" " api:\n image: acme/api\n build: ./src/api\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/composeedit.db", tmp); static const char old_img[] = "SELECT COUNT(*) FROM nodes WHERE label='Image' AND " "name='acme/web-old'"; static const char new_edge[] = "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id WHERE e.type='DEPENDS_ON' " "AND s.name='acme/web-new'"; if (k8s_inc_index(tmp, db_path) != 0 || k8s_inc_count(db_path, old_img) != 1) { th_rmtree(tmp); return NULL; } write_temp_file(tmp, "compose.yaml", "services:\n web:\n image: acme/web-new\n build: ./src/web\n" " api:\n image: acme/api\n build: ./src/api\n"); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API if (legacy) { cbm_pipeline_incremental_test_force_legacy_partial_once(); } #else (void)legacy; #endif char *out = NULL; if (k8s_inc_index(tmp, db_path) == 0) { char buf[64]; snprintf(buf, sizeof(buf), "old=%d new=%d", k8s_inc_count(db_path, old_img), k8s_inc_count(db_path, new_edge)); out = strdup(buf); } *route = cbm_pipeline_incremental_test_last_route(); th_rmtree(tmp); return out; } TEST(incremental_compose_edit_replaces_build_edges) { cbm_incremental_route_t route = CBM_INCREMENTAL_ROUTE_NONE; char *got = incremental_compose_edit(false, &route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, "old=0 new=1"); free(got); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API got = incremental_compose_edit(true, &route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, "old=0 new=1"); free(got); #endif PASS(); } /* Renaming a variable in a compose service's environment (and, with * `dockerfile`, in a Dockerfile's ENV) replaces its container-scoped EnvVar; * the unchanged compose file and Dockerfile keep theirs, once. `legacy`: * through the full-load route, else the closure-repair route (a renamed ENV * is a new Variable name, which the closure route leaves to a full * rebuild). Returns "old=N new=M keep=K", or NULL. */ static char *incremental_env_edit(bool legacy, bool dockerfile, cbm_incremental_route_t *route) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_envedit_XXXXXX"); if (!cbm_mkdtemp(tmp)) { return NULL; } write_temp_file(tmp, "Dockerfile", dockerfile ? "FROM alpine:3.20\nENV OLD_D=1\n" : "FROM alpine:3.20\nENV KEEP_D=1\n"); write_temp_file(tmp, "compose.yaml", "services:\n web:\n image: acme/web\n environment:\n OLD_C: x\n"); write_temp_file(tmp, "compose.other.yaml", "services:\n api:\n image: acme/api\n environment:\n - KEEP_C\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/envedit.db", tmp); static const char old_q[] = "SELECT COUNT(*) FROM nodes WHERE label='EnvVar' AND name IN ('OLD_C','OLD_D')"; static const char new_q[] = "SELECT COUNT(*) FROM edges e JOIN nodes t ON t.id=e.target_id WHERE " "e.type='CONFIGURES' AND json_extract(e.properties,'$.strategy')='env_set' AND " "t.name IN ('NEW_C','NEW_D')"; static const char keep_q[] = "SELECT COUNT(*) FROM edges e JOIN nodes t ON t.id=e.target_id WHERE " "e.type='CONFIGURES' AND json_extract(e.properties,'$.strategy')='env_set' AND " "t.name IN ('KEEP_C','KEEP_D')"; if (k8s_inc_index(tmp, db_path) != 0 || k8s_inc_count(db_path, old_q) != (dockerfile ? 2 : 1) || k8s_inc_count(db_path, keep_q) != (dockerfile ? 1 : 2)) { th_rmtree(tmp); return NULL; } if (dockerfile) { write_temp_file(tmp, "Dockerfile", "FROM alpine:3.20\nENV NEW_D=1\n"); } write_temp_file(tmp, "compose.yaml", "services:\n web:\n image: acme/web\n environment:\n NEW_C: x\n"); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API if (legacy) { cbm_pipeline_incremental_test_force_legacy_partial_once(); } #else (void)legacy; #endif char *out = NULL; if (k8s_inc_index(tmp, db_path) == 0) { char buf[64]; snprintf(buf, sizeof(buf), "old=%d new=%d keep=%d", k8s_inc_count(db_path, old_q), k8s_inc_count(db_path, new_q), k8s_inc_count(db_path, keep_q)); out = strdup(buf); } *route = cbm_pipeline_incremental_test_last_route(); th_rmtree(tmp); return out; } TEST(incremental_env_edit_replaces_scoped_envvars) { cbm_incremental_route_t route = CBM_INCREMENTAL_ROUTE_NONE; char *got = incremental_env_edit(false, false, &route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, "old=0 new=1 keep=2"); free(got); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API got = incremental_env_edit(true, false, &route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, "old=0 new=1 keep=2"); free(got); got = incremental_env_edit(true, true, &route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, "old=0 new=2 keep=1"); free(got); #endif PASS(); } /* A repository split by component (pass_docker.c P4.2b). `rename`: component * b becomes bee -- the split changes, and the reference in compose.run.yaml, * a file this run does not repair, must leave the old Image `*-b`: a closure * repair cannot re-point it, so the run rebuilds. Else an edit keeping the * split: a closure repair. Returns "repo=N b=M bimg=K" (runner's references * to the repository Image / to `*-b`, `*-b` Images), or NULL. */ static char *incremental_image_split(bool rename, cbm_incremental_route_t *route) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_imagesplit_XXXXXX"); if (!cbm_mkdtemp(tmp)) { return NULL; } write_temp_file(tmp, "svc/a/Dockerfile", "FROM alpine:3.20\n"); write_temp_file(tmp, "svc/b/Dockerfile", "FROM alpine:3.20\n"); write_temp_file(tmp, "compose.yaml", "services:\n a:\n image: acme/demo:1-a\n build: ./svc/a\n" " b:\n image: acme/demo:1-b\n build: ./svc/b\n"); write_temp_file(tmp, "compose.run.yaml", "services:\n runner:\n image: acme/demo:2-b\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/imagesplit.db", tmp); static const char to_repo[] = "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " "t.id=e.target_id WHERE e.type='USAGE' AND s.name='Service/runner' AND " "t.name='acme/demo'"; static const char to_b[] = "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " "t.id=e.target_id WHERE e.type='USAGE' AND s.name='Service/runner' AND " "t.name='acme/demo:*-b'"; static const char b_img[] = "SELECT COUNT(*) FROM nodes WHERE label='Image' AND name='acme/demo:*-b'"; if (k8s_inc_index(tmp, db_path) != 0 || k8s_inc_count(db_path, to_b) != 1) { th_rmtree(tmp); return NULL; } write_temp_file(tmp, "compose.yaml", rename ? "services:\n a:\n image: acme/demo:1-a\n build: ./svc/a\n" " b:\n image: acme/demo:1-bee\n build: ./svc/b\n" : "services:\n a:\n image: acme/demo:1-a\n build: ./svc/a\n" " environment:\n - MODE\n" " b:\n image: acme/demo:1-b\n build: ./svc/b\n"); char *out = NULL; if (k8s_inc_index(tmp, db_path) == 0) { char buf[64]; snprintf(buf, sizeof(buf), "repo=%d b=%d bimg=%d", k8s_inc_count(db_path, to_repo), k8s_inc_count(db_path, to_b), k8s_inc_count(db_path, b_img)); out = strdup(buf); } *route = cbm_pipeline_incremental_test_last_route(); th_rmtree(tmp); return out; } TEST(incremental_image_split_change_rebuilds) { cbm_incremental_route_t route = CBM_INCREMENTAL_ROUTE_NONE; char *got = incremental_image_split(true, &route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, "repo=1 b=0 bimg=0"); free(got); got = incremental_image_split(false, &route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, "repo=0 b=1 bimg=1"); free(got); PASS(); } /* A chart's values file CONFIGURES its Chart (pass_k8s.c). The edge is drawn * by the K8s pass from the chart directory, which a closure repair of the * values file alone does not see: an edit of it rebuilds, and the edge stays. */ TEST(incremental_helm_values_edit_keeps_chart_edge) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_helmvalues_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "chart/Chart.yaml", "apiVersion: v2\nname: web\nversion: 1.0.0\n"); write_temp_file(tmp, "chart/values.yaml", "replicas: 1\n"); write_temp_file(tmp, "chart/templates/svc.yaml", "apiVersion: v1\nkind: Service\nmetadata:\n name: web\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/helmvalues.db", tmp); static const char edge[] = "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " "t.id=e.target_id WHERE e.type='CONFIGURES' AND s.file_path='chart/values.yaml' AND " "t.label='Chart'"; int before = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, edge) : -1; write_temp_file(tmp, "chart/values.yaml", "replicas: 3\n"); int after = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, edge) : -1; cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); th_rmtree(tmp); ASSERT_EQ(before, 1); ASSERT_EQ(after, 1); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); PASS(); } /* A werf `.helm` dir is a chart without Chart.yaml (pass_k8s.c): its values * file CONFIGURES the werf Chart, an edge a closure repair of the values file * alone does not hold, so the edit rebuilds, like one beside a Chart.yaml. */ TEST(incremental_werf_values_edit_keeps_chart_edge) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_werfchart_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "werf.yaml", "project: site\nconfigVersion: 1\n"); write_temp_file(tmp, ".helm/values.yaml", "replicas: 1\n"); write_temp_file(tmp, ".helm/templates/svc.yaml", "apiVersion: v1\nkind: Service\nmetadata:\n name: web\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/werfchart.db", tmp); static const char edge[] = "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " "t.id=e.target_id WHERE e.type='CONFIGURES' AND s.file_path='.helm/values.yaml' AND " "t.label='Chart' AND t.name='site'"; int before = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, edge) : -1; write_temp_file(tmp, ".helm/values.yaml", "replicas: 3\n"); int after = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, edge) : -1; cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); th_rmtree(tmp); ASSERT_EQ(before, 1); ASSERT_EQ(after, 1); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); PASS(); } /* A kustomization's helmCharts valuesFile (an IMPORTS {local_name: * valuesFile} target) feeds the image the release renders: the values file * edited alone rebuilds, and the release's image follows the edit. */ TEST(incremental_release_values_edit_rebuilds) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_relvalues_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "kz/kustomization.yaml", "apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\n" "helmCharts:\n - name: app\n releaseName: kapp\n" " valuesFile: values-prod.yaml\n"); write_temp_file(tmp, "kz/values-prod.yaml", "image:\n tag: \"2.0\"\n"); write_temp_file(tmp, "kz/charts/app/Chart.yaml", "apiVersion: v2\nname: app\nversion: 1.0.0\n"); write_temp_file(tmp, "kz/charts/app/values.yaml", "image:\n repository: ghcr.io/acme/app\n tag: \"1.0\"\n"); write_temp_file(tmp, "kz/charts/app/templates/deploy.yaml", "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: app\nspec:\n" " template:\n spec:\n containers:\n - name: app\n" " image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/relvalues.db", tmp); static const char v2[] = "SELECT COUNT(*) FROM edges WHERE type='DEPLOYS' AND " "json_extract(properties,'$.image')='ghcr.io/acme/app:2.0'"; static const char v3[] = "SELECT COUNT(*) FROM edges WHERE type='DEPLOYS' AND " "json_extract(properties,'$.image')='ghcr.io/acme/app:3.0'"; int before = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, v2) : -1; write_temp_file(tmp, "kz/values-prod.yaml", "image:\n tag: \"3.0\"\n"); int rc = k8s_inc_index(tmp, db_path); cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); int stale = rc == 0 ? k8s_inc_count(db_path, v2) : -1; int after = rc == 0 ? k8s_inc_count(db_path, v3) : -1; th_rmtree(tmp); ASSERT_EQ(before, 1); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(stale, 0); ASSERT_EQ(after, 1); PASS(); } /* Helm's values as its templates see them (helm_template.c): go-yaml v2 * scalars -- YAML 1.1 booleans, every number a float64 printed like Go's %v * -- --set typing (int64, bool, null, a leading-zero string), coalesce where * a null deletes, and a field of a missing map an error: no value. */ TEST(helm_values_follow_helm_semantics) { yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); ASSERT_NOT_NULL(doc); static const char src[] = "tag: 1.10\nbig: 20231010\nm: 1000000\nyes_: yes\noff_: Off\n" "nul: ~\nq: \"1.10\"\noct: 0755\nkeep: k\ngone: g\n"; yyjson_mut_val *v = cbm_helm_values_parse(doc, src, strlen(src)); ASSERT_NOT_NULL(v); char buf[64]; ASSERT_TRUE(cbm_helm_scalar_text(yyjson_mut_obj_get(v, "tag"), buf, sizeof(buf))); ASSERT_STR_EQ(buf, "1.1"); ASSERT_TRUE(cbm_helm_scalar_text(yyjson_mut_obj_get(v, "big"), buf, sizeof(buf))); ASSERT_STR_EQ(buf, "2.023101e+07"); ASSERT_TRUE(cbm_helm_scalar_text(yyjson_mut_obj_get(v, "m"), buf, sizeof(buf))); ASSERT_STR_EQ(buf, "1e+06"); ASSERT_TRUE(cbm_helm_scalar_text(yyjson_mut_obj_get(v, "oct"), buf, sizeof(buf))); ASSERT_STR_EQ(buf, "493"); ASSERT_TRUE(yyjson_mut_is_true(yyjson_mut_obj_get(v, "yes_"))); ASSERT_TRUE(yyjson_mut_is_false(yyjson_mut_obj_get(v, "off_"))); ASSERT_TRUE(yyjson_mut_is_null(yyjson_mut_obj_get(v, "nul"))); ASSERT_STR_EQ(yyjson_mut_get_str(yyjson_mut_obj_get(v, "q")), "1.10"); ASSERT_TRUE(cbm_helm_values_set(doc, v, "a\\.b.c[1]", "007", false)); ASSERT_TRUE(cbm_helm_values_set(doc, v, "n", "42", false)); ASSERT_TRUE(cbm_helm_values_set(doc, v, "s", "42", true)); yyjson_mut_val *ab = yyjson_mut_obj_get(v, "a.b"); ASSERT_NOT_NULL(ab); yyjson_mut_val *c = yyjson_mut_obj_get(ab, "c"); ASSERT_TRUE(yyjson_mut_is_arr(c)); ASSERT_TRUE(yyjson_mut_is_null(yyjson_mut_arr_get(c, 0))); ASSERT_STR_EQ(yyjson_mut_get_str(yyjson_mut_arr_get(c, 1)), "007"); ASSERT_TRUE(yyjson_mut_is_int(yyjson_mut_obj_get(v, "n"))); ASSERT_TRUE(yyjson_mut_is_str(yyjson_mut_obj_get(v, "s"))); static const char over[] = "gone: null\nkeep: x\nnew: 1\n"; yyjson_mut_val *o = cbm_helm_values_parse(doc, over, strlen(over)); yyjson_mut_val *m = cbm_helm_values_coalesce(doc, v, o); ASSERT_NULL(yyjson_mut_obj_get(m, "gone")); ASSERT_STR_EQ(yyjson_mut_get_str(yyjson_mut_obj_get(m, "keep")), "x"); ASSERT_NOT_NULL(yyjson_mut_obj_get(m, "new")); ASSERT_STR_EQ(yyjson_mut_get_str(yyjson_mut_obj_get(v, "keep")), "k"); /* base untouched */ /* a template: a missing last key renders "", a field of it is an error */ static const char tpl[] = "image: \"{{ .Values.keep }}:{{ .Values.none }}\"\n" "image: \"{{ .Values.none.repo }}\"\n"; CBMArena a; cbm_arena_init_lazy(&a, CBM_SZ_4K); cbm_helm_probe_t probes[2] = {{.line = 1}, {.line = 2}}; cbm_helm_render(&a, "t.yaml", tpl, strlen(tpl), v, NULL, NULL, probes, 2); ASSERT_EQ(probes[0].n, 1); ASSERT_STR_EQ(probes[0].image[0], "k:"); ASSERT_EQ(probes[1].n, 1); ASSERT_NULL(probes[1].image[0]); cbm_arena_destroy(&a); yyjson_mut_doc_free(doc); PASS(); } /* Render template tpl with values src (one probe per `image:` line given) * and return the capture of probe 0 ("" for UNKNOWN, "" when * nothing was captured) -- a copy, freed by the caller. */ static char *helm_capture_chart(const char *vals_src, const char *tpl, const cbm_helm_defines_t *defs, const cbm_helm_chart_meta_t *meta, int line) { yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); yyjson_mut_val *v = doc ? cbm_helm_values_parse(doc, vals_src, strlen(vals_src)) : NULL; CBMArena a; cbm_arena_init_lazy(&a, CBM_SZ_4K); cbm_helm_probe_t probe = {.line = line}; cbm_helm_render(&a, "t.yaml", tpl, strlen(tpl), v, meta, defs, &probe, 1); char *out = strdup(probe.n == 0 ? "" : probe.image[0] ? probe.image[0] : ""); cbm_arena_destroy(&a); if (doc) { yyjson_mut_doc_free(doc); } return out; } /* helm_capture_chart of a template of no chart. */ static char *helm_capture(const char *vals_src, const char *tpl, const cbm_helm_defines_t *defs, int line) { return helm_capture_chart(vals_src, tpl, defs, NULL, line); } /* A render doing far more work than any chart -- here a range over an int * of 20,000,000 -- stops at its step budget (helm_template.c): every capture * of the template is UNKNOWN, also the one made before the loop, never a * partial or wrong image; a range over a huge int ends the same way. */ TEST(helm_render_range_budget_ends_unknown) { static const char vals[] = "tag: \"1.0\"\n"; static const char ok[] = "image: \"ghcr.io/acme/app:{{ .Values.tag }}\"\n"; char *got = helm_capture(vals, ok, NULL, 1); ASSERT_STR_EQ(got, "ghcr.io/acme/app:1.0"); free(got); static const char rng[] = "image: \"ghcr.io/acme/app:{{ .Values.tag }}\"\n" "{{- range 20000000 }}{{ end }}\n"; got = helm_capture(vals, rng, NULL, 1); ASSERT_STR_EQ(got, ""); free(got); static const char huge[] = "image: \"ghcr.io/acme/app:{{ .Values.tag }}\"\n" "{{- range 2000000000 }}{{ range 2000000000 }}{{ end }}{{ end }}\n"; got = helm_capture(vals, huge, NULL, 1); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* An indent whose output passes the output budget stops the render: no * capture of it is known (an indent of 9e18 stops before any work). */ TEST(helm_render_indent_budget_ends_unknown) { static const char big[] = "image: \"ghcr.io/acme/app{{ indent 70000000 \"\" | trim }}:1\"\n"; char *got = helm_capture("{}\n", big, NULL, 1); ASSERT_STR_EQ(got, ""); free(got); static const char huge[] = "image: \"ghcr.io/acme/app{{ indent 9000000000000000000 \"\" | trim }}:1\"\n"; got = helm_capture("{}\n", huge, NULL, 1); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* Defines that each include the one below twice multiply the work by two a * level: past the step budget the render stops, its captures UNKNOWN. */ TEST(helm_render_define_fanout_ends_unknown) { enum { LEVELS = 21, FAN_CAP = 8192 }; char *fan = malloc(FAN_CAP); ASSERT_NOT_NULL(fan); int o = snprintf(fan, FAN_CAP, "{{- define \"d0\" -}}x{{- end -}}\n"); for (int k = 1; k < LEVELS; k++) { o += snprintf(fan + o, (size_t)(FAN_CAP - o), "{{- define \"d%d\" -}}{{ include \"d%d\" . }}{{ include \"d%d\" . }}" "{{- end -}}\n", k, k - 1, k - 1); } cbm_helm_defines_t *d = cbm_helm_defines_new(); ASSERT_NOT_NULL(d); cbm_helm_defines_add(d, "templates/_fan.tpl", fan, strlen(fan)); free(fan); static const char tpl[] = "image: \"ghcr.io/acme/app:{{ include \"d20\" . | trunc 1 }}\"\n"; char *got = helm_capture("{}\n", tpl, d, 1); cbm_helm_defines_free(d); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* go-yaml v2's aliasing limit (Helm reads values with it): a document whose * decodes come mostly from alias expansion -- a billion laughs -- is no * values; a few aliases still read. */ TEST(helm_values_alias_expansion_limit) { enum { LEVELS = 5, BUF = 4096 }; char laughs[BUF]; int o = snprintf(laughs, sizeof(laughs), "a0: &a0 [x, x, x, x, x, x, x, x, x, x]\n"); for (int k = 1; k < LEVELS; k++) { o += snprintf(laughs + o, sizeof(laughs) - (size_t)o, "a%d: &a%d [", k, k); for (int i = 0; i < 10; i++) { o += snprintf(laughs + o, sizeof(laughs) - (size_t)o, "%s*a%d", i ? ", " : "", k - 1); } o += snprintf(laughs + o, sizeof(laughs) - (size_t)o, "]\n"); } yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); ASSERT_NOT_NULL(doc); static const char few[] = "base: &b {tag: \"1\"}\none: *b\ntwo: *b\n"; yyjson_mut_val *ok = cbm_helm_values_parse(doc, few, strlen(few)); yyjson_mut_val *bomb = cbm_helm_values_parse(doc, laughs, strlen(laughs)); yyjson_mut_doc_free(doc); ASSERT_NOT_NULL(ok); ASSERT_NULL(bomb); PASS(); } /* --set as Helm's strvals reads it: a list index past MaxIndex (65536) is an * error, so the set is no value (a release with it is unknown) -- never a * list padded to it; a key longer than the parser holds is no value either, * never split into two keys. */ TEST(helm_values_set_index_cap_and_long_key) { yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); ASSERT_NOT_NULL(doc); yyjson_mut_val *root = yyjson_mut_obj(doc); ASSERT_TRUE(cbm_helm_values_set(doc, root, "list[3]", "x", false)); ASSERT_FALSE(cbm_helm_values_set(doc, root, "big[65537]", "x", false)); ASSERT_FALSE(cbm_helm_values_set(doc, root, "huge[9000000000000]", "x", false)); char key[700]; memset(key, 'k', sizeof(key) - 1); key[sizeof(key) - 1] = '\0'; ASSERT_FALSE(cbm_helm_values_set(doc, root, key, "x", false)); yyjson_mut_doc_free(doc); PASS(); } /* A values key longer than 1 KiB read by an image is in its default_of * mark whole -- a mark is never cut. */ TEST(helm_render_long_key_mark_whole) { enum { KEY = 1500 }; char *key = malloc(KEY + 1); ASSERT_NOT_NULL(key); memset(key, 'k', KEY); key[KEY] = '\0'; size_t vn = KEY + CBM_SZ_64; char *vals = malloc(vn); char *tpl = malloc(vn); ASSERT_NOT_NULL(vals); ASSERT_NOT_NULL(tpl); snprintf(vals, vn, "%s: \"1.0\"\n", key); snprintf(tpl, vn, "image: \"ghcr.io/acme/app:{{ index .Values \"%s\" }}\"\n", key); yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); yyjson_mut_val *v = cbm_helm_values_parse(doc, vals, strlen(vals)); CBMArena a; cbm_arena_init_lazy(&a, CBM_SZ_4K); cbm_helm_probe_t probe = {.line = 1}; cbm_helm_render(&a, "t.yaml", tpl, strlen(tpl), v, NULL, NULL, &probe, 1); char want[KEY + 8]; snprintf(want, sizeof(want), "[\"%s\"]", key); bool whole = probe.n == 1 && probe.keys[0] && strcmp(probe.keys[0], want) == 0; cbm_arena_destroy(&a); yyjson_mut_doc_free(doc); free(key); free(vals); free(tpl); ASSERT_TRUE(whole); PASS(); } /* A condition that is UNKNOWN runs no branch -- and what a branch would * assign is UNKNOWN after it: `$tag = .Values.image.tagNew` under a * .Capabilities test leaves no image naming the old tag. */ TEST(helm_unknown_branch_spoils_what_it_assigns) { static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\", tagNew: \"2.0\"}\n"; static const char tpl[] = "{{- $tag := .Values.image.tag -}}\n" "{{- if semverCompare \">=1.29-0\" .Capabilities.KubeVersion.Version }}" "{{- $tag = .Values.image.tagNew -}}{{- end }}\n" "image: \"{{ .Values.image.repository }}:{{ $tag }}\"\n"; char *got = helm_capture(vals, tpl, NULL, 3); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* set / unset / merge change a map in place -- not modelled: what it holds * after is UNKNOWN (an image read before the change keeps its value). */ TEST(helm_mutator_spoils_the_map_it_changes) { static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n"; static const char tpl[] = "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n" "{{- $_ := set .Values.image \"tag\" \"2.0\" }}\n" "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; char *before = helm_capture(vals, tpl, NULL, 1); char *after = helm_capture(vals, tpl, NULL, 3); ASSERT_STR_EQ(before, "ghcr.io/acme/app:1.0"); ASSERT_STR_EQ(after, ""); free(before); free(after); PASS(); } /* A break under an UNKNOWN condition in a range: whether the loop goes on * is unknown, so its later iterations are not rendered (Helm with a true * condition renders no image here). */ TEST(helm_unknown_break_leaves_the_loop) { static const char tpl[] = "{{- range list \"a\" \"b\" }}\n" "{{- if .Capabilities.APIVersions }}{{ break }}{{ end }}\n" "image: \"ghcr.io/acme/{{ . }}:1\"\n" "{{ end }}\n"; char *got = helm_capture("{}\n", tpl, NULL, 3); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* A change of maps the render does not run exactly leaves every image after * it UNKNOWN -- also one of a new value (fromYaml / deepCopy results, an empty * dict): no exemption (the coarse rule, decided in review round 3). Rounds 1-3 * kept this image (the opentelemetry-ebpf-instrumentation configData * pattern); it is part of the rule's measured cost. */ TEST(helm_change_of_a_new_value_spoils_after) { static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\ncfg: {a: 1}\n"; static const char tpl[] = "{{- $config := dict -}}\n" "{{- $user := deepCopy (.Values.cfg | default dict) -}}\n" "{{- if .Capabilities.APIVersions }}{{- $config = toYaml .Values.cfg | fromYaml -}}" "{{- $config = mergeOverwrite $config $user -}}{{- end }}\n" "{{- $config = mergeOverwrite $config $user -}}\n" "{{- $_ := unset (deepCopy .Values.cfg) \"a\" }}\n" "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; char *got = helm_capture(vals, tpl, NULL, 6); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* An image line inside a range: n captures of one probe cost memory linear * in n (the capture arrays grow by doubling), not a copy of every earlier * capture each time. */ TEST(helm_render_captures_grow_linearly) { enum { ITER = 5000 }; static const char tpl[] = "{{- range 5000 }}\n" "image: \"ghcr.io/acme/app:1\"\n" "{{ end }}\n"; CBMArena a; cbm_arena_init_lazy(&a, CBM_SZ_4K); cbm_helm_probe_t probe = {.line = 2}; size_t before = cbm_arena_total(&a); cbm_helm_render(&a, "t.yaml", tpl, strlen(tpl), NULL, NULL, NULL, &probe, 1); size_t used = cbm_arena_total(&a) - before; int n = probe.n; cbm_arena_destroy(&a); ASSERT_EQ(n, ITER); ASSERT_TRUE(used < (size_t)CBM_SZ_16 * CBM_SZ_1K * CBM_SZ_1K); /* far below 8*n^2 bytes */ PASS(); } /* Memory a render takes -- here a 20,000-item values list copied by each of * 200 `first` calls -- counts against its budget: past 256 MiB it stops and * the template's captures are UNKNOWN. */ TEST(helm_render_memory_budget_ends_unknown) { enum { ITEMS = 20000 }; size_t cap = (size_t)ITEMS * 4 + CBM_SZ_64; char *vals = malloc(cap); ASSERT_NOT_NULL(vals); size_t o = (size_t)snprintf(vals, cap, "tag: \"1.0\"\nbig: ["); for (int i = 0; i < ITEMS; i++) { o += (size_t)snprintf(vals + o, cap - o, "%s1", i ? "," : ""); } snprintf(vals + o, cap - o, "]\n"); static const char tpl[] = "image: \"ghcr.io/acme/app:{{ .Values.tag }}\"\n" "{{- range 200 }}{{ $x := first $.Values.big }}{{ end }}\n"; char *got = helm_capture(vals, tpl, NULL, 1); free(vals); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* A set skipped by an UNKNOWN condition whose target is a call's result -- * `index .Values "image"`, the idiom for a dashed key -- may change the map * it names: what the image reads of it is UNKNOWN. */ TEST(helm_skipped_set_of_a_call_result) { static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n"; static const char tpl[] = "{{- if semverCompare \">=1.29-0\" .Capabilities.KubeVersion.Version }}" "{{- $_ := set (index .Values \"image\") \"tag\" \"2.0\" }}{{- end }}\n" "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; char *got = helm_capture(vals, tpl, NULL, 2); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* A new map given a map another part holds is new no more: a change made * through it (`set $img ...` of `index $d "image"`) reaches .Values. */ TEST(helm_own_map_given_a_shared_one) { static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n"; static const char tpl[] = "{{- $d := dict -}}{{- $_ := set $d \"image\" .Values.image -}}\n" "{{- $img := index $d \"image\" -}}{{- $_ := set $img \"tag\" \"2.0\" -}}\n" "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; char *got = helm_capture(vals, tpl, NULL, 3); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* A set whose target is an UNKNOWN call result (index by .Release.Name) may * change any map of the values: every image after it is UNKNOWN. */ TEST(helm_set_of_an_unknown_call_result) { static const char vals[] = "apps: {web: {repository: ghcr.io/acme/web, tag: \"1.0\"}}\n"; static const char tpl[] = "{{- $_ := set (index .Values.apps .Release.Name) \"tag\" \"2.0\" }}\n" "image: \"{{ .Values.apps.web.repository }}:{{ .Values.apps.web.tag }}\"\n"; char *got = helm_capture(vals, tpl, NULL, 2); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* A break under an UNKNOWN condition: whether the rest of the loop ran is * unknown, so what it assigns ($tag = .) is UNKNOWN after the loop. */ TEST(helm_unknown_break_spoils_the_rest_of_the_loop) { static const char vals[] = "versions: [\"1.0\", \"2.0\"]\n"; static const char tpl[] = "{{- $tag := \"0.9\" }}{{- range .Values.versions }}\n" "{{- if semverCompare \">=1.29-0\" $.Capabilities.KubeVersion.Version }}{{ break }}" "{{ end }}\n" "{{- $tag = . }}{{- end }}\n" "image: \"ghcr.io/acme/app:{{ $tag }}\"\n"; char *got = helm_capture(vals, tpl, NULL, 4); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* A variable once made from `dict` and then (in a body an UNKNOWN condition * skips) redefined or given .Values.image is no new map: a set of it there * may change .Values. Without the redefinition the set is of the new map -- * still a change in a skipped body, which (the coarse rule) leaves every * image after it UNKNOWN; rounds 2-3 kept that one's image. */ TEST(helm_skipped_set_after_redefinition) { static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n"; static const char redefined[] = "{{- $cfg := dict }}{{- if .Capabilities.APIVersions }}" "{{- $cfg := .Values.image }}{{- $_ := set $cfg \"tag\" \"2.0\" }}{{- end }}\n" "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; static const char assigned[] = "{{- $cfg := dict }}{{- if .Capabilities.APIVersions }}" "{{- $cfg = .Values.image }}{{- $_ := set $cfg \"tag\" \"2.0\" }}{{- end }}\n" "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; static const char kept[] = "{{- $cfg := dict }}{{- if .Capabilities.APIVersions }}" "{{- $_ := set $cfg \"tag\" \"2.0\" }}{{- end }}\n" "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; char *got = helm_capture(vals, redefined, NULL, 2); ASSERT_STR_EQ(got, ""); free(got); got = helm_capture(vals, assigned, NULL, 2); ASSERT_STR_EQ(got, ""); free(got); got = helm_capture(vals, kept, NULL, 2); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* Many maps poisoned (a new dict changed each iteration) keep every later * read correct: a values read after them is known. */ TEST(helm_many_changed_maps_keep_reads_right) { static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n"; static const char tpl[] = "{{- range 20000 }}{{ $c := dict }}{{ $_ := set $c \"k\" \"v\" }}" "{{ end }}\n" "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; char *got = helm_capture(vals, tpl, NULL, 2); ASSERT_STR_EQ(got, "ghcr.io/acme/app:1.0"); free(got); PASS(); } /* Defines that include each other: A changes its argument and includes B, * B includes A. Whether B may change its argument is not settled by A's * scan still running -- an UNKNOWN include of B with .Values.image spoils * what reads it. */ TEST(helm_define_memo_under_mutual_recursion) { static const char defs_src[] = "{{- define \"A\" -}}{{- include \"B\" . -}}{{- $_ := set . \"tag\" \"2.0\" -}}{{- end -}}\n" "{{- define \"B\" -}}{{- if false }}{{ include \"A\" . }}{{ end -}}{{- end -}}\n"; cbm_helm_defines_t *d = cbm_helm_defines_new(); ASSERT_NOT_NULL(d); cbm_helm_defines_add(d, "templates/_ab.tpl", defs_src, strlen(defs_src)); static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\nother: {tag: \"x\"}\n"; static const char tpl[] = "{{- if .Capabilities.APIVersions }}{{ include \"A\" .Values.other }}{{ end }}\n" "{{- if .Capabilities.APIVersions }}{{ include \"B\" .Values.image }}{{ end }}\n" "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; char *got = helm_capture(vals, tpl, d, 3); cbm_helm_defines_free(d); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* An include not run because its argument is poisoned may still change maps * within it: a handle taken before ($sub) reads UNKNOWN after. */ TEST(helm_include_with_poisoned_argument) { static const char defs_src[] = "{{- define \"mut\" -}}{{- $_ := set .sub \"tag\" \"2.0\" -}}{{- end -}}\n"; cbm_helm_defines_t *d = cbm_helm_defines_new(); ASSERT_NOT_NULL(d); cbm_helm_defines_add(d, "templates/_mut.tpl", defs_src, strlen(defs_src)); static const char vals[] = "image: {sub: {repository: ghcr.io/acme/app, tag: \"1.0\"}}\n"; static const char tpl[] = "{{- $sub := .Values.image.sub -}}\n" "{{- $_ := set .Values.image \"x\" \"y\" -}}\n" "{{- include \"mut\" .Values.image -}}\n" "image: \"{{ $sub.repository }}:{{ $sub.tag }}\"\n"; char *got = helm_capture(vals, tpl, d, 4); cbm_helm_defines_free(d); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* The keys a capture read -- 5,000 values paths -- become its mark in one * pass: memory linear in them, never a copy of the mark so far per key. */ TEST(helm_capture_keys_cost_linear_memory) { enum { ITEMS = 5000 }; size_t cap = (size_t)ITEMS * 8 + CBM_SZ_64; char *vals = malloc(cap); ASSERT_NOT_NULL(vals); size_t o = (size_t)snprintf(vals, cap, "big: ["); for (int i = 0; i < ITEMS; i++) { o += (size_t)snprintf(vals + o, cap - o, "%s{x: a}", i ? "," : ""); } snprintf(vals + o, cap - o, "]\n"); static const char tpl[] = "image: \"r/{{ range .Values.big }}{{ .x }}{{ end }}\"\n"; cbm_mem_class_flush_thread(); size_t base = cbm_mem_class_live_bytes(CBM_MEM_CLASS_ARENA); cbm_mem_class_reset_peaks(); char *got = helm_capture(vals, tpl, NULL, 1); cbm_mem_class_flush_thread(); size_t peak = cbm_mem_class_peak_bytes(CBM_MEM_CLASS_ARENA); free(vals); ASSERT_NOT_NULL(got); bool image = strncmp(got, "r/aaaa", strlen("r/aaaa")) == 0; free(got); ASSERT_TRUE(image); /* about 14 bytes a key: ~70 KB of keys; a copy per key is ~175 MB */ ASSERT_TRUE(peak - base < (size_t)CBM_SZ_32 * CBM_SZ_1K * CBM_SZ_1K); PASS(); } /* A render's work -- here 20 copies of a 20,000-item list -- stays in an * arena of its own: the caller's arena grows by the render's results only, * so renders one after another never keep each other's work. */ TEST(helm_render_keeps_only_its_results) { enum { ITEMS = 20000 }; size_t cap = (size_t)ITEMS * 2 + CBM_SZ_64; char *vals = malloc(cap); ASSERT_NOT_NULL(vals); size_t o = (size_t)snprintf(vals, cap, "big: ["); for (int i = 0; i < ITEMS; i++) { o += (size_t)snprintf(vals + o, cap - o, "%s1", i ? "," : ""); } snprintf(vals + o, cap - o, "]\n"); yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); yyjson_mut_val *v = doc ? cbm_helm_values_parse(doc, vals, strlen(vals)) : NULL; free(vals); static const char tpl[] = "image: \"ghcr.io/acme/app:1\"\n" "{{- range 20 }}{{ $x := first $.Values.big }}{{ end }}\n"; CBMArena a; cbm_arena_init_lazy(&a, CBM_SZ_4K); cbm_helm_probe_t probe = {.line = 1}; size_t before = cbm_arena_total(&a); bool ok = cbm_helm_render(&a, "t.yaml", tpl, strlen(tpl), v, NULL, NULL, &probe, 1); size_t kept = cbm_arena_total(&a) - before; bool image = ok && probe.n == 1 && probe.image[0] && strcmp(probe.image[0], "ghcr.io/acme/app:1") == 0; cbm_arena_destroy(&a); if (doc) { yyjson_mut_doc_free(doc); } ASSERT_NOT_NULL(v); ASSERT_TRUE(image); ASSERT_TRUE(kept < (size_t)CBM_SZ_1K * CBM_SZ_1K); /* the work: ~30 MB */ PASS(); } static char g_helm_budget_log[CBM_SZ_4K]; static void helm_budget_log_sink(const char *line) { if (!line || !strstr(line, "helm.render.budget")) { return; } size_t used = strlen(g_helm_budget_log); snprintf(g_helm_budget_log + used, sizeof(g_helm_budget_log) - used, "%s\n", line); } /* A render its output budget stopped -- an indent that writes past 64 MiB, * or one too wide to try -- says so: reason=output. */ TEST(helm_render_budget_logs_its_reason) { g_helm_budget_log[0] = '\0'; CBMLogLevel prev = cbm_log_get_level(); cbm_log_set_level(CBM_LOG_DEBUG); cbm_log_set_sink(helm_budget_log_sink); static const char big[] = "image: \"ghcr.io/acme/app{{ indent 70000000 \"\" | trim }}:1\"\n"; char *got = helm_capture("{}\n", big, NULL, 1); free(got); static const char huge[] = "image: \"ghcr.io/acme/app{{ indent 9000000000000000000 \"\" | trim }}:1\"\n"; got = helm_capture("{}\n", huge, NULL, 1); free(got); cbm_log_set_sink(NULL); cbm_log_set_level(prev); const char *first = strstr(g_helm_budget_log, "reason=output"); ASSERT_NOT_NULL(first); ASSERT_NOT_NULL(strstr(first + SKIP_ONE, "reason=output")); ASSERT_NULL(strstr(g_helm_budget_log, "reason=memory")); PASS(); } /* A merge into .Values may change any map of the values, also one a handle * ($img) was taken of before: every image after it is UNKNOWN. */ TEST(helm_merge_into_values_root) { static const char vals[] = "image: {repository: ghcr.io/acme/app}\n"; static const char tpl[] = "{{- $img := .Values.image }}\n" "{{- $_ := merge .Values (dict \"image\" (dict \"tag\" \"2.0\")) }}\n" "image: \"{{ $img.repository }}:{{ $img.tag | default \"1.0\" }}\"\n"; char *got = helm_capture(vals, tpl, NULL, 3); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* A set in a body an UNKNOWN condition skips, whatever its target -- here * the outer $vals (.Values.image), a nested range's own $vals aside -- * leaves every image after it UNKNOWN. */ TEST(helm_skipped_set_beside_a_nested_scope) { static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\njobs: [a]\n"; static const char tpl[] = "{{- $vals := .Values.image }}\n" "{{- if .Capabilities.APIVersions.Has \"batch/v1\" }}\n" "{{- range .Values.jobs }}{{- $vals := dict }}{{- end }}\n" "{{- $_ := set $vals \"tag\" \"2.0\" }}\n" "{{- end }}\n" "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; char *got = helm_capture(vals, tpl, NULL, 6); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* An UNKNOWN break in a range over maps: whether the later elements ($c of * web) were changed is not known -- not only the current one's. */ TEST(helm_unknown_break_spoils_later_elements) { static const char vals[] = "components:\n" " api: {repository: ghcr.io/acme/api, tag: \"1.0\"}\n" " web: {repository: ghcr.io/acme/web, tag: \"1.0\"}\n"; static const char tpl[] = "{{- range $n, $c := .Values.components }}\n" "{{- if semverCompare \">=1.29-0\" $.Capabilities.KubeVersion.Version }}{{ break }}" "{{ end }}\n" "{{- $_ := set $c \"tag\" \"2.0\" }}\n" "{{- end }}\n" "image: \"{{ .Values.components.web.repository }}:{{ .Values.components.web.tag }}\"\n"; char *got = helm_capture(vals, tpl, NULL, 5); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* A skipped range whose element variable ($c, each component) is set is no * change of an outer $c (a new dict): every image after it is UNKNOWN. */ TEST(helm_skipped_range_sets_its_element) { static const char vals[] = "components:\n" " web: {repository: ghcr.io/acme/web, tag: \"1.0\"}\n"; static const char tpl[] = "{{- $c := dict }}\n" "{{- if .Capabilities.APIVersions.Has \"x/v1\" }}{{- range $_, $c := .Values.components }}" "{{- $_ := set $c \"tag\" \"2.0\" }}{{- end }}{{- end }}\n" "image: \"{{ .Values.components.web.repository }}:{{ .Values.components.web.tag }}\"\n"; char *got = helm_capture(vals, tpl, NULL, 3); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* A deep merge into a map that holds a values map by reference ($d.img is * .Values.image, put by a set) changes that values map: the image after it * is UNKNOWN. */ TEST(helm_deep_merge_reaches_a_held_values_map) { static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n"; static const char tpl[] = "{{- $d := dict }}{{- $_ := set $d \"img\" .Values.image }}\n" "{{- $_ := mergeOverwrite $d (dict \"img\" (dict \"tag\" \"2.0\")) }}\n" "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n"; char *got = helm_capture(vals, tpl, NULL, 3); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* Template files of chart app (charts/app) and of its subchart redis, which * app's dependencies alias `cache` (charts/app/charts/redis): a Secret and a * ConfigMap with no change of maps, an app ConfigMap that sets a value, and * a file that includes itself. */ static cbm_helm_defines_t *helm_chart_files(void) { static const char secret[] = "apiVersion: v1\nkind: Secret\nmetadata:\n" " name: {{ include \"app.name\" . }}\n"; static const char helpers[] = "{{- define \"app.name\" -}}{{ .Chart.Name }}{{- end -}}\n"; static const char app_cm[] = "{{- $_ := set .Values.image \"tag\" \"2.0\" }}\n" "apiVersion: v1\nkind: ConfigMap\n"; static const char sub_cm[] = "apiVersion: v1\nkind: ConfigMap\nmetadata:\n" " name: {{ .Chart.Name }}\n"; static const char loop[] = "{{ include \"app/templates/loop.yaml\" . }}\n"; cbm_helm_defines_t *d = cbm_helm_defines_new(); if (d) { cbm_helm_defines_add(d, "charts/app/templates/_helpers.tpl", helpers, strlen(helpers)); cbm_helm_defines_add(d, "charts/app/templates/secret.yaml", secret, strlen(secret)); cbm_helm_defines_add(d, "charts/app/templates/cm.yaml", app_cm, strlen(app_cm)); cbm_helm_defines_add(d, "charts/app/templates/loop.yaml", loop, strlen(loop)); cbm_helm_defines_add(d, "charts/app/charts/redis/templates/cm.yaml", sub_cm, strlen(sub_cm)); } return d; } /* A template of chart `meta` checksumming its file `file`, then an image. */ static char *helm_capture_checksum(const cbm_helm_defines_t *d, const cbm_helm_chart_meta_t *meta, const char *file) { static const char vals[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n"; char tpl[CBM_SZ_512]; snprintf(tpl, sizeof(tpl), "metadata:\n annotations:\n" " checksum: {{ include (print $.Template.BasePath \"/%s\") . | sha256sum }}\n" "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n", file); return helm_capture_chart(vals, tpl, d, meta, 4); } static const cbm_helm_chart_meta_t HELM_APP = { .name = "app", .base_path = "app/templates", .dir = "charts/app"}; /* The checksum idiom names one of the chart's own template files by its * .Template.BasePath: one with no change of maps in it (a Secret) spoils * nothing -- its output is not rendered, the image after it is known. */ TEST(helm_checksum_include_keeps_the_image) { cbm_helm_defines_t *d = helm_chart_files(); ASSERT_NOT_NULL(d); char *got = helm_capture_checksum(d, &HELM_APP, "secret.yaml"); cbm_helm_defines_free(d); ASSERT_STR_EQ(got, "ghcr.io/acme/app:1.0"); free(got); PASS(); } /* An included template file that sets a value (cm.yaml) may change any map: * every image after it is UNKNOWN -- beside one that does not (secret.yaml). */ TEST(helm_include_of_a_changing_template_file) { cbm_helm_defines_t *d = helm_chart_files(); ASSERT_NOT_NULL(d); char *clean = helm_capture_checksum(d, &HELM_APP, "secret.yaml"); char *changing = helm_capture_checksum(d, &HELM_APP, "cm.yaml"); cbm_helm_defines_free(d); ASSERT_STR_EQ(clean, "ghcr.io/acme/app:1.0"); ASSERT_STR_EQ(changing, ""); free(clean); free(changing); PASS(); } /* A subchart's .Template.BasePath -- app/charts/cache/templates, by the alias * it is included under -- names its own cm.yaml (no change), not app's (a * set): its image is known; app's own checksum of cm.yaml is not. */ TEST(helm_subchart_base_path_names_its_own_file) { cbm_helm_defines_t *d = helm_chart_files(); ASSERT_NOT_NULL(d); const cbm_helm_chart_meta_t sub = {.name = "cache", .base_path = "app/charts/cache/templates", .dir = "charts/app/charts/redis"}; char *own = helm_capture_checksum(d, &sub, "cm.yaml"); char *parent = helm_capture_checksum(d, &HELM_APP, "cm.yaml"); cbm_helm_defines_free(d); ASSERT_STR_EQ(own, "ghcr.io/acme/app:1.0"); ASSERT_STR_EQ(parent, ""); free(own); free(parent); PASS(); } /* An include naming no template file of the chart is not looked into: every * image after it is UNKNOWN -- beside one naming a file that is there. */ TEST(helm_include_of_a_missing_template_file) { cbm_helm_defines_t *d = helm_chart_files(); ASSERT_NOT_NULL(d); char *there = helm_capture_checksum(d, &HELM_APP, "secret.yaml"); char *missing = helm_capture_checksum(d, &HELM_APP, "missing.yaml"); cbm_helm_defines_free(d); ASSERT_STR_EQ(there, "ghcr.io/acme/app:1.0"); ASSERT_STR_EQ(missing, ""); free(there); free(missing); PASS(); } /* A template file that includes itself is never "no change": every image * after an include of it is UNKNOWN. */ TEST(helm_template_file_including_itself) { cbm_helm_defines_t *d = helm_chart_files(); ASSERT_NOT_NULL(d); char *got = helm_capture_checksum(d, &HELM_APP, "loop.yaml"); cbm_helm_defines_free(d); ASSERT_STR_EQ(got, ""); free(got); PASS(); } static const char HELM_IMAGE_VALUES[] = "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\ncfg: {a: {}}\nempty: {}\n"; #define HELM_IMAGE_LINE "image: \"{{ .Values.image.repository }}:{{ .Values.image.tag }}\"\n" /* A set below a values map puts .Values.image at cfg.a.d; a deep merge into * the map above it (.Values.cfg) reaches .Values.image there: the image after * it is UNKNOWN. */ TEST(helm_deep_merge_over_a_change_below) { static const char tpl[] = "{{- $_ := set .Values.cfg.a \"d\" .Values.image }}\n" "{{- $_ := mergeOverwrite .Values.cfg (dict \"a\" (dict \"d\" (dict \"tag\" \"2.0\"))) }}\n" HELM_IMAGE_LINE; char *got = helm_capture(HELM_IMAGE_VALUES, tpl, NULL, 3); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* and / or stop at an operand whose truth is not known (.Release.Namespace; * a map a set changed -- .Values.empty is empty no more): whether the * operands after it run is not known, nor a change they make -- the image * after it is UNKNOWN. A known false one runs none: the image stays known. */ TEST(helm_and_or_with_an_unknown_operand) { static const char *const tpls[] = { "{{- $_ := and .Release.Namespace (set .Values.image \"tag\" \"2.0\") }}\n" HELM_IMAGE_LINE, "{{- $_ := or .Release.Namespace (set .Values.image \"tag\" \"2.0\") }}\n" HELM_IMAGE_LINE, "{{- $_ := set .Values.empty \"x\" \"y\" }}{{- $_ := and .Values.empty " "(set .Values.image \"tag\" \"2.0\") }}\n" HELM_IMAGE_LINE}; for (size_t i = 0; i < sizeof(tpls) / sizeof(tpls[0]); i++) { char *got = helm_capture(HELM_IMAGE_VALUES, tpls[i], NULL, 2); bool unknown = strcmp(got, "") == 0; free(got); ASSERT_TRUE(unknown); } static const char known[] = "{{- $_ := and false (set .Values.image \"tag\" \"2.0\") }}\n" HELM_IMAGE_LINE; char *got = helm_capture(HELM_IMAGE_VALUES, known, NULL, 2); ASSERT_STR_EQ(got, "ghcr.io/acme/app:1.0"); free(got); PASS(); } /* An if whose first condition is UNKNOWN: Helm evaluates the else-if * conditions after it -- a set there, or an assignment ($tag = ...), may run: * the image after it is UNKNOWN. */ TEST(helm_later_condition_of_a_skipped_if) { static const char set[] = "{{- if regexMatch \"^v\" .Values.image.tag }}{{- else if set .Values.image \"tag\" \"2.0\" }}" "{{- end }}\n" HELM_IMAGE_LINE; char *got = helm_capture(HELM_IMAGE_VALUES, set, NULL, 2); ASSERT_STR_EQ(got, ""); free(got); static const char assign[] = "{{- $tag := \"1.0\" }}{{- if regexMatch \"^v\" .Values.image.tag }}" "{{- else if $tag = \"2.0\" }}{{- end }}\n" "image: \"ghcr.io/acme/app:{{ $tag }}\"\n"; got = helm_capture(HELM_IMAGE_VALUES, assign, NULL, 2); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* sortAlpha sorts a []string in place (splitList gives one): the list $tags * holds changes -- not run here, so the image after it is UNKNOWN. */ TEST(helm_sort_alpha_changes_its_list) { static const char tpl[] = "{{- $tags := splitList \",\" .Values.tags }}" "{{- $_ := sortAlpha $tags }}\n" "image: \"ghcr.io/acme/app:{{ first $tags }}\"\n"; char *got = helm_capture("tags: \"2.0,1.0\"\n", tpl, NULL, 2); ASSERT_STR_EQ(got, ""); free(got); PASS(); } /* A form the grammar rejects may hide a change (Go may take it): a template, * a define it runs, or a template file it includes that does not parse * leaves its images UNKNOWN. */ TEST(helm_parse_error_spoils) { static const char bad[] = "{{- $x := }}\n" HELM_IMAGE_LINE; char *got = helm_capture(HELM_IMAGE_VALUES, bad, NULL, 2); ASSERT_STR_EQ(got, ""); free(got); static const char defs_src[] = "{{- define \"bad\" -}}{{- $x := }}{{- end -}}\n" "{{- define \"good\" -}}ok{{- end -}}\n"; cbm_helm_defines_t *d = cbm_helm_defines_new(); ASSERT_NOT_NULL(d); cbm_helm_defines_add(d, "charts/app/templates/_bad.tpl", defs_src, strlen(defs_src)); static const char bad_file[] = "{{- $x := }}\nkind: ConfigMap\n"; cbm_helm_defines_add(d, "charts/app/templates/bad.yaml", bad_file, strlen(bad_file)); static const char good[] = "x: {{ include \"good\" . }}\n" HELM_IMAGE_LINE; static const char via_define[] = "x: {{ include \"bad\" . }}\n" HELM_IMAGE_LINE; static const char via_file[] = "x: {{ include (print $.Template.BasePath \"/bad.yaml\") . | sha256sum }}\n" HELM_IMAGE_LINE; char *ok = helm_capture_chart(HELM_IMAGE_VALUES, good, d, &HELM_APP, 2); char *by_define = helm_capture_chart(HELM_IMAGE_VALUES, via_define, d, &HELM_APP, 2); char *by_file = helm_capture_chart(HELM_IMAGE_VALUES, via_file, d, &HELM_APP, 2); cbm_helm_defines_free(d); ASSERT_STR_EQ(ok, "ghcr.io/acme/app:1.0"); ASSERT_STR_EQ(by_define, ""); ASSERT_STR_EQ(by_file, ""); free(ok); free(by_define); free(by_file); PASS(); } /* One capture reading 20,000 keys, then 2,000 captures of two keys each: * every image and mark after the big one stays right (its keys leave the * set one by one; the set is never cleared by walking its capacity). */ TEST(helm_captures_after_a_huge_capture) { enum { ITEMS = 20000, LATER = 2000 }; size_t cap = (size_t)ITEMS * 8 + CBM_SZ_256; char *vals = malloc(cap); ASSERT_NOT_NULL(vals); size_t o = (size_t)snprintf(vals, cap, "image: {repository: ghcr.io/acme/app, tag: \"1.0\"}\n" "big: ["); for (int i = 0; i < ITEMS; i++) { o += (size_t)snprintf(vals + o, cap - o, "%s{x: a}", i ? "," : ""); } snprintf(vals + o, cap - o, "]\n"); yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); yyjson_mut_val *v = doc ? cbm_helm_values_parse(doc, vals, strlen(vals)) : NULL; free(vals); static const char tpl[] = "image: \"r/{{ range .Values.big }}{{ .x }}{{ end }}\"\n" "{{- range 2000 }}\n" "image: \"{{ $.Values.image.repository }}:{{ $.Values.image.tag }}\"\n" "{{- end }}\n"; CBMArena a; cbm_arena_init_lazy(&a, CBM_SZ_4K); cbm_helm_probe_t probes[2] = {{.line = 1}, {.line = 3}}; bool ok = cbm_helm_render(&a, "t.yaml", tpl, strlen(tpl), v, NULL, NULL, probes, 2); int n = probes[1].n; bool last = n == LATER && probes[1].image[n - 1] && strcmp(probes[1].image[n - 1], "ghcr.io/acme/app:1.0") == 0 && probes[1].keys[n - 1] && strcmp(probes[1].keys[n - 1], "[\"image.repository\",\"image.tag\"]") == 0; cbm_arena_destroy(&a); if (doc) { yyjson_mut_doc_free(doc); } ASSERT_TRUE(ok); ASSERT_EQ(n, LATER); ASSERT_TRUE(last); PASS(); } /* 5,000 includes of one name in a loop -- a template file of the chart with * no change, or a name that is none -- get one answer each time (asked * once): the first spoils nothing, the second every image after it. */ TEST(helm_includes_of_one_name_in_a_loop) { cbm_helm_defines_t *d = helm_chart_files(); ASSERT_NOT_NULL(d); static const char clean[] = "{{- range 5000 }}{{ $_ := include (print $.Template.BasePath \"/secret.yaml\") $ }}" "{{ end }}\n" HELM_IMAGE_LINE; static const char missing[] = "{{- range 5000 }}{{ $_ := include (print $.Template.BasePath \"/missing.yaml\") $ }}" "{{ end }}\n" HELM_IMAGE_LINE; char *kept = helm_capture_chart(HELM_IMAGE_VALUES, clean, d, &HELM_APP, 2); char *spoiled = helm_capture_chart(HELM_IMAGE_VALUES, missing, d, &HELM_APP, 2); cbm_helm_defines_free(d); ASSERT_STR_EQ(kept, "ghcr.io/acme/app:1.0"); ASSERT_STR_EQ(spoiled, ""); free(kept); free(spoiled); PASS(); } /* A compose project's .env feeds its compose file (variables in image, * build): changing it is no closure repair -- the unchanged compose file's * services would keep the old image. It rebuilds; no stale image remains. */ TEST(incremental_compose_dotenv_change_rebuilds) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_composeenv_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "src/web/Dockerfile", "FROM nginx:1.27\n"); write_temp_file(tmp, ".env", "REG=ghcr.io/old\n"); write_temp_file(tmp, "compose.yaml", "services:\n web:\n image: ${REG}/web\n build: ./src/web\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/composeenv.db", tmp); static const char old_img[] = "SELECT COUNT(*) FROM nodes WHERE label='Image' AND name='ghcr.io/old/web'"; static const char new_use[] = "SELECT COUNT(*) FROM edges e JOIN nodes t ON t.id=e.target_id WHERE e.type='USAGE' AND " "t.name='ghcr.io/new/web'"; int full_old = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, old_img) : -1; write_temp_file(tmp, ".env", "REG=ghcr.io/new\n"); int rc = k8s_inc_index(tmp, db_path); cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); int after_old = k8s_inc_count(db_path, old_img); int after_new = k8s_inc_count(db_path, new_use); th_rmtree(tmp); ASSERT_EQ(full_old, 1); ASSERT_EQ(rc, 0); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(after_old, 0); ASSERT_EQ(after_new, 1); PASS(); } /* Editing a Dockerfile keeps the build edge an unchanged script declares * for it: image build edges are recomputed whole, so the files that declared * builds before are read again. `legacy`: through the full-load route, else * the closure-repair route. Returns "full=N inc=M", or NULL. */ static char *incremental_docker_build_keep(bool legacy, cbm_incremental_route_t *route) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_dockerbuild_XXXXXX"); if (!cbm_mkdtemp(tmp)) { return NULL; } write_temp_file(tmp, "svc/Dockerfile", "FROM golang:1.22\n"); write_temp_file(tmp, "build.sh", "#!/bin/sh\ncd \"$(dirname \"$0\")\"\ndocker build -t ghcr.io/acme/svc:1 svc\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/dockerbuild.db", tmp); static const char builds[] = "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id WHERE e.type='DEPENDS_ON' " "AND s.label='Image' AND json_extract(e.properties,'$.tool')='docker-build'"; int full = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, builds) : -1; write_temp_file(tmp, "svc/Dockerfile", "FROM golang:1.22\n# edited\n"); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API if (legacy) { cbm_pipeline_incremental_test_force_legacy_partial_once(); } #else (void)legacy; #endif char *out = NULL; if (k8s_inc_index(tmp, db_path) == 0) { char buf[64]; snprintf(buf, sizeof(buf), "full=%d inc=%d", full, k8s_inc_count(db_path, builds)); out = strdup(buf); } *route = cbm_pipeline_incremental_test_last_route(); th_rmtree(tmp); return out; } TEST(incremental_dockerfile_edit_keeps_docker_build_edges) { cbm_incremental_route_t route = CBM_INCREMENTAL_ROUTE_NONE; char *got = incremental_docker_build_keep(false, &route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, "full=1 inc=1"); free(got); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API got = incremental_docker_build_keep(true, &route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, "full=1 inc=1"); free(got); #endif PASS(); } /* Two files build the same image from one Dockerfile: deleting the one * the edge happened to record keeps the edge -- the other file still * declares it, and it is read again (its File node carries the mark). */ TEST(incremental_second_build_declarer_keeps_the_edge) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_twodecl_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "svc/Dockerfile", "FROM golang:1.22\n"); write_temp_file(tmp, "a.sh", "#!/bin/sh\ncd \"$(dirname \"$0\")\"\n" "docker build -t ghcr.io/acme/svc:1 svc\n"); write_temp_file(tmp, "b.sh", "#!/bin/sh\ncd \"$(dirname \"$0\")\"\n" "docker build -t ghcr.io/acme/svc:2 svc\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/twodecl.db", tmp); static const char builds[] = "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id WHERE e.type='DEPENDS_ON' " "AND s.label='Image' AND json_extract(e.properties,'$.tool')='docker-build'"; int full = k8s_inc_index(tmp, db_path) == 0 ? k8s_inc_count(db_path, builds) : -1; char path[512]; snprintf(path, sizeof(path), "%s/a.sh", tmp); (void)remove(path); int rc_a = k8s_inc_index(tmp, db_path); int after_a = k8s_inc_count(db_path, builds); snprintf(path, sizeof(path), "%s/b.sh", tmp); (void)remove(path); int rc_b = k8s_inc_index(tmp, db_path); int after_b = k8s_inc_count(db_path, builds); th_rmtree(tmp); ASSERT_EQ(full, 1); ASSERT_EQ(rc_a, 0); ASSERT_EQ(after_a, 1); ASSERT_EQ(rc_b, 0); ASSERT_EQ(after_b, 0); PASS(); } TEST(incremental_deleted_dockerfile_drops_its_image) { cbm_incremental_route_t route = CBM_INCREMENTAL_ROUTE_NONE; int left = incremental_dockerfile_delete(false, &route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); ASSERT_EQ(left, 1); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API left = incremental_dockerfile_delete(true, &route); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); ASSERT_EQ(left, 1); #endif PASS(); } /* SQL DDL becomes first-class Table/View nodes wired into FROM/JOIN lineage, * while the shared name registry must NOT leak those relations into other * languages' textual resolution: a Python call or identifier sharing the * table's name (`users`) would otherwise unique-name-bind a false CALLS/USAGE * edge into the lineage layer. Pins the resolve-time relation veto * (cbm_registry_resolve) together with the lineage opt-in * (cbm_registry_resolve_lineage). */ TEST(pipeline_sql_lineage_and_relation_isolation) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_sql_lineage_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "schema.sql", "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);\n" "CREATE VIEW active_users AS SELECT * FROM users;\n"); /* `users` exists project-wide ONLY as the SQL table, so without the * relation veto the cross-file unique-name fallback would bind both the * call and the bare reference below straight to the Table node. */ write_temp_file(tmp, "app.py", "def load_users():\n" " return users()\n" "\n" "def show_users():\n" " return users\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/sql_lineage.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* Positive control: the view's FROM emits real lineage. */ ASSERT_EQ(named_edge_count(s, project, "USAGE", "active_users", "users"), 1); /* Isolation: no Python edge of any kind reaches the Table. */ ASSERT_EQ(named_edge_count(s, project, "CALLS", "load_users", "users"), 0); ASSERT_EQ(named_edge_count(s, project, "USAGE", "load_users", "users"), 0); ASSERT_EQ(named_edge_count(s, project, "USAGE", "show_users", "users"), 0); ASSERT_EQ(named_edge_count(s, project, "READS", "show_users", "users"), 0); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* dbt lineage end-to-end. A dbt project's dependency structure lives entirely * in Jinja ({{ ref('x') }}), which the SQL grammar cannot read, so this is the * whole value: model -> model edges across files, plus the join onto a Table * declared in ordinary DDL — Model and Table are both relation labels, so one * lineage layer spans both. The Python file is the isolation control: `stg_orders` * exists project-wide only as a dbt model, and the registry's relation veto must * keep a same-named call out of the lineage layer. */ TEST(pipeline_dbt_jinja_lineage) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_dbt_lineage_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "raw_schema.sql", "CREATE TABLE customers (id INTEGER, name TEXT);\n"); write_temp_file(tmp, "stg_orders.sql", "SELECT id, customer_id FROM {{ source('raw', 'customers') }}\n"); write_temp_file(tmp, "orders_enriched.sql", "SELECT o.id, c.name\n" "FROM {{ ref('stg_orders') }} o\n" "JOIN {{ ref('stg_orders') }} c ON c.id = o.customer_id\n"); write_temp_file(tmp, "app.py", "def load():\n return stg_orders()\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/dbt.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* model -> model: the ref() lineage the SQL grammar cannot see */ ASSERT_TRUE(named_edge_count(s, project, "USAGE", "orders_enriched", "stg_orders") >= 1); /* model -> table: source() joining dbt onto plain DDL in the same repo */ ASSERT_EQ(named_edge_count(s, project, "USAGE", "stg_orders", "customers"), 1); /* isolation: the Python call must not reach the model */ ASSERT_EQ(named_edge_count(s, project, "CALLS", "load", "stg_orders"), 0); ASSERT_EQ(named_edge_count(s, project, "USAGE", "load", "stg_orders"), 0); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* Renaming a table must drop lineage from DEPENDENT (unchanged) SQL files on * the incremental path. Table/View participate in the per-file LSP surface * hash as registry-only labels (lsp_surface.c), so tables.sql's def change * invalidates views.sql's resolution instead of slipping the early cutoff and * leaving a stale view -> old-table USAGE edge. */ TEST(pipeline_incremental_sql_table_rename_drops_stale_lineage) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_sql_rename_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "tables.sql", "CREATE TABLE users (id INTEGER PRIMARY KEY);\n"); write_temp_file(tmp, "views.sql", "CREATE VIEW active AS SELECT * FROM users;\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/sql_rename.db", tmp); cbm_pipeline_t *first = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(first); ASSERT_EQ(cbm_pipeline_run(first), 0); const char *first_project = cbm_pipeline_project_name(first); cbm_store_t *first_store = cbm_store_open_path(db_path); ASSERT_NOT_NULL(first_store); ASSERT_EQ(named_edge_count(first_store, first_project, "USAGE", "active", "users"), 1); cbm_store_close(first_store); cbm_pipeline_free(first); write_temp_file(tmp, "tables.sql", "CREATE TABLE people (id INTEGER PRIMARY KEY);\n"); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *second = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(second); ASSERT_EQ(cbm_pipeline_run(second), 0); const char *second_project = cbm_pipeline_project_name(second); cbm_store_t *second_store = cbm_store_open_path(db_path); ASSERT_NOT_NULL(second_store); /* The view's FROM still says `users`, which no longer exists: the old * edge must be gone (no stale lineage), and the unchanged dependent must * not have been rebound to the renamed table either. */ ASSERT_EQ(named_edge_count(second_store, second_project, "USAGE", "active", "users"), 0); ASSERT_EQ(named_edge_count(second_store, second_project, "USAGE", "active", "people"), 0); cbm_store_close(second_store); cbm_pipeline_free(second); th_rmtree(tmp); PASS(); } /* Re-indexing only the caller must retain cross-file semantic proof for a * callable value whose definition lives in an unchanged file. A fresh full * index of the edited sources is the convergence oracle: incremental output * must not downgrade the exact CALL_REFERENCE to a generic USAGE. */ TEST(pipeline_incremental_cross_file_call_reference_matches_fresh_full) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_ref_cross_incr_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } static const char target_source[] = "export function importedIncrementalHandler(): void {}\n"; static const char initial_caller_source[] = "import { importedIncrementalHandler } from './plugin';\n" "function acceptIncrementalReference(callback: () => void): void {}\n" "export function incrementalImportedArgument(): void {\n" " acceptIncrementalReference(importedIncrementalHandler);\n" "}\n"; static const char edited_caller_source[] = "import { importedIncrementalHandler } from './plugin';\n" "function acceptIncrementalReference(callback: () => void): void {}\n" "export function incrementalImportedArgument(): void {\n" " acceptIncrementalReference(importedIncrementalHandler);\n" "}\n" "// caller-only size-changing edit\n"; write_temp_file(tmp, "plugin.ts", target_source); write_temp_file(tmp, "main.ts", initial_caller_source); char incremental_db[512]; snprintf(incremental_db, sizeof(incremental_db), "%s/cross-reference-incremental.db", tmp); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, incremental_db, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); const char *baseline_project = cbm_pipeline_project_name(baseline); cbm_store_t *baseline_store = cbm_store_open_path(incremental_db); ASSERT_NOT_NULL(baseline_store); ASSERT_EQ(named_edge_count(baseline_store, baseline_project, "CALL_REFERENCE", "incrementalImportedArgument", "importedIncrementalHandler"), 1); ASSERT_EQ(named_edge_count(baseline_store, baseline_project, "USAGE", "incrementalImportedArgument", "importedIncrementalHandler"), 0); ASSERT_EQ(named_edge_count(baseline_store, baseline_project, "CALLS", "incrementalImportedArgument", "importedIncrementalHandler"), 0); cbm_store_close(baseline_store); cbm_pipeline_free(baseline); write_temp_file(tmp, "main.ts", edited_caller_source); cbm_pipeline_t *incremental = cbm_pipeline_new(tmp, incremental_db, CBM_MODE_FULL); ASSERT_NOT_NULL(incremental); ASSERT_EQ(cbm_pipeline_run(incremental), 0); const char *incremental_project = cbm_pipeline_project_name(incremental); cbm_store_t *incremental_store = cbm_store_open_path(incremental_db); ASSERT_NOT_NULL(incremental_store); int incremental_reference = named_edge_count(incremental_store, incremental_project, "CALL_REFERENCE", "incrementalImportedArgument", "importedIncrementalHandler"); int incremental_usage = named_edge_count(incremental_store, incremental_project, "USAGE", "incrementalImportedArgument", "importedIncrementalHandler"); int incremental_calls = named_edge_count(incremental_store, incremental_project, "CALLS", "incrementalImportedArgument", "importedIncrementalHandler"); cbm_store_close(incremental_store); cbm_pipeline_free(incremental); char full_db[512]; snprintf(full_db, sizeof(full_db), "%s/cross-reference-full.db", tmp); cbm_pipeline_t *full = cbm_pipeline_new(tmp, full_db, CBM_MODE_FULL); ASSERT_NOT_NULL(full); ASSERT_EQ(cbm_pipeline_run(full), 0); const char *full_project = cbm_pipeline_project_name(full); cbm_store_t *full_store = cbm_store_open_path(full_db); ASSERT_NOT_NULL(full_store); int full_reference = named_edge_count(full_store, full_project, "CALL_REFERENCE", "incrementalImportedArgument", "importedIncrementalHandler"); int full_usage = named_edge_count(full_store, full_project, "USAGE", "incrementalImportedArgument", "importedIncrementalHandler"); int full_calls = named_edge_count(full_store, full_project, "CALLS", "incrementalImportedArgument", "importedIncrementalHandler"); cbm_store_close(full_store); cbm_pipeline_free(full); th_rmtree(tmp); ASSERT_EQ(full_reference, 1); ASSERT_EQ(full_usage, 0); ASSERT_EQ(full_calls, 0); ASSERT_EQ(incremental_reference, full_reference); ASSERT_EQ(incremental_usage, full_usage); ASSERT_EQ(incremental_calls, full_calls); PASS(); } /* A semantic control file can change an unchanged source file's exact target. * Both target modules deliberately export the same short name: without the * tsconfig path mapping, project-wide unique-name fallback is ambiguous and * cannot make this test pass. The clean full index after the config-only edit * is therefore an exact oracle for which module owns the CALL_REFERENCE. * * RED: incremental hashing tracks source inputs but not the semantic influence * of tsconfig.json. Changing only the alias mapping leaves the same-DB graph * pointed at target_a.ts while a fresh full index points at target_b.ts. */ /* ── Closure-repair routing matrix + convergence ───────────────── * * Every test asserts BOTH the route taken and equality with a fresh full * index. Route equality matters because full-rebuild would satisfy any * convergence assertion vacuously; graph equality matters because a wrong * closure that still routes as CLOSURE_REPAIR is precisely the bug class * this feature must never ship. */ static void closure_probe_repo(const char *tmp) { write_temp_file(tmp, "lib.ts", "export function closureProbeHelper(x: number): string {\n" " return String(x);\n" "}\n"); /* The callback-pass shape produces the exact-value CALL_REFERENCE edge * (same idiom as the tsconfig alias convergence test). */ write_temp_file(tmp, "caller.ts", "import { closureProbeHelper } from './lib';\n" "function acceptClosureProbe(callback: (x: number) => string): void {\n" " void callback;\n" "}\n" "export function closureProbeCaller(): void {\n" " acceptClosureProbe(closureProbeHelper);\n" "}\n"); write_temp_file(tmp, "bystander.ts", "export function closureBystander(): number {\n" " return 7;\n" "}\n"); } /* Fresh full reference build of the same tree into its own DB. */ static void closure_fresh_full(const char *tmp, const char *db_path, int *out_nodes, int *out_edges, int *out_ref_edges, const char *project_hint) { *out_nodes = -1; *out_edges = -2; *out_ref_edges = -3; cbm_unlink(db_path); cbm_remove_db_sidecars(db_path); cbm_pipeline_t *full = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); if (!full) { return; } int rc = cbm_pipeline_run(full); const char *project = cbm_pipeline_project_name(full); if (rc == 0) { cbm_store_t *store = cbm_store_open_path(db_path); if (store) { *out_nodes = cbm_store_count_nodes(store, project); *out_edges = cbm_store_count_edges(store, project); *out_ref_edges = named_edge_to_file_count(store, project, "CALL_REFERENCE", "closureProbeCaller", "closureProbeHelper", "lib.ts"); cbm_store_close(store); } } (void)project_hint; cbm_pipeline_free(full); } /* The manifest hash fans out across workers above a file-count threshold; * below it the serial path runs. Byte-identity of the parallel build is * what the exact no-op route stands on, so pin it with a repo big enough * to take the parallel path on BOTH builds: unchanged bytes must route * NOOP, and one changed file must still classify exactly. */ TEST(pipeline_parallel_manifest_is_byte_stable_above_threshold) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_par_manifest_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); for (int i = 0; i < 72; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "mod_%02d.py", i); snprintf(body, sizeof(body), "def par_manifest_%02d():\n return %d\n", i, i); write_temp_file(tmp, name, body); } char db[512]; snprintf(db, sizeof(db), "%s/manifest.db", tmp); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); cbm_pipeline_free(baseline); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *unchanged = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(unchanged); ASSERT_EQ(cbm_pipeline_run(unchanged), 0); ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_NOOP); cbm_pipeline_free(unchanged); write_temp_file(tmp, "mod_07.py", "def par_manifest_07():\n return 700\n"); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *edited = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(edited); ASSERT_EQ(cbm_pipeline_run(edited), 0); ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); cbm_pipeline_free(edited); th_rmtree(tmp); PASS(); } TEST(pipeline_closure_repair_body_edit_converges_with_fresh_full) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_closure_body_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); closure_probe_repo(tmp); char db[512]; snprintf(db, sizeof(db), "%s/closure.db", tmp); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); /* Body edit only: same exported surface, different body. */ write_temp_file(tmp, "lib.ts", "export function closureProbeHelper(x: number): string {\n" " const doubled = x + x;\n" " return String(doubled);\n" "}\n"); cbm_pipeline_t *incr = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(incr); ASSERT_EQ(cbm_pipeline_run(incr), 0); cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(incr); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); int repaired_nodes = -1; int repaired_edges = -1; int repaired_refs = -1; cbm_store_t *store = cbm_store_open_path(db); ASSERT_NOT_NULL(store); repaired_nodes = cbm_store_count_nodes(store, project); repaired_edges = cbm_store_count_edges(store, project); repaired_refs = named_edge_to_file_count(store, project, "CALL_REFERENCE", "closureProbeCaller", "closureProbeHelper", "lib.ts"); cbm_store_close(store); char full_db[512]; snprintf(full_db, sizeof(full_db), "%s/reference-full.db", tmp); int full_nodes; int full_edges; int full_refs; closure_fresh_full(tmp, full_db, &full_nodes, &full_edges, &full_refs, project); th_rmtree(tmp); ASSERT_EQ(repaired_refs, 1); ASSERT_EQ(repaired_nodes, full_nodes); ASSERT_EQ(repaired_edges, full_edges); ASSERT_EQ(repaired_refs, full_refs); PASS(); } /* Removing a definition (no additions) keeps the closure route AND must drop * the dependent's stale CALL_REFERENCE. This is the assertion legacy partial * could never pass: its QN-keyed re-link resurrected yesterday's edge. */ TEST(pipeline_closure_repair_removed_def_drops_dependent_edge) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_closure_removed_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); closure_probe_repo(tmp); char db[512]; snprintf(db, sizeof(db), "%s/closure.db", tmp); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); cbm_store_t *pre = cbm_store_open_path(db); ASSERT_NOT_NULL(pre); ASSERT_EQ(named_edge_to_file_count(pre, project, "CALL_REFERENCE", "closureProbeCaller", "closureProbeHelper", "lib.ts"), 1); cbm_store_close(pre); /* The helper vanishes; nothing is added. The caller keeps calling a name * that no longer resolves anywhere. */ write_temp_file(tmp, "lib.ts", "export const closureProbeHelper = undefined;\n"); cbm_pipeline_t *incr = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(incr); ASSERT_EQ(cbm_pipeline_run(incr), 0); cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(incr); int repaired_refs = -1; cbm_store_t *store = cbm_store_open_path(db); ASSERT_NOT_NULL(store); repaired_refs = named_edge_to_file_count(store, project, "CALL_REFERENCE", "closureProbeCaller", "closureProbeHelper", "lib.ts"); int repaired_nodes = cbm_store_count_nodes(store, project); int repaired_edges = cbm_store_count_edges(store, project); cbm_store_close(store); char full_db[512]; snprintf(full_db, sizeof(full_db), "%s/reference-full.db", tmp); int full_nodes; int full_edges; int full_refs; closure_fresh_full(tmp, full_db, &full_nodes, &full_edges, &full_refs, project); th_rmtree(tmp); /* Function def -> const def is a label/type change, not an added NAME, so * the closure route must hold — and the stale edge must be gone. */ ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); ASSERT_EQ(repaired_refs, full_refs); ASSERT_EQ(repaired_nodes, full_nodes); ASSERT_EQ(repaired_edges, full_edges); PASS(); } TEST(pipeline_closure_repair_added_name_declines_to_full) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_closure_added_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); closure_probe_repo(tmp); char db[512]; snprintf(db, sizeof(db), "%s/closure.db", tmp); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); cbm_pipeline_free(baseline); /* A NEW name: yesterday's graph cannot know who would now resolve to * it, so the planner must decline. */ write_temp_file(tmp, "lib.ts", "export function closureProbeHelper(x: number): string {\n" " return String(x);\n" "}\n" "export function closureProbeExtra(): number {\n" " return 1;\n" "}\n"); cbm_pipeline_t *incr = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(incr); ASSERT_EQ(cbm_pipeline_run(incr), 0); cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(incr); th_rmtree(tmp); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); PASS(); } TEST(pipeline_closure_repair_new_file_declines_to_full) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_closure_newfile_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); closure_probe_repo(tmp); char db[512]; snprintf(db, sizeof(db), "%s/closure.db", tmp); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); cbm_pipeline_free(baseline); write_temp_file(tmp, "newcomer.ts", "export function closureNewcomer(): void {}\n"); cbm_pipeline_t *incr = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(incr); ASSERT_EQ(cbm_pipeline_run(incr), 0); cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(incr); th_rmtree(tmp); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); PASS(); } static int stem_index(const char *repo, const char *db) { cbm_pipeline_t *p = cbm_pipeline_new(repo, db, CBM_MODE_FULL); if (!p) { return -1; } (void)cbm_pipeline_set_project_name(p, "stem"); cbm_pipeline_set_persistence(p, false); int rc = cbm_pipeline_run(p); cbm_pipeline_free(p); return rc; } static void stem_unlink(const char *tmp, const char *rel) { char path[512]; snprintf(path, sizeof(path), "%s/%s", tmp, rel); (void)cbm_unlink(path); } /* Every collision shape of cbm_stem_marks_t, plus `fillers` plain scripts. */ static void stem_repo(const char *tmp, int fillers) { write_temp_file(tmp, "genproto.sh", "#!/bin/sh\ngen_all() {\n echo all\n}\n"); write_temp_file(tmp, "genproto/one.sh", "#!/bin/sh\ngen_one() {\n echo one\n}\n"); write_temp_file(tmp, "rs/foo.rs", "pub mod bar;\npub fn top() {}\n"); write_temp_file(tmp, "rs/foo/bar.rs", "pub fn inner() {}\n"); write_temp_file(tmp, "c/foo.h", "int foo_add(int a, int b);\n"); write_temp_file(tmp, "c/foo.c", "#include \"foo.h\"\nint foo_add(int a, int b) { return a + b; }\n"); write_temp_file(tmp, "c/foo/x.c", "int foo_x(void) { return 1; }\n"); write_temp_file(tmp, "ts/foo.test.ts", "export function stemProbeA(): number { return 1; }\n"); write_temp_file(tmp, "ts/foo/test/a.ts", "export function stemProbeB(): number { return 2; }\n"); write_temp_file(tmp, "go.mod", "module example.com/stem\n\ngo 1.21\n"); write_temp_file(tmp, "goapi/api.sh", "#!/bin/sh\nrun_api() {\n echo api\n}\n"); write_temp_file(tmp, "goapi/api/server.go", "package api\n\nfunc Serve() {}\n"); write_temp_file(tmp, "goapi/main/main.go", "package main\n\nimport \"example.com/stem/goapi/api\"\n\n" "func main() { api.Serve() }\n"); for (int i = 0; i < fillers; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "fill/f%02d.sh", i); snprintf(body, sizeof(body), "#!/bin/sh\nfill_%02d() {\n echo %d\n}\n", i, i); write_temp_file(tmp, name, body); } } /* A file and the directory beside it with one stem (genproto.sh, genproto/) * shared a QN, and the graph buffer kept only the Folder: the file's Module * was lost. Now one side yields (cbm_stem_marks_t). The Folder is spelled * `stem.genproto/` -- unless the directory is a Go package: it IS that * package's module and Go imports land on it, so the file's Module yields as * `stem.goapi.api#sh`. Rust's foo.rs beside foo/ and C's foo.h/foo.c stem pair * keep their Module QNs; a dotted stem (foo.test.ts, foo/test/) collides by QN * too. Run small (sequential extraction) and past MIN_FILES_FOR_PARALLEL * (parallel workers), which insert Modules on separate paths. */ TEST(pipeline_stem_folder_collision_keeps_both_nodes) { static const char *const checks[][2] = { {"SELECT COUNT(*) FROM nodes WHERE label='Module' AND qualified_name='stem.genproto' AND " "file_path='genproto.sh'", "genproto.sh Module"}, {"SELECT COUNT(*) FROM nodes WHERE label='Folder' AND qualified_name='stem.genproto/' AND " "file_path='genproto'", "genproto/ Folder marked"}, {"SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " "t.id=e.target_id WHERE e.type='CONTAINS_FILE' AND s.qualified_name='stem.genproto/' AND " "t.file_path='genproto/one.sh'", "marked Folder contains its file"}, {"SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " "t.id=e.target_id WHERE e.type='DEFINES' AND s.label='File' AND " "s.file_path='genproto.sh' AND t.label='Module' AND t.qualified_name='stem.genproto'", "genproto.sh defines its Module"}, {"SELECT COUNT(*) FROM nodes WHERE label='Module' AND qualified_name='stem.rs.foo' AND " "file_path='rs/foo.rs'", "Rust foo.rs Module"}, {"SELECT COUNT(*) FROM nodes WHERE label='Module' AND qualified_name='stem.rs.foo.bar' AND " "file_path='rs/foo/bar.rs'", "Rust foo/bar.rs Module"}, {"SELECT COUNT(*) FROM nodes WHERE label='Folder' AND qualified_name='stem.rs.foo/'", "rs/foo/ Folder marked"}, {"SELECT COUNT(*) FROM nodes WHERE label='Module' AND qualified_name='stem.c.foo'", "C stem pair Module"}, {"SELECT COUNT(*) FROM nodes WHERE label='Folder' AND qualified_name='stem.c.foo/'", "c/foo/ Folder marked"}, {"SELECT COUNT(*) FROM nodes WHERE label='Module' AND qualified_name='stem.ts.foo.test' AND " "file_path='ts/foo.test.ts'", "dotted foo.test.ts Module"}, {"SELECT COUNT(*) FROM nodes WHERE label='Folder' AND qualified_name='stem.ts.foo.test/'", "ts/foo/test/ Folder marked"}, {"SELECT COUNT(*) FROM nodes WHERE label='Folder' AND qualified_name='stem.goapi.api'", "Go package Folder keeps its QN"}, {"SELECT COUNT(*) FROM nodes WHERE label='Module' AND qualified_name='stem.goapi.api#sh' " "AND file_path='goapi/api.sh'", "api.sh Module beside the Go package"}, {"SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " "t.id=e.target_id WHERE e.type='DEFINES' AND s.label='File' AND " "s.file_path='goapi/api.sh' AND t.qualified_name='stem.goapi.api#sh'", "api.sh defines its Module"}, {"SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " "t.id=e.target_id WHERE e.type='IMPORTS' AND s.file_path='goapi/main/main.go' AND " "t.label='Folder' AND t.qualified_name='stem.goapi.api'", "Go import lands on the package Folder"}, }; enum { STEM_CHECKS = sizeof(checks) / sizeof(checks[0]) }; static const int fillers[] = {0, 60}; int got[2][STEM_CHECKS]; int rcs[2]; for (int run = 0; run < 2; run++) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_stemdir_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); stem_repo(tmp, fillers[run]); char db[512]; snprintf(db, sizeof(db), "%s/stem.db", tmp); rcs[run] = stem_index(tmp, db); for (int c = 0; c < STEM_CHECKS; c++) { got[run][c] = k8s_inc_count(db, checks[c][0]); } th_rmtree(tmp); } int wrong = 0; for (int run = 0; run < 2; run++) { ASSERT_EQ(rcs[run], 0); for (int c = 0; c < STEM_CHECKS; c++) { if (got[run][c] != 1) { printf(" fillers=%d: %s: %d\n", fillers[run], checks[c][1], got[run][c]); wrong++; } } } ASSERT_EQ(wrong, 0); PASS(); } /* A kustomization names a directory (`dashboards`) and the manifest beside it * with the same stem (`dashboards.yaml`), as kubernetes/k8s.io does. The * file's Module, once swallowed by the Folder, is a node now: the directory * entry must still land on the (marked) Folder, the file entry on the file. */ TEST(pipeline_kustomize_entry_beside_same_stem_file) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_stemkust_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "monitoring/kustomization.yaml", "apiVersion: kustomize.config.k8s.io/v1beta1\n" "kind: Kustomization\n" "resources:\n" " - dashboards\n" " - dashboards.yaml\n"); write_temp_file(tmp, "monitoring/dashboards.yaml", "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: board\n"); write_temp_file(tmp, "monitoring/dashboards/kustomization.yaml", "apiVersion: kustomize.config.k8s.io/v1beta1\n" "kind: Kustomization\n" "resources:\n" " - cm.yaml\n"); write_temp_file(tmp, "monitoring/dashboards/cm.yaml", "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: inner\n"); char db[512]; snprintf(db, sizeof(db), "%s/stemkust.db", tmp); int rc = stem_index(tmp, db); int to_dir = k8s_inc_count( db, "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " "t.id=e.target_id WHERE e.type='IMPORTS' AND " "s.file_path='monitoring/kustomization.yaml' AND t.label='Folder' AND " "t.qualified_name='stem.monitoring.dashboards/'"); int to_file = k8s_inc_count( db, "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " "t.id=e.target_id WHERE e.type='IMPORTS' AND " "s.file_path='monitoring/kustomization.yaml' AND t.label='Module' AND " "t.file_path='monitoring/dashboards.yaml'"); th_rmtree(tmp); ASSERT_EQ(rc, 0); ASSERT_EQ(to_dir, 1); ASSERT_EQ(to_file, 1); PASS(); } /* Deleting a file that takes part in a stem collision changes which side * carries the mark, and only a full rebuild re-spells the survivors, so the * planner declines the closure repair: deleting genproto.sh frees the * Folder's plain QN; deleting a Go package's only Go file leaves a plain * directory, whose Folder then yields to api.sh's Module. A deletion away * from every collision stays a closure repair. */ TEST(incremental_stem_mark_deletion_rebuilds) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_stemdel_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); closure_probe_repo(tmp); write_temp_file(tmp, "genproto.sh", "#!/bin/sh\ngen_all() {\n echo all\n}\n"); write_temp_file(tmp, "genproto/one.sh", "#!/bin/sh\ngen_one() {\n echo one\n}\n"); write_temp_file(tmp, "goapi/api.sh", "#!/bin/sh\nrun_api() {\n echo api\n}\n"); write_temp_file(tmp, "goapi/api/server.go", "package api\n\nfunc Serve() {}\n"); write_temp_file(tmp, "goapi/api/run.sh", "#!/bin/sh\nrun_it() {\n echo run\n}\n"); write_temp_file(tmp, "misc/other.sh", "#!/bin/sh\nother() {\n echo other\n}\n"); char db[512]; snprintf(db, sizeof(db), "%s/stemdel.db", tmp); static const char marked_dir[] = "SELECT COUNT(*) FROM nodes WHERE label='Folder' AND qualified_name='stem.genproto/'"; static const char plain_dir[] = "SELECT COUNT(*) FROM nodes WHERE label='Folder' AND qualified_name='stem.genproto'"; static const char go_side[] = "SELECT COUNT(*) FROM nodes WHERE label='Module' AND qualified_name='stem.goapi.api#sh'"; static const char api_dir[] = "SELECT COUNT(*) FROM nodes WHERE label='Folder' AND qualified_name='stem.goapi.api/'"; static const char api_mod[] = "SELECT COUNT(*) FROM nodes WHERE label='Module' AND " "qualified_name='stem.goapi.api' AND file_path='goapi/api.sh'"; int full_rc = stem_index(tmp, db); int full_marked = k8s_inc_count(db, marked_dir); int full_go_side = k8s_inc_count(db, go_side); stem_unlink(tmp, "misc/other.sh"); int other_rc = stem_index(tmp, db); cbm_incremental_route_t other_route = cbm_pipeline_incremental_test_last_route(); stem_unlink(tmp, "genproto.sh"); int gen_rc = stem_index(tmp, db); cbm_incremental_route_t gen_route = cbm_pipeline_incremental_test_last_route(); int gen_marked = k8s_inc_count(db, marked_dir); int gen_plain = k8s_inc_count(db, plain_dir); stem_unlink(tmp, "goapi/api/server.go"); int go_rc = stem_index(tmp, db); cbm_incremental_route_t go_route = cbm_pipeline_incremental_test_last_route(); int go_side_after = k8s_inc_count(db, go_side); int api_dir_after = k8s_inc_count(db, api_dir); int api_mod_after = k8s_inc_count(db, api_mod); th_rmtree(tmp); ASSERT_EQ(full_rc, 0); ASSERT_EQ(full_marked, 1); ASSERT_EQ(full_go_side, 1); ASSERT_EQ(other_rc, 0); ASSERT_EQ(other_route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); ASSERT_EQ(gen_rc, 0); ASSERT_EQ(gen_route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(gen_marked, 0); ASSERT_EQ(gen_plain, 1); ASSERT_EQ(go_rc, 0); ASSERT_EQ(go_route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(go_side_after, 0); ASSERT_EQ(api_dir_after, 1); ASSERT_EQ(api_mod_after, 1); PASS(); } /* The env join on a closure repair: a reader file edited, the join edges of * the files the repair leaves alone come back from the store (their setters, * image references and readers are proxies there), the edited file's are * recomputed -- the same set a full build gives. */ TEST(incremental_env_join_repair_keeps_unchanged_pairs) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_envjoin_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); closure_probe_repo(tmp); write_temp_file(tmp, "k8s/shop.yaml", "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: shop\nspec:\n" " template:\n spec:\n containers:\n - name: web\n" " image: acme/web:1\n env:\n - name: PORT\n" " value: \"8080\"\n - name: MODE\n" " value: prod\n"); write_temp_file(tmp, "compose.yaml", "services:\n web:\n image: acme/web:1\n build: ./web\n"); write_temp_file(tmp, "web/Dockerfile", "FROM node:20\nCOPY src ./src\nCOPY lib ./lib\nCMD [\"node\", \"src/server.js\"]\n"); write_temp_file(tmp, "web/src/server.js", "const port = process.env.PORT;\nconst mode = process.env.MODE;\n"); write_temp_file(tmp, "web/lib/conf.js", "module.exports = process.env.MODE;\n"); write_temp_file(tmp, "web/lib/more.js", "module.exports = process.env.PORT;\n"); write_temp_file(tmp, "misc/other.sh", "#!/bin/sh\nother() {\n echo other\n}\n"); char db[512]; snprintf(db, sizeof(db), "%s/envjoin.db", tmp); static const char joins[] = "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id " "WHERE e.type='CONFIGURES' AND s.label='EnvVar'"; #define ENVJOIN_PAIR(key, file) \ "SELECT COUNT(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " \ "t.id=e.target_id WHERE e.type='CONFIGURES' AND s.label='EnvVar' AND s.qualified_name LIKE " \ "'%#env/web/" key "' AND t.file_path='" file "'" static const char mode_server[] = ENVJOIN_PAIR("MODE", "web/src/server.js"); static const char mode_conf[] = ENVJOIN_PAIR("MODE", "web/lib/conf.js"); static const char port_server[] = ENVJOIN_PAIR("PORT", "web/src/server.js"); static const char port_more[] = ENVJOIN_PAIR("PORT", "web/lib/more.js"); #undef ENVJOIN_PAIR int full_rc = stem_index(tmp, db); int full_joins = k8s_inc_count(db, joins); int full_mode_server = k8s_inc_count(db, mode_server); write_temp_file(tmp, "web/src/server.js", "const port = process.env.PORT;\n"); int inc_rc = stem_index(tmp, db); cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); int inc_joins = k8s_inc_count(db, joins); int inc_mode_server = k8s_inc_count(db, mode_server); int inc_mode_conf = k8s_inc_count(db, mode_conf); int inc_port_server = k8s_inc_count(db, port_server); int inc_port_more = k8s_inc_count(db, port_more); /* a repair far from every image: each join edge joins two files the * repair leaves alone, and is written again although both ends are * pre-existing rows */ write_temp_file(tmp, "misc/other.sh", "#!/bin/sh\nother() {\n echo changed\n}\n"); int far_rc = stem_index(tmp, db); cbm_incremental_route_t far_route = cbm_pipeline_incremental_test_last_route(); int far_joins = k8s_inc_count(db, joins); int far_mode_conf = k8s_inc_count(db, mode_conf); th_rmtree(tmp); ASSERT_EQ(full_rc, 0); ASSERT_EQ(full_joins, 4); ASSERT_EQ(full_mode_server, 1); ASSERT_EQ(inc_rc, 0); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); ASSERT_EQ(inc_joins, 3); ASSERT_EQ(inc_mode_server, 0); ASSERT_EQ(inc_mode_conf, 1); ASSERT_EQ(inc_port_server, 1); ASSERT_EQ(inc_port_more, 1); ASSERT_EQ(far_rc, 0); ASSERT_EQ(far_route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); ASSERT_EQ(far_joins, 3); ASSERT_EQ(far_mode_conf, 1); PASS(); } /* A script's request to a compose service keeps the host in its Route on a * repair as in a full run: the repair holds the compose services already * (a full run makes them after the scripts), yet only a Kubernetes Service * of the repository makes a Route path-only. */ TEST(incremental_script_request_to_compose_service_keeps_host) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_composeurl_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); closure_probe_repo(tmp); write_temp_file(tmp, "compose.yaml", "services:\n web:\n image: acme/web:1\n"); write_temp_file(tmp, "hack/ping.sh", "#!/bin/sh\ncurl http://web:3000/x\n"); char db[512]; snprintf(db, sizeof(db), "%s/composeurl.db", tmp); static const char kept[] = "SELECT COUNT(*) FROM nodes WHERE label='Route' AND " "qualified_name='__route__GET__http://web:3000/y'"; static const char bare[] = "SELECT COUNT(*) FROM nodes WHERE label='Route' AND " "qualified_name LIKE '__route__GET__/%'"; int full_rc = stem_index(tmp, db); write_temp_file(tmp, "hack/ping.sh", "#!/bin/sh\ncurl http://web:3000/y\n"); int inc_rc = stem_index(tmp, db); cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); int inc_kept = k8s_inc_count(db, kept); int inc_bare = k8s_inc_count(db, bare); th_rmtree(tmp); ASSERT_EQ(full_rc, 0); ASSERT_EQ(inc_rc, 0); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); ASSERT_EQ(inc_kept, 1); ASSERT_EQ(inc_bare, 0); PASS(); } TEST(pipeline_closure_repair_budget_declines_to_full) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_closure_budget_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); closure_probe_repo(tmp); for (int i = 0; i < 12; i++) { char name[64]; char body[256]; snprintf(name, sizeof(name), "budget_%02d.ts", i); snprintf(body, sizeof(body), "export function closureBudget%02d(): number {\n return %d;\n}\n", i, i); write_temp_file(tmp, name, body); } for (int i = 0; i < 18; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "filler_%02d.ts", i); snprintf(body, sizeof(body), "export const closureFiller%02d = %d;\n", i, i); write_temp_file(tmp, name, body); } char db[512]; snprintf(db, sizeof(db), "%s/closure.db", tmp); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); cbm_pipeline_free(baseline); /* Body-edit enough files to clear the absolute floor AND the percentage: * 12 of 33 files is 36%% with 12 > CLOSURE_BUDGET_FLOOR_FILES, so the * planner concedes to a full rebuild. */ for (int i = 0; i < 12; i++) { char name[64]; char body[256]; snprintf(name, sizeof(name), "budget_%02d.ts", i); snprintf(body, sizeof(body), "export function closureBudget%02d(): number {\n return %d + 1;\n}\n", i, i); write_temp_file(tmp, name, body); } cbm_pipeline_t *incr = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(incr); ASSERT_EQ(cbm_pipeline_run(incr), 0); cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(incr); th_rmtree(tmp); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); PASS(); } TEST(pipeline_incremental_tsconfig_alias_change_matches_fresh_full) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_ref_tsconfig_incr_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } static const char target_a_source[] = "export function semanticControlHandler(): void {\n" " const selected = 'a';\n" " void selected;\n" "}\n"; static const char target_b_source[] = "export function semanticControlHandler(): void {\n" " const selected = 'b';\n" " void selected;\n" "}\n"; static const char caller_source[] = "import { semanticControlHandler } from '@semantic-target';\n" "function acceptSemanticControl(callback: () => void): void {\n" " void callback;\n" "}\n" "export function semanticControlCaller(): void {\n" " acceptSemanticControl(semanticControlHandler);\n" "}\n"; static const char initial_tsconfig[] = "{\n" " \"compilerOptions\": {\n" " \"baseUrl\": \".\",\n" " \"paths\": {\"@semantic-target\": [\"./target_a\"]}\n" " }\n" "}\n"; static const char replacement_tsconfig[] = "{\n" " \"compilerOptions\": {\n" " \"baseUrl\": \".\",\n" " \"paths\": {\"@semantic-target\": [\"./target_b\"]}\n" " }\n" "}\n"; static const char initial_jsconfig[] = "{\"compilerOptions\":{\"baseUrl\":\"./one\"}}\n"; static const char replacement_jsconfig[] = "{\"compilerOptions\":{\"baseUrl\":\"./two\"}}\n"; ASSERT_EQ(sizeof(initial_tsconfig), sizeof(replacement_tsconfig)); ASSERT_EQ(sizeof(initial_jsconfig), sizeof(replacement_jsconfig)); write_temp_file(tmp, "target_a.ts", target_a_source); write_temp_file(tmp, "target_b.ts", target_b_source); write_temp_file(tmp, "caller.ts", caller_source); write_temp_file(tmp, "tsconfig.json", initial_tsconfig); write_temp_file(tmp, "jsconfig.json", initial_jsconfig); char tsconfig_path[512]; char jsconfig_path[512]; snprintf(tsconfig_path, sizeof(tsconfig_path), "%s/tsconfig.json", tmp); snprintf(jsconfig_path, sizeof(jsconfig_path), "%s/jsconfig.json", tmp); const time_t fixed_config_mtime = 1700000100; ASSERT_EQ(pipeline_test_set_mtime(tsconfig_path, fixed_config_mtime, 222222222L), 0); ASSERT_EQ(pipeline_test_set_mtime(jsconfig_path, fixed_config_mtime, 333333333L), 0); char incremental_db[512]; snprintf(incremental_db, sizeof(incremental_db), "%s/tsconfig-reference-incremental.db", tmp); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, incremental_db, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); const char *baseline_project = cbm_pipeline_project_name(baseline); cbm_store_t *baseline_store = cbm_store_open_path(incremental_db); ASSERT_NOT_NULL(baseline_store); ASSERT_EQ(named_node_count(baseline_store, baseline_project, "semanticControlHandler"), 2); ASSERT_EQ(named_edge_to_file_count(baseline_store, baseline_project, "CALL_REFERENCE", "semanticControlCaller", "semanticControlHandler", "target_a.ts"), 1); ASSERT_EQ(named_edge_to_file_count(baseline_store, baseline_project, "CALL_REFERENCE", "semanticControlCaller", "semanticControlHandler", "target_b.ts"), 0); ASSERT_EQ(named_edge_count(baseline_store, baseline_project, "USAGE", "semanticControlCaller", "semanticControlHandler"), 0); ASSERT_EQ(named_edge_count(baseline_store, baseline_project, "CALLS", "semanticControlCaller", "semanticControlHandler"), 0); cbm_file_hash_t selected_config_hash = {0}; ASSERT_EQ(cbm_store_get_file_hash(baseline_store, baseline_project, "tsconfig.json", &selected_config_hash), CBM_STORE_OK); char expected_config_hash[CBM_SHA256_HEX_LEN + 1]; cbm_sha256_hex(initial_tsconfig, strlen(initial_tsconfig), expected_config_hash); ASSERT_NOT_NULL(selected_config_hash.sha256); ASSERT_STR_EQ(selected_config_hash.sha256, expected_config_hash); cbm_store_clear_file_hash(&selected_config_hash); cbm_file_hash_t unselected_config_hash = {0}; ASSERT_EQ(cbm_store_get_file_hash(baseline_store, baseline_project, "jsconfig.json", &unselected_config_hash), CBM_STORE_NOT_FOUND); cbm_store_close(baseline_store); cbm_pipeline_free(baseline); /* The lower-priority jsconfig is not a semantic input while tsconfig is * present in the same directory. Its byte change must remain a no-op. */ write_temp_file(tmp, "jsconfig.json", replacement_jsconfig); ASSERT_EQ(pipeline_test_set_mtime(jsconfig_path, fixed_config_mtime, 333333333L), 0); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *unselected = cbm_pipeline_new(tmp, incremental_db, CBM_MODE_FULL); ASSERT_NOT_NULL(unselected); ASSERT_EQ(cbm_pipeline_run(unselected), 0); ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_NOOP); cbm_pipeline_free(unselected); /* The source files remain byte-for-byte unchanged; selected control bytes * change at the same length and mtime. */ write_temp_file(tmp, "tsconfig.json", replacement_tsconfig); ASSERT_EQ(pipeline_test_set_mtime(tsconfig_path, fixed_config_mtime, 222222222L), 0); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *incremental = cbm_pipeline_new(tmp, incremental_db, CBM_MODE_FULL); ASSERT_NOT_NULL(incremental); ASSERT_EQ(cbm_pipeline_run(incremental), 0); /* Config-mediated retargeting is the closure route's hardest case: no * source file changed, yet the caller's CALL_REFERENCE must move from * target_a.ts to target_b.ts. Since alias-config governance landed this * runs as a closure repair, and the convergence assertions below now * prove that route rather than being satisfied by a full rebuild. */ ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); const char *incremental_project = cbm_pipeline_project_name(incremental); cbm_store_t *incremental_store = cbm_store_open_path(incremental_db); ASSERT_NOT_NULL(incremental_store); int incremental_reference_a = named_edge_to_file_count(incremental_store, incremental_project, "CALL_REFERENCE", "semanticControlCaller", "semanticControlHandler", "target_a.ts"); int incremental_reference_b = named_edge_to_file_count(incremental_store, incremental_project, "CALL_REFERENCE", "semanticControlCaller", "semanticControlHandler", "target_b.ts"); int incremental_usage = named_edge_count(incremental_store, incremental_project, "USAGE", "semanticControlCaller", "semanticControlHandler"); int incremental_calls = named_edge_count(incremental_store, incremental_project, "CALLS", "semanticControlCaller", "semanticControlHandler"); cbm_store_close(incremental_store); cbm_pipeline_free(incremental); char full_db[512]; snprintf(full_db, sizeof(full_db), "%s/tsconfig-reference-full.db", tmp); cbm_pipeline_t *full = cbm_pipeline_new(tmp, full_db, CBM_MODE_FULL); ASSERT_NOT_NULL(full); ASSERT_EQ(cbm_pipeline_run(full), 0); const char *full_project = cbm_pipeline_project_name(full); cbm_store_t *full_store = cbm_store_open_path(full_db); ASSERT_NOT_NULL(full_store); int full_reference_a = named_edge_to_file_count(full_store, full_project, "CALL_REFERENCE", "semanticControlCaller", "semanticControlHandler", "target_a.ts"); int full_reference_b = named_edge_to_file_count(full_store, full_project, "CALL_REFERENCE", "semanticControlCaller", "semanticControlHandler", "target_b.ts"); int full_usage = named_edge_count(full_store, full_project, "USAGE", "semanticControlCaller", "semanticControlHandler"); int full_calls = named_edge_count(full_store, full_project, "CALLS", "semanticControlCaller", "semanticControlHandler"); cbm_store_close(full_store); cbm_pipeline_free(full); th_rmtree(tmp); /* Prove the fresh oracle changed only because the alias now selects B. */ ASSERT_EQ(full_reference_a, 0); ASSERT_EQ(full_reference_b, 1); ASSERT_EQ(full_usage, 0); ASSERT_EQ(full_calls, 0); /* Incremental output must converge exactly on the fresh target and kind. */ ASSERT_EQ(incremental_reference_a, full_reference_a); ASSERT_EQ(incremental_reference_b, full_reference_b); ASSERT_EQ(incremental_usage, full_usage); ASSERT_EQ(incremental_calls, full_calls); PASS(); } #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API static int read_published_generation(const char *db_path, char *out, size_t out_size) { cbm_store_t *store = cbm_store_open_path_query(db_path); if (!store) { return CBM_STORE_ERR; } int rc = cbm_store_generation(store, out, out_size); cbm_store_close(store); return rc; } static bool split_published_generation(const char *generation, char uid[18], unsigned long long *mutation) { if (!generation || strlen(generation) < 19 || generation[0] != 'u' || generation[17] != 'g') { return false; } memcpy(uid, generation, 17); uid[17] = '\0'; char *end = NULL; unsigned long long parsed = strtoull(generation + 18, &end, 10); if (!end || end == generation + 18 || *end != '\0') { return false; } *mutation = parsed; return true; } /* Every published database must carry cursor-generation metadata. A complete * replacement gets a fresh database identity; an isolated delta clones the * live database and advances only its mutation counter. */ TEST(pipeline_publication_stamps_full_and_delta_generations) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_publish_cursor_generation_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "generation.py", "def PublishedGeneration():\n return 1\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FAST); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); cbm_pipeline_free(baseline); char first[128]; ASSERT_EQ(read_published_generation(db_path, first, sizeof(first)), CBM_STORE_OK); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *replacement = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(replacement); ASSERT_EQ(cbm_pipeline_run(replacement), 0); ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_FORCED_FULL); cbm_pipeline_free(replacement); char second[128]; ASSERT_EQ(read_published_generation(db_path, second, sizeof(second)), CBM_STORE_OK); write_temp_file(tmp, "generation.py", "def PublishedGeneration():\n return 2\n"); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *delta = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(delta); ASSERT_EQ(cbm_pipeline_run(delta), 0); ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); cbm_pipeline_free(delta); char third[128]; ASSERT_EQ(read_published_generation(db_path, third, sizeof(third)), CBM_STORE_OK); char first_uid[18]; char second_uid[18]; char third_uid[18]; unsigned long long first_mutation = 0; unsigned long long second_mutation = 0; unsigned long long third_mutation = 0; ASSERT_TRUE(split_published_generation(first, first_uid, &first_mutation)); ASSERT_TRUE(split_published_generation(second, second_uid, &second_mutation)); ASSERT_TRUE(split_published_generation(third, third_uid, &third_mutation)); ASSERT_TRUE(strcmp(first_uid, second_uid) != 0); ASSERT_STR_EQ(second_uid, third_uid); ASSERT_TRUE(third_mutation > second_mutation); th_rmtree(tmp); cbm_pipeline_incremental_test_reset_faults(); PASS(); } static void observe_named_generation(const char *db_path, const char *project, const char *before_name, const char *after_name, int *before_count, int *after_count) { *before_count = -1; *after_count = -1; cbm_store_t *store = cbm_store_open_path(db_path); if (!store) { return; } *before_count = named_node_count(store, project, before_name); *after_count = named_node_count(store, project, after_name); cbm_store_close(store); } static int count_generation_stage_artifacts(const char *dir_path, const char *db_basename) { cbm_dir_t *dir = cbm_opendir(dir_path); if (!dir) { return -1; } char prefix[256]; snprintf(prefix, sizeof(prefix), "%s.stage.", db_basename); size_t prefix_len = strlen(prefix); int count = 0; cbm_dirent_t *entry; while ((entry = cbm_readdir(dir)) != NULL) { if (strncmp(entry->name, prefix, prefix_len) == 0) { count++; } } cbm_closedir(dir); return count; } typedef struct { const char *path; const char *replacement; int write_rc; } manifest_race_mutation_t; static void mutate_semantic_input_before_final_manifest(void *userdata) { manifest_race_mutation_t *mutation = (manifest_race_mutation_t *)userdata; mutation->write_rc = th_write_file(mutation->path, mutation->replacement); } TEST(pipeline_late_source_limit_preserves_previous_generation) { char *repo = th_mktempdir("cbm_pipeline_late_limit"); ASSERT_NOT_NULL(repo); ASSERT_EQ(th_write_file(TH_PATH(repo, "first.c"), "int first;\n"), 0); char db_path[512]; char late_path[512]; snprintf(db_path, sizeof(db_path), "%s/index.db", repo); snprintf(late_path, sizeof(late_path), "%s/late.py", repo); manifest_race_mutation_t mutation = { .path = late_path, .replacement = "late = 2\n", .write_rc = -1, }; cbm_pipeline_incremental_test_before_final_manifest_once( mutate_semantic_input_before_final_manifest, &mutation); cbm_pipeline_t *pipeline = cbm_pipeline_new(repo, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(pipeline); cbm_index_resource_policy_t policy; cbm_index_policy_init(&policy); policy.max_files = (cbm_index_limit_u64_t){.enabled = true, .value = 1}; cbm_pipeline_set_resource_policy(pipeline, &policy); int rc = cbm_pipeline_run(pipeline); cbm_index_resource_violation_t violation = {0}; cbm_pipeline_get_resource_violation(pipeline, &violation); cbm_pipeline_free(pipeline); cbm_pipeline_incremental_test_reset_faults(); ASSERT_EQ(mutation.write_rc, 0); ASSERT_EQ(rc, CBM_PIPELINE_RESOURCE_LIMIT); ASSERT_EQ(violation.resource, CBM_INDEX_RESOURCE_FILES); ASSERT_EQ(violation.observed, 2); ASSERT_EQ(violation.limit, 1); ASSERT_FALSE(cbm_file_exists(db_path)); th_cleanup(repo); PASS(); } /* A graph and its exact manifest are one generation. If source bytes change * after extraction but before publication, the mixed generation must be * rejected and the previous live DB preserved. */ TEST(pipeline_source_mutation_before_publication_preserves_previous_generation) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_publish_generation_race_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); char source_path[512]; char db_path[512]; snprintf(source_path, sizeof(source_path), "%s/generation.py", tmp); snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); ASSERT_EQ(th_write_file(source_path, "def StableGeneration():\n return 1\n"), 0); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); ASSERT_EQ(th_write_file(source_path, "def ExtractedGeneration():\n return 2\n"), 0); manifest_race_mutation_t mutation = { .path = source_path, .replacement = "def FinalGeneration():\n return 3\n", .write_rc = -1, }; cbm_pipeline_incremental_test_before_final_manifest_once( mutate_semantic_input_before_final_manifest, &mutation); cbm_pipeline_t *raced = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(raced); int raced_rc = cbm_pipeline_run(raced); cbm_pipeline_free(raced); int raced_stable = -1; int raced_extracted = -1; observe_named_generation(db_path, project, "StableGeneration", "ExtractedGeneration", &raced_stable, &raced_extracted); int raced_stage_count = count_generation_stage_artifacts(tmp, "generation.db"); cbm_pipeline_t *retry = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(retry); int retry_rc = cbm_pipeline_run(retry); cbm_pipeline_free(retry); int retry_stable = -1; int retry_final = -1; observe_named_generation(db_path, project, "StableGeneration", "FinalGeneration", &retry_stable, &retry_final); int retry_stage_count = count_generation_stage_artifacts(tmp, "generation.db"); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(mutation.write_rc, 0); ASSERT_EQ(raced_rc, CBM_PIPELINE_ABORT_PRESERVE_DB); ASSERT_EQ(raced_stable, 1); ASSERT_EQ(raced_extracted, 0); ASSERT_EQ(raced_stage_count, 0); ASSERT_EQ(retry_rc, 0); ASSERT_EQ(retry_stable, 0); ASSERT_EQ(retry_final, 1); ASSERT_EQ(retry_stage_count, 0); PASS(); } /* Publication must never write through a name another local process can * predict. The staging database used to be ".stage..", * which anyone can compute in advance; publication then unlinked that name * and wrote it, so a symlink planted in the gap redirects the write to a * target of the attacker's choosing. * * Winning that timing window is not something a test should attempt -- a * test that has to win a race is a coin flip, not a gate. The property that * REMOVES the window is deterministic: publication must not touch any * predictable name at all. Canaries occupy every name the old scheme could * have picked, and all must survive. Against the old code exactly one is * consumed, whichever serial the counter had reached. * * This calls cbm_pipeline_publish_generation directly. The only in-tree * caller sits behind CBM_INCREMENTAL_TEST_API, so going through the pipeline * would never reach the code under test. */ TEST(pipeline_publication_never_uses_a_predictable_staging_path) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_publish_predictable_stage_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); enum { PREDICTABLE_CANARIES = 32 }; static const char canary[] = "canary-must-survive\n"; char canary_path[PREDICTABLE_CANARIES][640]; for (int i = 0; i < PREDICTABLE_CANARIES; i++) { snprintf(canary_path[i], sizeof(canary_path[i]), "%s.stage.%ld.%d", db_path, (long)getpid(), i + 1); ASSERT_EQ(th_write_file(canary_path[i], canary), 0); } cbm_gbuf_t *gb = cbm_gbuf_new("predictable-stage-proj", tmp); ASSERT_NOT_NULL(gb); cbm_pipeline_generation_t generation = { .gbuf = gb, .final_db_path = db_path, .project = "predictable-stage-proj", .cancelled = NULL, .manifest = NULL, .manifest_count = 0, .adr_content = NULL, .coverage = NULL, .coverage_count = 0, }; int publish_rc = cbm_pipeline_publish_generation(&generation); cbm_gbuf_free(gb); int survived = 0; int intact = 0; for (int i = 0; i < PREDICTABLE_CANARIES; i++) { FILE *f = cbm_fopen(canary_path[i], "rb"); if (!f) { continue; } survived++; char buf[64] = {0}; size_t n = fread(buf, 1, sizeof(buf) - 1, f); (void)fclose(f); if (n == strlen(canary) && memcmp(buf, canary, n) == 0) { intact++; } } th_rmtree(tmp); ASSERT_EQ(publish_rc, 0); /* Not one may be unlinked, truncated, or written through. */ ASSERT_EQ(survived, PREDICTABLE_CANARIES); ASSERT_EQ(intact, PREDICTABLE_CANARIES); PASS(); } static int count_substring(const char *haystack, const char *needle) { int count = 0; size_t needle_len = strlen(needle); for (const char *at = strstr(haystack, needle); at; at = strstr(at + needle_len, needle)) { count++; } return count; } static int count_nested_stage_entries(const char *dir_path, const char *db_basename) { cbm_dir_t *dir = cbm_opendir(dir_path); if (!dir) { return -1; } size_t base_len = strlen(db_basename); int count = 0; cbm_dirent_t *entry; while ((entry = cbm_readdir(dir)) != NULL) { if (strncmp(entry->name, db_basename, base_len) == 0 && count_substring(entry->name + base_len, ".stage.") >= 2) { count++; } } cbm_closedir(dir); return count; } /* #1839: the outer run rewrites the pipeline's db_path to its stage, so the * inner publication (dump or delta clone) minted ITS stage from a stage: * .stage.A.stage.B, plus -wal/-shm under WAL. A generation's stage is a * sibling of the live database, whichever path it is minted from; a database * whose own basename merely contains ".stage." is not a stage and keeps its * full name. */ TEST(pipeline_stage_names_never_nest) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_stage_nesting_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); char odd_db[512]; snprintf(odd_db, sizeof(odd_db), "%s/x.stage.y.db", tmp); char *outer = cbm_pipeline_create_staging_path(db_path); ASSERT_NOT_NULL(outer); char *inner = cbm_pipeline_create_staging_path(outer); ASSERT_NOT_NULL(inner); char *odd_stage = cbm_pipeline_create_staging_path(odd_db); ASSERT_NOT_NULL(odd_stage); size_t db_len = strlen(db_path); size_t odd_len = strlen(odd_db); int outer_tokens = count_substring(outer, ".stage."); int inner_tokens = count_substring(inner, ".stage."); bool inner_is_sibling = strncmp(inner, db_path, db_len) == 0 && strncmp(inner + db_len, ".stage.", 7) == 0; bool inner_distinct = strcmp(inner, outer) != 0; bool odd_keeps_basename = strncmp(odd_stage, odd_db, odd_len) == 0 && strncmp(odd_stage + odd_len, ".stage.", 7) == 0; int nested_entries = count_nested_stage_entries(tmp, "generation.db"); cbm_pipeline_discard_stage(inner); cbm_pipeline_discard_stage(outer); cbm_pipeline_discard_stage(odd_stage); int leftover_entries = count_generation_stage_artifacts(tmp, "generation.db") + count_generation_stage_artifacts(tmp, "x.stage.y.db"); free(inner); free(outer); free(odd_stage); th_rmtree(tmp); ASSERT_EQ(outer_tokens, 1); ASSERT_EQ(inner_tokens, 1); ASSERT_TRUE(inner_is_sibling); ASSERT_TRUE(inner_distinct); ASSERT_TRUE(odd_keeps_basename); ASSERT_EQ(nested_entries, 0); ASSERT_EQ(leftover_entries, 0); PASS(); } static bool path_exists(const char *path) { cbm_path_info_t info; return cbm_path_info_utf8(path, &info) == CBM_PATH_INFO_OK; } /* #1839: a minted stage is OWNED through an exclusive kernel lock on its * ".lock" sidecar for exactly as long as the stage exists. A second * holder cannot take it while the writer is live; discarding the stage frees * the name and removes the sidecar, so nothing stays behind. */ TEST(pipeline_minted_stage_is_owned_until_released) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_stage_owner_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); char *stage = cbm_pipeline_create_staging_path(db_path); ASSERT_NOT_NULL(stage); char lock_path[600]; snprintf(lock_path, sizeof(lock_path), "%s.lock", stage); bool lock_present_while_live = path_exists(lock_path); int hold_while_live = cbm_pipeline_stage_lock_hold(stage); if (hold_while_live >= 0) { cbm_pipeline_stage_lock_drop(stage, hold_while_live); } cbm_pipeline_discard_stage(stage); bool stage_gone = !path_exists(stage); bool lock_gone = !path_exists(lock_path); int hold_after_discard = cbm_pipeline_stage_lock_hold(stage); int second_holder = cbm_pipeline_stage_lock_hold(stage); if (second_holder >= 0) { cbm_pipeline_stage_lock_drop(stage, second_holder); } cbm_pipeline_stage_lock_drop(stage, hold_after_discard); int hold_after_drop = cbm_pipeline_stage_lock_hold(stage); cbm_pipeline_stage_lock_drop(stage, hold_after_drop); int leftovers = count_generation_stage_artifacts(tmp, "generation.db"); free(stage); th_rmtree(tmp); ASSERT_TRUE(lock_present_while_live); ASSERT_EQ(hold_while_live, -1); ASSERT_TRUE(stage_gone); ASSERT_TRUE(lock_gone); ASSERT_TRUE(hold_after_discard >= 0); ASSERT_EQ(second_holder, -1); ASSERT_TRUE(hold_after_drop >= 0); ASSERT_EQ(leftovers, 0); PASS(); } static bool file_has_content(const char *path, const char *expected) { FILE *f = cbm_fopen(path, "rb"); if (!f) { return false; } char buf[256] = {0}; size_t n = fread(buf, 1, sizeof(buf) - 1, f); (void)fclose(f); return n == strlen(expected) && memcmp(buf, expected, n) == 0; } /* #1839 / #1864: a stage a dead writer left beside a VALID database is swept * by the next run and never influences its route. The 0-byte shape is what a * worker killed before its backup wrote a page leaves behind; a stale -shm * beside it is swept with it. */ TEST(pipeline_stale_zero_byte_stage_beside_valid_db_routes_incremental_and_is_swept) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_stale_stage_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "generation.py", "def StableGeneration():\n return 1\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); char stale_stage[600]; char stale_shm[600]; snprintf(stale_stage, sizeof(stale_stage), "%s.stage.deadbe", db_path); snprintf(stale_shm, sizeof(stale_shm), "%s.stage.deadbe-shm", db_path); ASSERT_EQ(th_write_file(stale_stage, ""), 0); ASSERT_EQ(th_write_file(stale_shm, "stale-shm"), 0); /* A body-only change: no added names, so the planner may repair. */ write_temp_file(tmp, "generation.py", "def StableGeneration():\n return 2\n"); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *incr = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(incr); int incr_rc = cbm_pipeline_run(incr); cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(incr); bool stale_stage_gone = !path_exists(stale_stage); bool stale_shm_gone = !path_exists(stale_shm); int stage_count = count_generation_stage_artifacts(tmp, "generation.db"); int stable_count = -1; int absent_count = -1; observe_named_generation(db_path, project, "StableGeneration", "NeverDefined", &stable_count, &absent_count); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(incr_rc, 0); ASSERT_TRUE(route != CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_TRUE(route != CBM_INCREMENTAL_ROUTE_NONE); ASSERT_TRUE(stale_stage_gone); ASSERT_TRUE(stale_shm_gone); ASSERT_EQ(stage_count, 0); ASSERT_EQ(stable_count, 1); ASSERT_EQ(absent_count, 0); PASS(); } /* A killed run cleans up nothing, and until 2026-09-18 its staging database was * reclaimed only when THAT project was indexed again: a kernel index killed by * the OOM killer left ~15 GB parked until someone re-indexed the kernel, and * indefinitely if nobody ever did. Indexing ANY project now reclaims the * orphans of every project sharing that cache directory -- while the lock probe * still keeps a LIVE writer's stage, whichever project owns it. */ TEST(pipeline_sweep_reclaims_orphans_of_other_projects_and_keeps_their_live_stages) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_stage_sweep_other_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "generation.py", "def StableGeneration():\n return 1\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/mine.db", tmp); /* Two stages of a DIFFERENT project sharing the cache directory: one whose * writer died (nothing holds its lock) and one whose writer is alive. */ char other_dead[600]; char other_dead_wal[600]; char other_live[600]; snprintf(other_dead, sizeof(other_dead), "%s/other.db.stage.cccccc", tmp); snprintf(other_dead_wal, sizeof(other_dead_wal), "%s/other.db.stage.cccccc-wal", tmp); snprintf(other_live, sizeof(other_live), "%s/other.db.stage.dddddd", tmp); static const char other_live_bytes[] = "other-project-live-bytes"; ASSERT_EQ(th_write_file(other_dead, "other-project-dead-bytes"), 0); ASSERT_EQ(th_write_file(other_dead_wal, "other-project-dead-wal"), 0); ASSERT_EQ(th_write_file(other_live, other_live_bytes), 0); int other_live_fd = cbm_pipeline_stage_lock_hold(other_live); ASSERT_TRUE(other_live_fd >= 0); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *mine = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(mine); ASSERT_EQ(cbm_pipeline_run(mine), 0); cbm_pipeline_free(mine); bool dead_gone = !path_exists(other_dead); bool dead_wal_gone = !path_exists(other_dead_wal); bool live_kept = file_has_content(other_live, other_live_bytes); cbm_pipeline_stage_lock_drop(other_live, other_live_fd); ASSERT_TRUE(dead_gone); /* reclaimed even though it belongs to "other" */ ASSERT_TRUE(dead_wal_gone); /* sidecars go with their stage */ ASSERT_TRUE(live_kept); /* another project's LIVE stage is untouched */ PASS(); } /* #1839: the sweep removes exactly the stages nobody owns. A dead writer's * stage (main, -wal, and the unlocked .lock sidecar its death left) goes; a * stage whose writer is LIVE -- here the test, holding its lock -- is kept * byte for byte, and goes only once that lock is dropped. No timing: liveness * is the kernel lock, nothing else. */ TEST(pipeline_sweep_removes_dead_writer_stage_keeps_locked_stage) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_stage_sweep_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "generation.py", "def StableGeneration():\n return 1\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); char dead_stage[600]; char dead_wal[600]; char dead_lock[600]; char live_stage[600]; char live_lock[600]; snprintf(dead_stage, sizeof(dead_stage), "%s.stage.aaaaaa", db_path); snprintf(dead_wal, sizeof(dead_wal), "%s.stage.aaaaaa-wal", db_path); snprintf(dead_lock, sizeof(dead_lock), "%s.stage.aaaaaa.lock", db_path); snprintf(live_stage, sizeof(live_stage), "%s.stage.bbbbbb", db_path); snprintf(live_lock, sizeof(live_lock), "%s.stage.bbbbbb.lock", db_path); static const char live_bytes[] = "live-stage-bytes"; ASSERT_EQ(th_write_file(dead_stage, "dead-stage-bytes"), 0); ASSERT_EQ(th_write_file(dead_wal, "dead-wal"), 0); ASSERT_EQ(th_write_file(dead_lock, ""), 0); ASSERT_EQ(th_write_file(live_stage, live_bytes), 0); int live_fd = cbm_pipeline_stage_lock_hold(live_stage); ASSERT_TRUE(live_fd >= 0); write_temp_file(tmp, "generation.py", "def StableGeneration():\n return 2\n"); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *first = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(first); int first_rc = cbm_pipeline_run(first); cbm_pipeline_free(first); bool dead_stage_gone = !path_exists(dead_stage); bool dead_wal_gone = !path_exists(dead_wal); bool dead_lock_gone = !path_exists(dead_lock); bool live_kept = file_has_content(live_stage, live_bytes); bool live_lock_kept = path_exists(live_lock); int stable_after_first = -1; int absent_after_first = -1; observe_named_generation(db_path, project, "StableGeneration", "NeverDefined", &stable_after_first, &absent_after_first); cbm_pipeline_stage_lock_drop(live_stage, live_fd); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *second = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(second); int second_rc = cbm_pipeline_run(second); cbm_pipeline_free(second); bool live_gone = !path_exists(live_stage); bool live_lock_gone = !path_exists(live_lock); int stage_count = count_generation_stage_artifacts(tmp, "generation.db"); int stable_after_second = -1; int absent_after_second = -1; observe_named_generation(db_path, project, "StableGeneration", "NeverDefined", &stable_after_second, &absent_after_second); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(first_rc, 0); ASSERT_TRUE(dead_stage_gone); ASSERT_TRUE(dead_wal_gone); ASSERT_TRUE(dead_lock_gone); ASSERT_TRUE(live_kept); ASSERT_TRUE(live_lock_kept); ASSERT_EQ(stable_after_first, 1); ASSERT_EQ(absent_after_first, 0); ASSERT_EQ(second_rc, 0); ASSERT_TRUE(live_gone); ASSERT_TRUE(live_lock_gone); ASSERT_EQ(stage_count, 0); ASSERT_EQ(stable_after_second, 1); ASSERT_EQ(absent_after_second, 0); PASS(); } /* #2111 create->register TOCTOU guard. The bug this pins: create_staging_path() * used to make the stage's main file visible (via mkstemp) BEFORE taking its * sidecar lock, and sweep_orphan_stages() treats an absent lock sidecar * (ENOENT) as a confirmed-dead writer (kernel released the lock on death). So a * second, concurrent cbm_pipeline_run() against the SAME final_path, landing * its own sweep in that narrow unlocked window, removed the first run's * in-flight stage out from under it: every extraction pass still completed * (none touch the stage file on disk), but the publish that followed found its * own stage gone. Two writers racing the same project is a real scenario this * PR's own sweep exists to clean up after (auto_index; the recently-fixed * stale-rendezvous-recovery retry path) -- not hypothetical, and exactly the * shape of #2111's windows-guards red (every pass logged success, nothing was * ever committed, "Pipeline failed" surfaced generic; on Windows the sidecar * collision surfaced as EACCES/errno=13). * * The fix takes the sidecar lock BEFORE the main file becomes visible, so this * hook -- fired the instant the main file exists -- finds the stage already * lock-protected and the racing sweep keeps it. The hook fires at the same * point under the old ordering, where the lock was NOT yet held, so this test * goes RED if that ordering ever regresses. * * The racing run is cancelled immediately so it never reaches ITS OWN * publish -- isolating the sweep's effect on the first run's stage from the * separate question of two full runs both completing for the same project. */ typedef struct { const char *tmp_dir; const char *db_path; bool stage_survived; } racing_sweep_arg_t; static bool find_sole_stage_path(const char *dir, const char *db_basename, char *out, size_t out_sz) { cbm_dir_t *d = cbm_opendir(dir); if (!d) { return false; } char prefix[256]; snprintf(prefix, sizeof(prefix), "%s.stage.", db_basename); size_t prefix_len = strlen(prefix); bool found = false; cbm_dirent_t *entry; while ((entry = cbm_readdir(d)) != NULL) { size_t name_len = strlen(entry->name); enum { STAGE_SUFFIX_RANDOM_CHARS = 6 }; /* mirrors CBM_STAGE_SUFFIX_RANDOM_CHARS */ if (strncmp(entry->name, prefix, prefix_len) == 0 && name_len == prefix_len + STAGE_SUFFIX_RANDOM_CHARS) { snprintf(out, out_sz, "%s/%s", dir, entry->name); found = true; break; } } cbm_closedir(d); return found; } static void *racing_sweep_thread(void *arg) { racing_sweep_arg_t *a = (racing_sweep_arg_t *)arg; cbm_pipeline_t *p = cbm_pipeline_new(a->tmp_dir, a->db_path, CBM_MODE_FULL); if (p) { /* Its own sweep_orphan_stages() runs at the very start, before this * run mints its own stage -- exactly like the first run's. Cancel * immediately: this run must reach the sweep and nothing past it. */ cbm_pipeline_cancel(p); (void)cbm_pipeline_run(p); cbm_pipeline_free(p); } return NULL; } /* Fired from inside the FIRST run's create_staging_path(), the instant its * stage main file exists (under the fix, with its sidecar lock already held; * under the old create-then-lock ordering, before the lock was taken): run a * second, cancelled cbm_pipeline_run() for the same project synchronously on * another thread, so its sweep has every chance to reach the stage before * control returns to the first run. */ static void racing_sweep_hook(void *userdata) { racing_sweep_arg_t *a = (racing_sweep_arg_t *)userdata; char stage_path[600] = {0}; bool had_stage = find_sole_stage_path(a->tmp_dir, "generation.db", stage_path, sizeof(stage_path)); cbm_thread_t tid; if (cbm_thread_create(&tid, 0, racing_sweep_thread, a) == 0) { cbm_thread_join(&tid); } a->stage_survived = had_stage && path_exists(stage_path); } TEST(pipeline_concurrent_sweep_must_not_remove_inflight_stage) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_stage_race_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "generation.py", "def StableGeneration():\n return 1\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); cbm_pipeline_incremental_test_reset_faults(); racing_sweep_arg_t race_arg = {.tmp_dir = tmp, .db_path = db_path, .stage_survived = false}; cbm_pipeline_incremental_test_after_stage_created_once(racing_sweep_hook, &race_arg); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(p)); int rc = cbm_pipeline_run(p); cbm_pipeline_free(p); bool db_exists = path_exists(db_path); int defined_count = -1; int absent_count = -1; observe_named_generation(db_path, project, "StableGeneration", "NeverDefined", &defined_count, &absent_count); int stage_count = count_generation_stage_artifacts(tmp, "generation.db"); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_TRUE(race_arg.stage_survived); ASSERT_EQ(rc, 0); ASSERT_TRUE(db_exists); ASSERT_EQ(defined_count, 1); ASSERT_EQ(absent_count, 0); ASSERT_EQ(stage_count, 0); PASS(); } static int live_noop_run(const char *repo, const char *db_path, cbm_incremental_route_t *route) { cbm_pipeline_incremental_test_reset_faults(); *route = CBM_INCREMENTAL_ROUTE_NONE; cbm_pipeline_t *p = cbm_pipeline_new(repo, db_path, CBM_MODE_FULL); if (!p) { return CBM_NOT_FOUND; } int rc = cbm_pipeline_run(p); *route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(p); return rc; } /* An unchanged re-index proves the exact no-op on the LIVE generation. It * never copies the database into a stage, seals the copy and renames it back * (on a kernel-scale index that copy alone was a 19 GB write per call): the * live file keeps its identity, size and mtime, no stage artifact remains, * and the graph still serves. */ TEST(pipeline_unchanged_reindex_leaves_live_generation_untouched) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_live_noop_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "generation.py", "def StableGeneration():\n return 1\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); char *project = cbm_project_name_from_path(tmp); ASSERT_NOT_NULL(project); cbm_incremental_route_t first_route; int first_rc = live_noop_run(tmp, db_path, &first_route); cbm_path_info_t before = {0}; struct stat before_st; bool before_ok = cbm_path_info_utf8(db_path, &before) == CBM_PATH_INFO_OK && stat(db_path, &before_st) == 0; cbm_incremental_route_t noop_route; int noop_rc = live_noop_run(tmp, db_path, &noop_route); cbm_path_info_t after = {0}; struct stat after_st; bool after_ok = cbm_path_info_utf8(db_path, &after) == CBM_PATH_INFO_OK && stat(db_path, &after_st) == 0; int defined_count = -1; int absent_count = -1; observe_named_generation(db_path, project, "StableGeneration", "NeverDefined", &defined_count, &absent_count); int stage_count = count_generation_stage_artifacts(tmp, "generation.db"); cbm_pipeline_incremental_test_reset_faults(); free(project); th_rmtree(tmp); ASSERT_EQ(first_rc, 0); ASSERT_TRUE(before_ok); ASSERT_EQ(noop_rc, 0); ASSERT_EQ(noop_route, CBM_INCREMENTAL_ROUTE_NOOP); ASSERT_TRUE(after_ok); ASSERT_TRUE(after_st.st_ino == before_st.st_ino); ASSERT_TRUE(after.mtime_ns == before.mtime_ns); ASSERT_TRUE(after.size == before.size); ASSERT_EQ(stage_count, 0); ASSERT_EQ(defined_count, 1); ASSERT_EQ(absent_count, 0); PASS(); } /* The live proof answers only for an exact manifest. Every real change -- a * same-size edit that keeps the original mtime, an addition, a deletion and * a rename -- still routes past it into a rebuild that the graph shows, and * only the unchanged re-index after them takes the no-op. */ TEST(pipeline_live_noop_proof_never_hides_a_change) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_live_noop_change_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "alpha.py", "def AlphaDefined():\n return 1\n"); write_temp_file(tmp, "beta.py", "def BetaDefined():\n return 2\n"); char db_path[512]; char alpha_path[512]; char beta_path[512]; char gamma_path[512]; char delta_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); snprintf(alpha_path, sizeof(alpha_path), "%s/alpha.py", tmp); snprintf(beta_path, sizeof(beta_path), "%s/beta.py", tmp); snprintf(gamma_path, sizeof(gamma_path), "%s/gamma.py", tmp); snprintf(delta_path, sizeof(delta_path), "%s/delta.py", tmp); char *project = cbm_project_name_from_path(tmp); ASSERT_NOT_NULL(project); cbm_incremental_route_t initial_route; int initial_rc = live_noop_run(tmp, db_path, &initial_route); /* Same size, original mtime restored: only the bytes differ. */ cbm_path_info_t alpha_info = {0}; bool alpha_stat_ok = cbm_path_info_utf8(alpha_path, &alpha_info) == CBM_PATH_INFO_OK; write_temp_file(tmp, "alpha.py", "def AlphbDefined():\n return 1\n"); int mtime_rc = pipeline_test_set_mtime(alpha_path, (time_t)(alpha_info.mtime_ns / 1000000000), (long)(alpha_info.mtime_ns % 1000000000)); cbm_path_info_t edited_info = {0}; bool edited_same_stat = cbm_path_info_utf8(alpha_path, &edited_info) == CBM_PATH_INFO_OK && edited_info.size == alpha_info.size && edited_info.mtime_ns == alpha_info.mtime_ns; cbm_incremental_route_t modify_route; int modify_rc = live_noop_run(tmp, db_path, &modify_route); int old_alpha = -1; int new_alpha = -1; observe_named_generation(db_path, project, "AlphaDefined", "AlphbDefined", &old_alpha, &new_alpha); write_temp_file(tmp, "gamma.py", "def GammaDefined():\n return 3\n"); cbm_incremental_route_t add_route; int add_rc = live_noop_run(tmp, db_path, &add_route); int gamma_added = -1; int unused = -1; observe_named_generation(db_path, project, "GammaDefined", "NeverDefined", &gamma_added, &unused); int unlink_rc = cbm_unlink(beta_path); cbm_incremental_route_t delete_route; int delete_rc = live_noop_run(tmp, db_path, &delete_route); int beta_left = -1; observe_named_generation(db_path, project, "BetaDefined", "NeverDefined", &beta_left, &unused); int rename_rc = cbm_rename_replace(gamma_path, delta_path); cbm_incremental_route_t rename_route; int renamed_rc = live_noop_run(tmp, db_path, &rename_route); int gamma_renamed = -1; observe_named_generation(db_path, project, "GammaDefined", "NeverDefined", &gamma_renamed, &unused); int old_path_hash = CBM_STORE_OK; int new_path_hash = CBM_STORE_NOT_FOUND; cbm_store_t *store = cbm_store_open_path_query(db_path); if (store) { cbm_file_hash_t hash = {0}; old_path_hash = cbm_store_get_file_hash(store, project, "gamma.py", &hash); cbm_store_clear_file_hash(&hash); new_path_hash = cbm_store_get_file_hash(store, project, "delta.py", &hash); cbm_store_clear_file_hash(&hash); cbm_store_close(store); } cbm_incremental_route_t final_route; int final_rc = live_noop_run(tmp, db_path, &final_route); cbm_pipeline_incremental_test_reset_faults(); free(project); th_rmtree(tmp); ASSERT_EQ(initial_rc, 0); ASSERT_TRUE(alpha_stat_ok); ASSERT_EQ(mtime_rc, 0); ASSERT_TRUE(edited_same_stat); ASSERT_EQ(modify_rc, 0); ASSERT_TRUE(modify_route != CBM_INCREMENTAL_ROUTE_NOOP); ASSERT_EQ(old_alpha, 0); ASSERT_EQ(new_alpha, 1); ASSERT_EQ(add_rc, 0); ASSERT_TRUE(add_route != CBM_INCREMENTAL_ROUTE_NOOP); ASSERT_EQ(gamma_added, 1); ASSERT_EQ(unlink_rc, 0); ASSERT_EQ(delete_rc, 0); ASSERT_TRUE(delete_route != CBM_INCREMENTAL_ROUTE_NOOP); ASSERT_EQ(beta_left, 0); ASSERT_EQ(rename_rc, 0); ASSERT_EQ(renamed_rc, 0); ASSERT_TRUE(rename_route != CBM_INCREMENTAL_ROUTE_NOOP); ASSERT_EQ(gamma_renamed, 1); ASSERT_EQ(old_path_hash, CBM_STORE_NOT_FOUND); ASSERT_EQ(new_path_hash, CBM_STORE_OK); ASSERT_EQ(final_rc, 0); ASSERT_EQ(final_route, CBM_INCREMENTAL_ROUTE_NOOP); PASS(); } static char g_route_log_capture[8192]; static atomic_flag g_route_log_spin = ATOMIC_FLAG_INIT; /* Keeps only the route decisions; worker threads log too, and only the * appends need serialising. */ static void capture_route_log_sink(const char *line) { if (!line || !strstr(line, "pipeline.route")) { return; } while (atomic_flag_test_and_set_explicit(&g_route_log_spin, memory_order_acquire)) {} size_t used = strlen(g_route_log_capture); size_t avail = sizeof(g_route_log_capture) - used; if (avail > 1) { int n = snprintf(g_route_log_capture + used, avail, "%s\n", line); if (n < 0 || (size_t)n >= avail) { g_route_log_capture[sizeof(g_route_log_capture) - 1] = '\0'; } } atomic_flag_clear_explicit(&g_route_log_spin, memory_order_release); } /* #1864: a first index has no previous generation. The route probe used to * open the run's own EMPTY stage placeholder, fail its integrity check, and * warn "reason=invalid_existing_db" on every first index of every project, * which the report read as a corrupted database and a crash loop. A fresh * index says what it is. */ TEST(pipeline_fresh_index_never_reports_invalid_existing_db) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_fresh_route_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "generation.py", "def StableGeneration():\n return 1\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); g_route_log_capture[0] = '\0'; CBMLogLevel previous_level = cbm_log_get_level(); cbm_log_set_level(CBM_LOG_DEBUG); cbm_log_set_sink(capture_route_log_sink); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *fresh = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); int fresh_rc = fresh ? cbm_pipeline_run(fresh) : -1; cbm_pipeline_free(fresh); cbm_log_set_sink(NULL); cbm_log_set_level(previous_level); bool invalid_reported = strstr(g_route_log_capture, "invalid_existing_db") != NULL; bool fresh_reported = strstr(g_route_log_capture, "reason=no_existing_db") != NULL; bool db_present = path_exists(db_path); int stage_count = count_generation_stage_artifacts(tmp, "generation.db"); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(fresh_rc, 0); ASSERT_TRUE(!invalid_reported); ASSERT_TRUE(fresh_reported); ASSERT_TRUE(db_present); ASSERT_EQ(stage_count, 0); PASS(); } /* Discovery and extraction must describe the same immutable generation. A * source file created after extraction is not present in the original file * list, so merely re-hashing that list cannot detect the race. Publication * must abort, leave the old DB intact, and let a clean retry discover both * files. */ TEST(pipeline_source_addition_before_publication_preserves_previous_generation) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_publish_addition_race_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); char source_path[512]; char late_path[512]; char db_path[512]; snprintf(source_path, sizeof(source_path), "%s/generation.py", tmp); snprintf(late_path, sizeof(late_path), "%s/late.py", tmp); snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); ASSERT_EQ(th_write_file(source_path, "def StableAdditionGeneration():\n return 1\n"), 0); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); ASSERT_EQ(th_write_file(source_path, "def ExtractedAdditionGeneration():\n return 2\n"), 0); manifest_race_mutation_t mutation = { .path = late_path, .replacement = "def LateDiscoveredGeneration():\n return 3\n", .write_rc = -1, }; cbm_pipeline_incremental_test_before_final_manifest_once( mutate_semantic_input_before_final_manifest, &mutation); cbm_pipeline_t *raced = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(raced); int raced_rc = cbm_pipeline_run(raced); cbm_pipeline_free(raced); int raced_stable = -1; int raced_extracted = -1; int raced_late = -1; cbm_store_t *raced_store = cbm_store_open_path(db_path); if (raced_store) { raced_stable = named_node_count(raced_store, project, "StableAdditionGeneration"); raced_extracted = named_node_count(raced_store, project, "ExtractedAdditionGeneration"); raced_late = named_node_count(raced_store, project, "LateDiscoveredGeneration"); cbm_store_close(raced_store); } int raced_stage_count = count_generation_stage_artifacts(tmp, "generation.db"); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *retry = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(retry); int retry_rc = cbm_pipeline_run(retry); cbm_incremental_route_t retry_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(retry); int retry_stable = -1; int retry_extracted = -1; int retry_late = -1; cbm_store_t *retry_store = cbm_store_open_path(db_path); if (retry_store) { retry_stable = named_node_count(retry_store, project, "StableAdditionGeneration"); retry_extracted = named_node_count(retry_store, project, "ExtractedAdditionGeneration"); retry_late = named_node_count(retry_store, project, "LateDiscoveredGeneration"); cbm_store_close(retry_store); } int retry_stage_count = count_generation_stage_artifacts(tmp, "generation.db"); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(mutation.write_rc, 0); ASSERT_EQ(raced_rc, CBM_PIPELINE_ABORT_PRESERVE_DB); ASSERT_EQ(raced_stable, 1); ASSERT_EQ(raced_extracted, 0); ASSERT_EQ(raced_late, 0); ASSERT_EQ(raced_stage_count, 0); ASSERT_EQ(retry_rc, 0); ASSERT_EQ(retry_route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(retry_stable, 0); ASSERT_EQ(retry_extracted, 1); ASSERT_EQ(retry_late, 1); ASSERT_EQ(retry_stage_count, 0); PASS(); } /* A selected path-alias config is an extraction input, not just manifest * metadata. Rewriting it after extraction must not publish an A-resolved graph * beside a B-hashed manifest; that combination would make the next run a * false no-op and permanently retain the stale target. */ TEST(pipeline_tsconfig_mutation_before_publication_preserves_previous_generation) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_publish_tsconfig_race_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); static const char target_a_source[] = "export function publicationRaceHandler(): void {\n" " const selected = 'a';\n" " void selected;\n" "}\n"; static const char target_b_source[] = "export function publicationRaceHandler(): void {\n" " const selected = 'b';\n" " void selected;\n" "}\n"; static const char stable_caller_source[] = "import { publicationRaceHandler } from '@publication-race';\n" "function acceptPublicationRace(callback: () => void): void {\n" " void callback;\n" "}\n" "export function PublicationRaceStableCaller(): void {\n" " acceptPublicationRace(publicationRaceHandler);\n" "}\n"; static const char extracted_caller_source[] = "import { publicationRaceHandler } from '@publication-race';\n" "function acceptPublicationRace(callback: () => void): void {\n" " void callback;\n" "}\n" "export function PublicationRaceExtractedCaller(): void {\n" " acceptPublicationRace(publicationRaceHandler);\n" "}\n"; static const char tsconfig_a[] = "{\n" " \"compilerOptions\": {\n" " \"baseUrl\": \".\",\n" " \"paths\": {\"@publication-race\": [\"./target_a\"]}\n" " }\n" "}\n"; static const char tsconfig_b[] = "{\n" " \"compilerOptions\": {\n" " \"baseUrl\": \".\",\n" " \"paths\": {\"@publication-race\": [\"./target_b\"]}\n" " }\n" "}\n"; write_temp_file(tmp, "target_a.ts", target_a_source); write_temp_file(tmp, "target_b.ts", target_b_source); write_temp_file(tmp, "caller.ts", stable_caller_source); write_temp_file(tmp, "tsconfig.json", tsconfig_a); char caller_path[512]; char tsconfig_path[512]; char db_path[512]; snprintf(caller_path, sizeof(caller_path), "%s/caller.ts", tmp); snprintf(tsconfig_path, sizeof(tsconfig_path), "%s/tsconfig.json", tmp); snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); ASSERT_EQ(th_write_file(caller_path, extracted_caller_source), 0); manifest_race_mutation_t mutation = { .path = tsconfig_path, .replacement = tsconfig_b, .write_rc = -1, }; cbm_pipeline_incremental_test_before_final_manifest_once( mutate_semantic_input_before_final_manifest, &mutation); cbm_pipeline_t *raced = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(raced); int raced_rc = cbm_pipeline_run(raced); cbm_pipeline_free(raced); int raced_stable_a = -1; int raced_stable_b = -1; int raced_extracted_nodes = -1; cbm_store_t *raced_store = cbm_store_open_path(db_path); if (raced_store) { raced_stable_a = named_edge_to_file_count(raced_store, project, "CALL_REFERENCE", "PublicationRaceStableCaller", "publicationRaceHandler", "target_a.ts"); raced_stable_b = named_edge_to_file_count(raced_store, project, "CALL_REFERENCE", "PublicationRaceStableCaller", "publicationRaceHandler", "target_b.ts"); raced_extracted_nodes = named_node_count(raced_store, project, "PublicationRaceExtractedCaller"); cbm_store_close(raced_store); } int raced_stage_count = count_generation_stage_artifacts(tmp, "generation.db"); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *retry = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(retry); int retry_rc = cbm_pipeline_run(retry); cbm_incremental_route_t retry_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(retry); int retry_stable_nodes = -1; int retry_extracted_a = -1; int retry_extracted_b = -1; int retry_extracted_usage = -1; int retry_extracted_calls = -1; cbm_store_t *retry_store = cbm_store_open_path(db_path); if (retry_store) { retry_stable_nodes = named_node_count(retry_store, project, "PublicationRaceStableCaller"); retry_extracted_a = named_edge_to_file_count(retry_store, project, "CALL_REFERENCE", "PublicationRaceExtractedCaller", "publicationRaceHandler", "target_a.ts"); retry_extracted_b = named_edge_to_file_count(retry_store, project, "CALL_REFERENCE", "PublicationRaceExtractedCaller", "publicationRaceHandler", "target_b.ts"); retry_extracted_usage = named_edge_count(retry_store, project, "USAGE", "PublicationRaceExtractedCaller", "publicationRaceHandler"); retry_extracted_calls = named_edge_count(retry_store, project, "CALLS", "PublicationRaceExtractedCaller", "publicationRaceHandler"); cbm_store_close(retry_store); } int retry_stage_count = count_generation_stage_artifacts(tmp, "generation.db"); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(mutation.write_rc, 0); ASSERT_EQ(raced_rc, CBM_PIPELINE_ABORT_PRESERVE_DB); ASSERT_EQ(raced_stable_a, 1); ASSERT_EQ(raced_stable_b, 0); ASSERT_EQ(raced_extracted_nodes, 0); ASSERT_EQ(raced_stage_count, 0); ASSERT_EQ(retry_rc, 0); ASSERT_EQ(retry_route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(retry_stable_nodes, 0); ASSERT_EQ(retry_extracted_a, 0); ASSERT_EQ(retry_extracted_b, 1); ASSERT_EQ(retry_extracted_usage, 0); ASSERT_EQ(retry_extracted_calls, 0); ASSERT_EQ(retry_stage_count, 0); PASS(); } /* Metadata participates in exact-input compatibility. Old coverage schema or * an upgrade to a more comprehensive discovery/index mode must force a * complete replacement even when every semantic-input byte is unchanged; the * replacement must write current metadata so the next identical run is a * true no-op. Cheaper requests retain fuller published coverage separately. */ TEST(pipeline_exact_inputs_migrate_coverage_metadata_and_index_mode) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_manifest_metadata_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "generation.py", "def ExactMetadataGeneration():\n return 1\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FAST); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *exact_before_migration = cbm_pipeline_new(tmp, db_path, CBM_MODE_FAST); ASSERT_NOT_NULL(exact_before_migration); ASSERT_EQ(cbm_pipeline_run(exact_before_migration), 0); cbm_incremental_route_t exact_before_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(exact_before_migration); cbm_store_t *metadata_store = cbm_store_open_path(db_path); ASSERT_NOT_NULL(metadata_store); cbm_coverage_row_t *coverage_rows = NULL; int coverage_count = 0; ASSERT_EQ(cbm_store_coverage_get(metadata_store, project, &coverage_rows, &coverage_count), CBM_STORE_OK); cbm_coverage_meta_t current_meta = {0}; ASSERT_EQ(cbm_store_coverage_meta_get(metadata_store, project, ¤t_meta), CBM_STORE_OK); cbm_coverage_meta_t legacy_meta = current_meta; legacy_meta.coverage_version = 1; ASSERT_EQ(cbm_store_coverage_replace_ex(metadata_store, project, coverage_rows, coverage_count, &legacy_meta), CBM_STORE_OK); cbm_store_free_coverage(coverage_rows, coverage_count); cbm_store_coverage_meta_clear(¤t_meta); cbm_store_close(metadata_store); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *migration = cbm_pipeline_new(tmp, db_path, CBM_MODE_FAST); ASSERT_NOT_NULL(migration); ASSERT_EQ(cbm_pipeline_run(migration), 0); cbm_incremental_route_t migration_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(migration); metadata_store = cbm_store_open_path(db_path); ASSERT_NOT_NULL(metadata_store); cbm_coverage_meta_t migrated_meta = {0}; ASSERT_EQ(cbm_store_coverage_meta_get(metadata_store, project, &migrated_meta), CBM_STORE_OK); int migrated_version = migrated_meta.coverage_version; bool migrated_hashes_complete = migrated_meta.hash_records_complete; char migrated_mode[32]; snprintf(migrated_mode, sizeof(migrated_mode), "%s", migrated_meta.index_mode ? migrated_meta.index_mode : ""); cbm_store_coverage_meta_clear(&migrated_meta); cbm_store_close(metadata_store); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *mode_change = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(mode_change); ASSERT_EQ(cbm_pipeline_run(mode_change), 0); cbm_incremental_route_t mode_change_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(mode_change); metadata_store = cbm_store_open_path(db_path); ASSERT_NOT_NULL(metadata_store); cbm_coverage_meta_t full_meta = {0}; ASSERT_EQ(cbm_store_coverage_meta_get(metadata_store, project, &full_meta), CBM_STORE_OK); int full_version = full_meta.coverage_version; bool full_hashes_complete = full_meta.hash_records_complete; char full_mode[32]; snprintf(full_mode, sizeof(full_mode), "%s", full_meta.index_mode ? full_meta.index_mode : ""); cbm_store_coverage_meta_clear(&full_meta); cbm_store_close(metadata_store); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *exact_full = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(exact_full); ASSERT_EQ(cbm_pipeline_run(exact_full), 0); cbm_incremental_route_t exact_full_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(exact_full); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(exact_before_route, CBM_INCREMENTAL_ROUTE_NOOP); ASSERT_EQ(migration_route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(migrated_version, CBM_SEMANTIC_INDEX_VERSION); ASSERT_TRUE(migrated_hashes_complete); ASSERT_STR_EQ(migrated_mode, "fast"); ASSERT_EQ(mode_change_route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(full_version, CBM_SEMANTIC_INDEX_VERSION); ASSERT_TRUE(full_hashes_complete); ASSERT_STR_EQ(full_mode, "full"); ASSERT_EQ(exact_full_route, CBM_INCREMENTAL_ROUTE_NOOP); PASS(); } typedef struct { bool published; int rename_calls; int export_count; int exports_before_publish; } artifact_publish_observer_t; static artifact_publish_observer_t *g_artifact_publish_observer; static void observe_artifact_publish_log(const char *line) { if (g_artifact_publish_observer && line && strstr(line, "msg=artifact.export")) { g_artifact_publish_observer->export_count++; if (!g_artifact_publish_observer->published) { g_artifact_publish_observer->exports_before_publish++; } } } static int observe_successful_publish_rename(const char *staging_path, const char *final_path, void *arg) { artifact_publish_observer_t *observer = (artifact_publish_observer_t *)arg; observer->rename_calls++; int rc = cbm_rename_replace(staging_path, final_path); observer->published = rc == 0; return rc; } static int run_observing_artifact_publish(cbm_pipeline_t *pipeline, artifact_publish_observer_t *observer) { cbm_pipeline_set_rename_hook_for_tests(pipeline, observe_successful_publish_rename, observer); CBMLogLevel previous_level = cbm_log_get_level(); CBMLogFormat previous_format = cbm_log_get_format(); g_artifact_publish_observer = observer; cbm_log_set_level(CBM_LOG_INFO); cbm_log_set_format(CBM_LOG_FORMAT_TEXT); cbm_log_set_sink_ex(observe_artifact_publish_log, CBM_LOG_SINK_REPLACE); int rc = cbm_pipeline_run(pipeline); cbm_log_set_sink(NULL); cbm_log_set_format(previous_format); cbm_log_set_level(previous_level); g_artifact_publish_observer = NULL; return rc; } static bool pipeline_reports_excluded_dir(cbm_pipeline_t *pipeline, const char *rel_path) { char **excluded = NULL; int excluded_count = 0; cbm_pipeline_get_excluded(pipeline, &excluded, &excluded_count); for (int i = 0; i < excluded_count; i++) { if (excluded[i] && strcmp(excluded[i], rel_path) == 0) { return true; } } return false; } typedef struct { int rc; cbm_incremental_route_t route; bool tools_excluded; artifact_publish_observer_t publish; } observed_fast_run_t; static observed_fast_run_t run_observed_fast_pipeline(const char *repo_path, const char *db_path) { observed_fast_run_t result = {.rc = CBM_NOT_FOUND}; cbm_pipeline_t *pipeline = cbm_pipeline_new(repo_path, db_path, CBM_MODE_FAST); if (!pipeline) { return result; } result.rc = run_observing_artifact_publish(pipeline, &result.publish); result.route = cbm_pipeline_incremental_test_last_route(); result.tools_excluded = pipeline_reports_excluded_dir(pipeline, "tools"); cbm_pipeline_free(pipeline); return result; } typedef struct { int rc; int before_nodes; int after_nodes; } imported_generation_t; static imported_generation_t import_artifact_generation(const char *repo_path, const char *import_path, const char *project) { imported_generation_t result = { .rc = cbm_artifact_import(repo_path, import_path), .before_nodes = -1, .after_nodes = -1, }; if (result.rc == 0) { observe_named_generation(import_path, project, "StoredBefore", "StoredAfter", &result.before_nodes, &result.after_nodes); } return result; } static bool stored_mode_is_full(const char *db_path, const char *project) { cbm_store_t *store = cbm_store_open_path(db_path); if (!store) { return false; } cbm_coverage_meta_t meta = {0}; bool full = cbm_store_coverage_meta_get(store, project, &meta) == CBM_STORE_OK && meta.index_mode && strcmp(meta.index_mode, "full") == 0; cbm_store_coverage_meta_clear(&meta); cbm_store_close(store); return full; } /* Once a repository contains a shared artifact, every subsequently published * full generation must refresh it, even when persistence was not explicitly * requested on that invocation. Otherwise the live DB advances while a clean * teammate import silently restores the previous graph. */ TEST(pipeline_existing_artifact_refreshes_after_default_forced_full_reindex) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_artifact_refresh_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); char source_path[512]; char db_path[512]; char imported_path[512]; snprintf(source_path, sizeof(source_path), "%s/generation.py", tmp); snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); snprintf(imported_path, sizeof(imported_path), "%s/imported.db", tmp); ASSERT_EQ(th_write_file(source_path, "def ArtifactGenerationBefore():\n return 1\n"), 0); cbm_pipeline_incremental_test_reset_faults(); artifact_publish_observer_t baseline_observer = {0}; cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); cbm_pipeline_set_persistence(baseline, true); int baseline_rc = run_observing_artifact_publish(baseline, &baseline_observer); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); ASSERT_TRUE(cbm_artifact_exists(tmp)); ASSERT_EQ(th_write_file(source_path, "def ArtifactGenerationAfter():\n return 2\n"), 0); cbm_pipeline_incremental_test_reset_faults(); artifact_publish_observer_t reindex_observer = {0}; cbm_pipeline_t *default_reindex = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(default_reindex); int reindex_rc = run_observing_artifact_publish(default_reindex, &reindex_observer); cbm_incremental_route_t reindex_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(default_reindex); /* A derived artifact must not become an input that forces another rebuild, * and refreshing it must not switch the authoritative DB back to WAL. */ cbm_pipeline_incremental_test_reset_faults(); artifact_publish_observer_t explicit_observer = {0}; cbm_pipeline_t *explicit_noop = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(explicit_noop); cbm_pipeline_set_persistence(explicit_noop, true); int explicit_rc = run_observing_artifact_publish(explicit_noop, &explicit_observer); cbm_incremental_route_t explicit_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(explicit_noop); cbm_pipeline_incremental_test_reset_faults(); artifact_publish_observer_t observer = {0}; cbm_pipeline_t *unchanged = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(unchanged); int unchanged_rc = run_observing_artifact_publish(unchanged, &observer); cbm_incremental_route_t unchanged_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(unchanged); char journal_mode[16] = {0}; sqlite3 *raw = NULL; sqlite3_stmt *journal_stmt = NULL; int journal_ok = sqlite3_open_v2(db_path, &raw, SQLITE_OPEN_READONLY, NULL) == SQLITE_OK && sqlite3_prepare_v2(raw, "PRAGMA journal_mode;", -1, &journal_stmt, NULL) == SQLITE_OK && sqlite3_step(journal_stmt) == SQLITE_ROW; if (journal_ok) { const unsigned char *mode = sqlite3_column_text(journal_stmt, 0); if (!mode || snprintf(journal_mode, sizeof(journal_mode), "%s", (const char *)mode) >= (int)sizeof(journal_mode)) { journal_ok = 0; } } sqlite3_finalize(journal_stmt); if (raw) { sqlite3_close(raw); } int live_before = -1; int live_after = -1; cbm_store_t *live_store = cbm_store_open_path(db_path); if (live_store) { live_before = named_node_count(live_store, project, "ArtifactGenerationBefore"); live_after = named_node_count(live_store, project, "ArtifactGenerationAfter"); cbm_store_close(live_store); } int import_rc = cbm_artifact_import(tmp, imported_path); int artifact_before = -1; int artifact_after = -1; cbm_store_t *artifact_store = import_rc == 0 ? cbm_store_open_path(imported_path) : NULL; if (artifact_store) { artifact_before = named_node_count(artifact_store, project, "ArtifactGenerationBefore"); artifact_after = named_node_count(artifact_store, project, "ArtifactGenerationAfter"); cbm_store_close(artifact_store); } cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(baseline_rc, 0); ASSERT_EQ(baseline_observer.rename_calls, 1); ASSERT_EQ(baseline_observer.exports_before_publish, 0); ASSERT_EQ(baseline_observer.export_count, 1); ASSERT_EQ(reindex_rc, 0); ASSERT_EQ(reindex_route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(reindex_observer.rename_calls, 1); ASSERT_EQ(reindex_observer.exports_before_publish, 0); ASSERT_EQ(reindex_observer.export_count, 1); /* The no-ops are proven on the live generation and publish nothing; the * artifact is still refreshed, from that already published database. */ ASSERT_EQ(explicit_rc, 0); ASSERT_EQ(explicit_route, CBM_INCREMENTAL_ROUTE_NOOP); ASSERT_EQ(explicit_observer.rename_calls, 0); ASSERT_EQ(explicit_observer.export_count, 1); ASSERT_EQ(unchanged_rc, 0); ASSERT_EQ(unchanged_route, CBM_INCREMENTAL_ROUTE_NOOP); ASSERT_EQ(observer.rename_calls, 0); ASSERT_EQ(observer.export_count, 1); ASSERT_TRUE(journal_ok); ASSERT_STR_EQ(journal_mode, "delete"); ASSERT_EQ(live_before, 0); ASSERT_EQ(live_after, 1); ASSERT_EQ(import_rc, 0); ASSERT_EQ(artifact_before, 0); ASSERT_EQ(artifact_after, 1); PASS(); } /* Cancellation observed after predump must be an explicit preserved abort, * never a false success. The retry then publishes the edited generation. */ TEST(pipeline_full_cancel_after_predump_preserves_previous_generation) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_publish_cancel_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); static const char before_source[] = "def BeforeCancel():\n return 1\n"; static const char after_source[] = "def AfterCancel():\n return 2\n# changed\n"; write_temp_file(tmp, "generation.py", before_source); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); write_temp_file(tmp, "generation.py", after_source); cbm_pipeline_incremental_test_cancel_after_predump_once(); cbm_pipeline_t *cancelled = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(cancelled); int cancelled_rc = cbm_pipeline_run(cancelled); cbm_incremental_route_t cancelled_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(cancelled); int cancelled_before = -1; int cancelled_after = -1; observe_named_generation(db_path, project, "BeforeCancel", "AfterCancel", &cancelled_before, &cancelled_after); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *retry = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(retry); int retry_rc = cbm_pipeline_run(retry); cbm_incremental_route_t retry_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(retry); int retry_before = -1; int retry_after = -1; observe_named_generation(db_path, project, "BeforeCancel", "AfterCancel", &retry_before, &retry_after); int stage_count = count_generation_stage_artifacts(tmp, "generation.db"); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(cancelled_rc, CBM_PIPELINE_ABORT_PRESERVE_DB); ASSERT_EQ(cancelled_route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(cancelled_before, 1); ASSERT_EQ(cancelled_after, 0); ASSERT_EQ(retry_rc, 0); ASSERT_EQ(retry_route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(retry_before, 0); ASSERT_EQ(retry_after, 1); ASSERT_EQ(stage_count, 0); PASS(); } /* Cancellation can arrive while the destination DB is being quiesced. It must * still be observed at the final commit boundary, before the atomic rename. */ TEST(pipeline_full_cancel_after_destination_prepare_preserves_previous_generation) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_publish_late_cancel_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "generation.py", "def BeforeLateCancel():\n return 1\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); write_temp_file(tmp, "generation.py", "def AfterLateCancel():\n return 2\n# changed\n"); cbm_pipeline_incremental_test_cancel_after_destination_prepare_once(); cbm_pipeline_t *cancelled = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(cancelled); int cancelled_rc = cbm_pipeline_run(cancelled); cbm_pipeline_free(cancelled); int before_count = -1; int after_count = -1; observe_named_generation(db_path, project, "BeforeLateCancel", "AfterLateCancel", &before_count, &after_count); int stage_count = count_generation_stage_artifacts(tmp, "generation.db"); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(cancelled_rc, CBM_PIPELINE_ABORT_PRESERVE_DB); ASSERT_EQ(before_count, 1); ASSERT_EQ(after_count, 0); ASSERT_EQ(stage_count, 0); PASS(); } /* A failure after graph serialization but before metadata/publication must * discard only the staged generation. The final path remains the complete old * graph and a clean retry converges. */ TEST(pipeline_full_persist_failure_after_stage_dump_preserves_previous_generation) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_publish_full_fail_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "generation.py", "def BeforeFullPersist():\n return 1\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); write_temp_file(tmp, "generation.py", "def AfterFullPersist():\n return 2\n# changed generation\n"); cbm_pipeline_incremental_test_fail_after_stage_dump_once(); cbm_pipeline_t *faulted = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(faulted); int faulted_rc = cbm_pipeline_run(faulted); cbm_pipeline_free(faulted); int faulted_before = -1; int faulted_after = -1; observe_named_generation(db_path, project, "BeforeFullPersist", "AfterFullPersist", &faulted_before, &faulted_after); int faulted_stage_count = count_generation_stage_artifacts(tmp, "generation.db"); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *retry = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(retry); int retry_rc = cbm_pipeline_run(retry); cbm_pipeline_free(retry); int retry_before = -1; int retry_after = -1; observe_named_generation(db_path, project, "BeforeFullPersist", "AfterFullPersist", &retry_before, &retry_after); int retry_stage_count = count_generation_stage_artifacts(tmp, "generation.db"); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(faulted_rc, CBM_PIPELINE_PERSIST_FAILED); ASSERT_EQ(faulted_before, 1); ASSERT_EQ(faulted_after, 0); ASSERT_EQ(faulted_stage_count, 0); ASSERT_EQ(retry_rc, 0); ASSERT_EQ(retry_before, 0); ASSERT_EQ(retry_after, 1); ASSERT_EQ(retry_stage_count, 0); PASS(); } /* Keep the legacy partial route test-only so its fail-closed behavior remains * covered even though production semantic changes now force a full rebuild. */ TEST(pipeline_incremental_persist_failure_preserves_previous_generation_and_retries) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_publish_incr_fail_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "generation.py", "def BeforeIncrementalPersist():\n return 1\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); write_temp_file(tmp, "generation.py", "def AfterIncrementalPersist():\n return 2\n# changed generation\n"); cbm_pipeline_incremental_test_force_legacy_partial_once(); cbm_pipeline_incremental_test_fail_after_stage_dump_once(); cbm_pipeline_t *faulted = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(faulted); int faulted_rc = cbm_pipeline_run(faulted); cbm_incremental_route_t faulted_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(faulted); int faulted_before = -1; int faulted_after = -1; observe_named_generation(db_path, project, "BeforeIncrementalPersist", "AfterIncrementalPersist", &faulted_before, &faulted_after); int faulted_stage_count = count_generation_stage_artifacts(tmp, "generation.db"); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *retry = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(retry); int retry_rc = cbm_pipeline_run(retry); cbm_incremental_route_t retry_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(retry); int retry_before = -1; int retry_after = -1; observe_named_generation(db_path, project, "BeforeIncrementalPersist", "AfterIncrementalPersist", &retry_before, &retry_after); int retry_stage_count = count_generation_stage_artifacts(tmp, "generation.db"); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(faulted_rc, CBM_PIPELINE_PERSIST_FAILED); ASSERT_EQ(faulted_route, CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); ASSERT_EQ(faulted_before, 1); ASSERT_EQ(faulted_after, 0); ASSERT_EQ(faulted_stage_count, 0); ASSERT_EQ(retry_rc, 0); ASSERT_EQ(retry_route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(retry_before, 0); ASSERT_EQ(retry_after, 1); ASSERT_EQ(retry_stage_count, 0); PASS(); } TEST(pipeline_incremental_successful_publication_preserves_adr) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_publish_incr_adr_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "generation.py", "def BeforeAdrIncremental():\n return 1\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); static const char adr_text[] = "# Decision\nPreserve across publication."; cbm_store_t *adr_store = cbm_store_open_path(db_path); ASSERT_NOT_NULL(adr_store); ASSERT_EQ(cbm_store_adr_store(adr_store, project, adr_text), CBM_STORE_OK); cbm_store_close(adr_store); write_temp_file(tmp, "generation.py", "def AfterAdrIncremental():\n return 2\n# changed generation\n"); cbm_pipeline_incremental_test_force_legacy_partial_once(); cbm_pipeline_t *incremental = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(incremental); int run_rc = cbm_pipeline_run(incremental); cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(incremental); cbm_store_t *published = cbm_store_open_path(db_path); ASSERT_NOT_NULL(published); cbm_adr_t adr = {0}; int adr_rc = cbm_store_adr_get(published, project, &adr); bool adr_matches = adr_rc == CBM_STORE_OK && adr.content && strcmp(adr.content, adr_text) == 0; cbm_store_adr_free(&adr); cbm_store_close(published); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(run_rc, 0); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); ASSERT_TRUE(adr_matches); PASS(); } /* A forced-full rebuild must never erase an ADR merely because the old * generation could not be read completely. The capture is part of the * publication transaction: failure preserves both graph and ADR. */ TEST(pipeline_full_adr_capture_failure_preserves_previous_generation) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_publish_adr_capture_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "generation.py", "def BeforeAdrCapture():\n return 1\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); static const char adr_text[] = "# Decision\nADR capture is fail-closed."; cbm_store_t *adr_store = cbm_store_open_path(db_path); ASSERT_NOT_NULL(adr_store); ASSERT_EQ(cbm_store_adr_store(adr_store, project, adr_text), CBM_STORE_OK); cbm_store_close(adr_store); write_temp_file(tmp, "generation.py", "def AfterAdrCapture():\n return 2\n"); cbm_pipeline_incremental_test_fail_adr_capture_once(); cbm_pipeline_t *faulted = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(faulted); int faulted_rc = cbm_pipeline_run(faulted); cbm_pipeline_free(faulted); int faulted_before = -1; int faulted_after = -1; observe_named_generation(db_path, project, "BeforeAdrCapture", "AfterAdrCapture", &faulted_before, &faulted_after); cbm_store_t *preserved = cbm_store_open_path(db_path); ASSERT_NOT_NULL(preserved); cbm_adr_t preserved_adr = {0}; int preserved_adr_rc = cbm_store_adr_get(preserved, project, &preserved_adr); bool preserved_adr_matches = preserved_adr_rc == CBM_STORE_OK && preserved_adr.content && strcmp(preserved_adr.content, adr_text) == 0; cbm_store_adr_free(&preserved_adr); cbm_store_close(preserved); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *retry = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(retry); int retry_rc = cbm_pipeline_run(retry); cbm_pipeline_free(retry); int retry_before = -1; int retry_after = -1; observe_named_generation(db_path, project, "BeforeAdrCapture", "AfterAdrCapture", &retry_before, &retry_after); cbm_store_t *published = cbm_store_open_path(db_path); ASSERT_NOT_NULL(published); cbm_adr_t published_adr = {0}; int published_adr_rc = cbm_store_adr_get(published, project, &published_adr); bool published_adr_matches = published_adr_rc == CBM_STORE_OK && published_adr.content && strcmp(published_adr.content, adr_text) == 0; cbm_store_adr_free(&published_adr); cbm_store_close(published); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(faulted_rc, CBM_PIPELINE_ABORT_PRESERVE_DB); ASSERT_EQ(faulted_before, 1); ASSERT_EQ(faulted_after, 0); ASSERT_TRUE(preserved_adr_matches); ASSERT_EQ(retry_rc, 0); ASSERT_EQ(retry_before, 0); ASSERT_EQ(retry_after, 1); ASSERT_TRUE(published_adr_matches); PASS(); } /* An exact semantic manifest must fail closed when its repository root cannot * be traversed. A regular file is a deterministic cross-platform opendir * failure that does not depend on process permissions. */ TEST(pipeline_semantic_manifest_rejects_non_directory_root) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_manifest_not_dir_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "not-a-directory", "manifest root sentinel\n"); char root_path[512]; snprintf(root_path, sizeof(root_path), "%s/not-a-directory", tmp); cbm_file_hash_t *manifest = NULL; int manifest_count = -1; int rc = cbm_pipeline_build_semantic_manifest("manifest-fail-closed", root_path, NULL, 0, NULL, 0, NULL, NULL, &manifest, &manifest_count); cbm_pipeline_free_semantic_manifest(manifest, manifest_count > 0 ? manifest_count : 0); th_rmtree(tmp); ASSERT_TRUE(rc != 0); ASSERT_EQ(manifest_count, 0); PASS(); } /* A fully validated staged graph must be able to recover from a definitely * non-SQLite destination without deleting evidence or overwriting an earlier * quarantine. The replacement happens only after the corrupt bytes are moved. */ TEST(pipeline_full_reindex_quarantines_corrupt_destination_without_overwrite) { static const char corrupt_bytes[] = "not a sqlite database\n"; static const char previous_backup[] = "older quarantine\n"; char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_publish_corrupt_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "fresh.py", "def FreshAfterCorruption():\n return 1\n"); write_temp_file(tmp, "generation.db", corrupt_bytes); write_temp_file(tmp, "generation.db.corrupt", previous_backup); char db_path[512]; char old_backup_path[512]; char new_backup_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); snprintf(old_backup_path, sizeof(old_backup_path), "%s.corrupt", db_path); snprintf(new_backup_path, sizeof(new_backup_path), "%s.corrupt.1", db_path); cbm_pipeline_t *pipeline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(pipeline); int run_rc = cbm_pipeline_run(pipeline); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(pipeline)); cbm_pipeline_free(pipeline); char old_observed[64] = {0}; char new_observed[64] = {0}; FILE *old_backup_file = cbm_fopen(old_backup_path, "rb"); FILE *new_backup_file = cbm_fopen(new_backup_path, "rb"); size_t old_size = old_backup_file ? fread(old_observed, 1, sizeof(old_observed) - 1, old_backup_file) : 0; size_t new_size = new_backup_file ? fread(new_observed, 1, sizeof(new_observed) - 1, new_backup_file) : 0; if (old_backup_file) { fclose(old_backup_file); } if (new_backup_file) { fclose(new_backup_file); } int fresh_nodes = -1; cbm_store_t *store = cbm_store_open_path_query(db_path); if (store) { fresh_nodes = named_node_count(store, project, "FreshAfterCorruption"); cbm_store_close(store); } th_rmtree(tmp); ASSERT_EQ(run_rc, 0); ASSERT_EQ((int)old_size, (int)strlen(previous_backup)); ASSERT_STR_EQ(old_observed, previous_backup); ASSERT_EQ((int)new_size, (int)strlen(corrupt_bytes)); ASSERT_STR_EQ(new_observed, corrupt_bytes); ASSERT_EQ(fresh_nodes, 1); PASS(); } /* Readable legacy graphs are replaceable generations, not corruption. The * publication boundary must neither run current-schema migration against the * old edges table nor require an ADR table that did not exist yet. */ TEST(pipeline_full_reindex_replaces_legacy_schema_without_quarantine) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_publish_legacy_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "fresh.py", "def FreshAfterLegacy():\n return 1\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/generation.db", tmp); sqlite3 *legacy = NULL; ASSERT_EQ(sqlite3_open_v2(db_path, &legacy, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL), SQLITE_OK); const char *legacy_schema = "CREATE TABLE projects(name TEXT PRIMARY KEY,indexed_at TEXT NOT NULL,root_path TEXT NOT " "NULL);" "CREATE TABLE file_hashes(project TEXT NOT NULL,rel_path TEXT NOT NULL,sha256 TEXT NOT " "NULL,mtime_ns INTEGER NOT NULL DEFAULT 0,size INTEGER NOT NULL DEFAULT 0,PRIMARY KEY " "(project,rel_path));" "CREATE TABLE nodes(id INTEGER PRIMARY KEY,project TEXT NOT NULL,label TEXT NOT NULL,name " "TEXT NOT NULL,qualified_name TEXT NOT NULL,file_path TEXT,start_line INTEGER,end_line " "INTEGER,properties TEXT DEFAULT '{}');" "CREATE TABLE edges(id INTEGER PRIMARY KEY,project TEXT NOT NULL,source_id INTEGER NOT " "NULL,target_id INTEGER NOT NULL,type TEXT NOT NULL,properties TEXT DEFAULT '{}',UNIQUE " "(source_id,target_id,type));"; ASSERT_EQ(sqlite3_exec(legacy, legacy_schema, NULL, NULL, NULL), SQLITE_OK); sqlite3_stmt *insert_project = NULL; ASSERT_EQ(sqlite3_prepare_v2( legacy, "INSERT INTO projects(name,indexed_at,root_path) VALUES(?1,'legacy',?2);", -1, &insert_project, NULL), SQLITE_OK); const char *project_name = strrchr(tmp, '/'); project_name = project_name ? project_name + 1 : tmp; ASSERT_EQ(sqlite3_bind_text(insert_project, 1, project_name, -1, SQLITE_TRANSIENT), SQLITE_OK); ASSERT_EQ(sqlite3_bind_text(insert_project, 2, tmp, -1, SQLITE_TRANSIENT), SQLITE_OK); ASSERT_EQ(sqlite3_step(insert_project), SQLITE_DONE); sqlite3_finalize(insert_project); sqlite3_close(legacy); cbm_pipeline_t *pipeline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(pipeline); int run_rc = cbm_pipeline_run(pipeline); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(pipeline)); cbm_pipeline_free(pipeline); int fresh_nodes = -1; cbm_store_t *store = cbm_store_open_path_query(db_path); if (store) { fresh_nodes = named_node_count(store, project, "FreshAfterLegacy"); cbm_store_close(store); } int generated_column_count = 0; sqlite3 *current = NULL; sqlite3_stmt *column_query = NULL; if (sqlite3_open_v2(db_path, ¤t, SQLITE_OPEN_READONLY, NULL) == SQLITE_OK && sqlite3_prepare_v2(current, "SELECT count(*) FROM pragma_table_xinfo('edges') " "WHERE name='local_name_gen';", -1, &column_query, NULL) == SQLITE_OK && sqlite3_step(column_query) == SQLITE_ROW) { generated_column_count = sqlite3_column_int(column_query, 0); } sqlite3_finalize(column_query); if (current) { sqlite3_close(current); } char quarantine_path[512]; snprintf(quarantine_path, sizeof(quarantine_path), "%s.corrupt", db_path); cbm_path_info_t quarantine_info; bool quarantined = cbm_path_info_utf8(quarantine_path, &quarantine_info) == 0; th_rmtree(tmp); ASSERT_EQ(run_rc, 0); ASSERT_EQ(fresh_nodes, 1); ASSERT_EQ(generated_column_count, 1); ASSERT_FALSE(quarantined); PASS(); } #endif /* Re-indexing only a target whose qualified name survives a callable-to-value * edit must not restore the unchanged caller's stale inbound CALL_REFERENCE * snapshot. A fresh full index of the edited sources is the convergence oracle: * the now-non-callable identifier remains a USAGE, never a call relationship. */ TEST(pipeline_incremental_changed_target_invalidates_stale_inbound_call_reference) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_ref_stale_inbound_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } static const char callable_target_source[] = "export function staleSemanticTarget(): void {}\n"; static const char value_target_source[] = "export const staleSemanticTarget = 42;\n"; static const char caller_source[] = "import { staleSemanticTarget } from './target';\n" "function acceptStaleSemanticReference(callback: () => void): void {}\n" "export function staleSemanticCaller(): void {\n" " acceptStaleSemanticReference(staleSemanticTarget);\n" "}\n"; write_temp_file(tmp, "target.ts", callable_target_source); write_temp_file(tmp, "caller.ts", caller_source); char incremental_db[512]; snprintf(incremental_db, sizeof(incremental_db), "%s/stale-inbound-incremental.db", tmp); cbm_pipeline_t *baseline = cbm_pipeline_new(tmp, incremental_db, CBM_MODE_FULL); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); const char *baseline_project = cbm_pipeline_project_name(baseline); cbm_store_t *baseline_store = cbm_store_open_path(incremental_db); ASSERT_NOT_NULL(baseline_store); ASSERT_EQ(named_edge_count(baseline_store, baseline_project, "CALL_REFERENCE", "staleSemanticCaller", "staleSemanticTarget"), 1); ASSERT_EQ(named_edge_count(baseline_store, baseline_project, "USAGE", "staleSemanticCaller", "staleSemanticTarget"), 0); ASSERT_EQ(named_edge_count(baseline_store, baseline_project, "CALLS", "staleSemanticCaller", "staleSemanticTarget"), 0); cbm_store_close(baseline_store); cbm_pipeline_free(baseline); write_temp_file(tmp, "target.ts", value_target_source); cbm_pipeline_t *incremental = cbm_pipeline_new(tmp, incremental_db, CBM_MODE_FULL); ASSERT_NOT_NULL(incremental); ASSERT_EQ(cbm_pipeline_run(incremental), 0); const char *incremental_project = cbm_pipeline_project_name(incremental); cbm_store_t *incremental_store = cbm_store_open_path(incremental_db); ASSERT_NOT_NULL(incremental_store); int incremental_reference = named_edge_count(incremental_store, incremental_project, "CALL_REFERENCE", "staleSemanticCaller", "staleSemanticTarget"); int incremental_usage = named_edge_count(incremental_store, incremental_project, "USAGE", "staleSemanticCaller", "staleSemanticTarget"); int incremental_calls = named_edge_count(incremental_store, incremental_project, "CALLS", "staleSemanticCaller", "staleSemanticTarget"); cbm_store_close(incremental_store); cbm_pipeline_free(incremental); char full_db[512]; snprintf(full_db, sizeof(full_db), "%s/stale-inbound-full.db", tmp); cbm_pipeline_t *full = cbm_pipeline_new(tmp, full_db, CBM_MODE_FULL); ASSERT_NOT_NULL(full); ASSERT_EQ(cbm_pipeline_run(full), 0); const char *full_project = cbm_pipeline_project_name(full); cbm_store_t *full_store = cbm_store_open_path(full_db); ASSERT_NOT_NULL(full_store); int full_reference = named_edge_count(full_store, full_project, "CALL_REFERENCE", "staleSemanticCaller", "staleSemanticTarget"); int full_usage = named_edge_count(full_store, full_project, "USAGE", "staleSemanticCaller", "staleSemanticTarget"); int full_calls = named_edge_count(full_store, full_project, "CALLS", "staleSemanticCaller", "staleSemanticTarget"); cbm_store_close(full_store); cbm_pipeline_free(full); th_rmtree(tmp); ASSERT_EQ(full_reference, 0); ASSERT_EQ(full_usage, 1); ASSERT_EQ(full_calls, 0); ASSERT_EQ(incremental_reference, full_reference); ASSERT_EQ(incremental_usage, full_usage); ASSERT_EQ(incremental_calls, full_calls); PASS(); } /* Registry construction in incremental-parallel mode can materialize serial * resource nodes after extraction establishes the workers' shared-ID * watermark. A subsequent worker-created synthetic node must not reuse that * ID: duplicate temporary IDs make dump-time edge remapping point CONFIGURES * or DECORATES at the wrong target. The second run changes all 51 files so the * production incremental path crosses MIN_FILES_FOR_PARALLEL_INCR. */ TEST(pipeline_incremental_parallel_registry_nodes_advance_shared_ids) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_incr_ids_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } char collision_path[512]; snprintf(collision_path, sizeof(collision_path), "%s/00_collision.py", tmp); int setup_ok = th_write_file(collision_path, "import os\n" "def incrementalCollisionTarget(): return 1\n" "def incrementalCollisionAccept(callback): pass\n" "@old_marker\n" "def incrementalCollisionCarrier():\n" " incrementalCollisionTarget()\n" " incrementalCollisionAccept(incrementalCollisionTarget)\n" " return os.getenv(\"OLD_TOKEN\")\n") == 0; for (int i = 0; setup_ok && i < 50; i++) { char path[512]; char source[128]; snprintf(path, sizeof(path), "%s/z_pad_%02d.ts", tmp, i); snprintf(source, sizeof(source), "// initial incremental ID pad %02d\n", i); setup_ok = th_write_file(path, source) == 0; } char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); cbm_setenv("CBM_WORKERS", "4", 1); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/incremental-ids.db", tmp); int first_rc = -1; int second_rc = -1; int env_edge_count = -1; int decorator_edge_count = -1; int misdirected_env_edge_count = -1; int reference_edge_count = -1; int env_node_count = -1; int decorator_node_count = -1; if (setup_ok) { cbm_pipeline_t *first = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); if (first) { first_rc = cbm_pipeline_run(first); cbm_pipeline_free(first); } } int update_ok = setup_ok && first_rc == 0 && th_write_file(collision_path, "import os\n" "def incrementalCollisionTarget(): return 1\n" "def incrementalCollisionAccept(callback): pass\n" "@new_marker\n" "def incrementalCollisionCarrier():\n" " incrementalCollisionTarget()\n" " incrementalCollisionAccept(incrementalCollisionTarget)\n" " return os.getenv(\"NEW_TOKEN\")\n" "# incremental size marker\n") == 0; for (int i = 0; update_ok && i < 50; i++) { char path[512]; char source[160]; snprintf(path, sizeof(path), "%s/z_pad_%02d.ts", tmp, i); snprintf(source, sizeof(source), "// changed incremental ID pad %02d with marker\n", i); update_ok = th_write_file(path, source) == 0; } if (update_ok) { cbm_pipeline_t *second = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); if (second) { second_rc = cbm_pipeline_run(second); const char *project = cbm_pipeline_project_name(second); cbm_store_t *store = cbm_store_open_path(db_path); if (store && project) { env_edge_count = named_edge_count(store, project, "CONFIGURES", "incrementalCollisionCarrier", "NEW_TOKEN"); decorator_edge_count = named_edge_count( store, project, "DECORATES", "incrementalCollisionCarrier", "new_marker"); misdirected_env_edge_count = named_edge_count( store, project, "CONFIGURES", "incrementalCollisionCarrier", "new_marker"); reference_edge_count = named_edge_count(store, project, "CALL_REFERENCE", "incrementalCollisionCarrier", "incrementalCollisionTarget"); env_node_count = named_node_count(store, project, "NEW_TOKEN"); decorator_node_count = named_node_count(store, project, "new_marker"); } if (store) { cbm_store_close(store); } cbm_pipeline_free(second); } } if (saved_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); free(saved_workers); } else { cbm_unsetenv("CBM_WORKERS"); } if (saved_single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); free(saved_single); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } th_rmtree(tmp); if (env_node_count != 1 || decorator_node_count != 1 || env_edge_count != 1 || decorator_edge_count != 1 || misdirected_env_edge_count != 0 || reference_edge_count != 1) { printf(" incremental ID diagnostic: env_node=%d decorator_node=%d " "configures_env=%d decorates=%d configures_decorator=%d call_reference=%d\n", env_node_count, decorator_node_count, env_edge_count, decorator_edge_count, misdirected_env_edge_count, reference_edge_count); } ASSERT_TRUE(setup_ok); ASSERT_EQ(first_rc, 0); ASSERT_TRUE(update_ok); ASSERT_EQ(second_rc, 0); ASSERT_EQ(env_node_count, 1); ASSERT_EQ(decorator_node_count, 1); ASSERT_EQ(env_edge_count, 1); ASSERT_EQ(decorator_edge_count, 1); ASSERT_EQ(misdirected_env_edge_count, 0); ASSERT_EQ(reference_edge_count, 1); PASS(); } #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API static void observe_incremental_generation(const char *db_path, const char *project, int generation_size, int *before_count, int *after_count) { *before_count = -1; *after_count = -1; cbm_store_t *store = cbm_store_open_path(db_path); if (!store || !project) { if (store) { cbm_store_close(store); } return; } int before = 0; int after = 0; for (int i = 0; i < generation_size; i++) { char before_name[32]; char after_name[32]; snprintf(before_name, sizeof(before_name), "Before%02d", i); snprintf(after_name, sizeof(after_name), "After%02d", i); int before_nodes = named_node_count(store, project, before_name); int after_nodes = named_node_count(store, project, after_name); if (before_nodes < 0 || after_nodes < 0) { cbm_store_close(store); return; } before += before_nodes; after += after_nodes; } cbm_store_close(store); *before_count = before; *after_count = after; } /* A parallel incremental run must preserve its prior database when the * per-file result cache cannot be allocated. A clean retry must then converge * to the new generation. * * RED before fail-closed propagation: the injected allocation failure skips * extract/resolve, but the caller dumps the already-purged graph, advances all * file hashes, and returns 0. The second graph is empty and the third run is a * no-op instead of recovering the missing definitions. */ TEST(pipeline_incremental_parallel_result_cache_alloc_failure_preserves_db_and_retries) { enum { GENERATION_SIZE = 51 }; char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_incr_cache_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } cbm_pipeline_incremental_test_reset_faults(); char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); cbm_setenv("CBM_WORKERS", "4", 1); int setup_ok = 1; for (int i = 0; setup_ok && i < GENERATION_SIZE; i++) { char path[512]; char source[128]; snprintf(path, sizeof(path), "%s/generation_%02d.py", tmp, i); snprintf(source, sizeof(source), "def Before%02d():\n return %d\n", i, i); setup_ok = th_write_file(path, source) == 0; } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/incremental-cache.db", tmp); int first_rc = -1; int baseline_before = -1; int baseline_after = -1; if (setup_ok) { cbm_pipeline_t *first = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); if (first) { first_rc = cbm_pipeline_run(first); observe_incremental_generation(db_path, cbm_pipeline_project_name(first), GENERATION_SIZE, &baseline_before, &baseline_after); cbm_pipeline_free(first); } } int edit_ok = setup_ok && first_rc == 0; for (int i = 0; edit_ok && i < GENERATION_SIZE; i++) { char path[512]; char source[160]; snprintf(path, sizeof(path), "%s/generation_%02d.py", tmp, i); snprintf(source, sizeof(source), "def After%02d():\n return %d\n# edited generation %02d\n", i, i, i); edit_ok = th_write_file(path, source) == 0; } int second_rc = -1; int second_before = -1; int second_after = -1; if (edit_ok) { cbm_pipeline_incremental_test_fail_result_cache_alloc_once(); cbm_pipeline_incremental_test_force_legacy_partial_once(); cbm_pipeline_t *second = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); if (second) { second_rc = cbm_pipeline_run(second); observe_incremental_generation(db_path, cbm_pipeline_project_name(second), GENERATION_SIZE, &second_before, &second_after); cbm_pipeline_free(second); } } cbm_pipeline_incremental_test_reset_faults(); int third_rc = -1; int retry_before = -1; int retry_after = -1; if (edit_ok) { cbm_pipeline_t *third = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); if (third) { third_rc = cbm_pipeline_run(third); observe_incremental_generation(db_path, cbm_pipeline_project_name(third), GENERATION_SIZE, &retry_before, &retry_after); cbm_pipeline_free(third); } } cbm_pipeline_incremental_test_reset_faults(); if (saved_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); free(saved_workers); } else { cbm_unsetenv("CBM_WORKERS"); } if (saved_single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); free(saved_single); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } th_rmtree(tmp); bool second_is_preserved_failure = second_rc == CBM_PIPELINE_ABORT_PRESERVE_DB && second_before == GENERATION_SIZE && second_after == 0; if (!second_is_preserved_failure) { printf(" incremental cache failure diagnostic: rc=%d before=%d after=%d\n", second_rc, second_before, second_after); } ASSERT_TRUE(setup_ok); ASSERT_EQ(first_rc, 0); ASSERT_EQ(baseline_before, GENERATION_SIZE); ASSERT_EQ(baseline_after, 0); ASSERT_TRUE(edit_ok); ASSERT_TRUE(second_is_preserved_failure); ASSERT_EQ(third_rc, 0); ASSERT_EQ(retry_before, 0); ASSERT_EQ(retry_after, GENERATION_SIZE); PASS(); } #endif /* #1916: one index of the axios-wrapper fixture, reduced to the edge counts * the test asserts. */ typedef struct { int run_rc; int wrapper_http; /* loadWrapperOrders -HTTP_CALLS-> /orders */ int wrapper_http_v3; /* same call after the wrapper's base moves to /v3 */ int wrapper_http_v3_inv; /* ... and then the caller's path moves to /invoices */ int wrapper_raw_http; /* loadWrapperOrders -HTTP_CALLS-> /orders (uncomposed) */ int wrapper_route_reg; /* loadWrapperOrders -CALLS-> /orders (phantom route reg) */ int named_http; /* named import, base with trailing '/' */ int local_http; /* same-file instance spelled axiosInstance */ int local_raw_http; /* ... uncomposed (the #523 spelling match) */ int dynamic_http; /* non-literal baseURL: path kept, never guessed */ int dynamic_route_reg; /* ... misread as a route registration */ int direct_http; /* control: axios.get('/direct') */ int fake_http; /* look-alike factory.create: never a client */ int provider_handles; /* control: Express app.get stays a registration */ } AxiosWrapperObs; static AxiosWrapperObs observe_axios_wrapper(const char *repo, const char *db) { AxiosWrapperObs o; memset(&o, 0xff, sizeof(o)); /* -1 everywhere: "not observed" */ cbm_pipeline_t *p = cbm_pipeline_new(repo, db, CBM_MODE_FULL); if (!p) { return o; } o.run_rc = cbm_pipeline_run(p); const char *proj = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db); if (s && proj) { o.wrapper_http = named_edge_count(s, proj, "HTTP_CALLS", "loadWrapperOrders", "/api/orders"); o.wrapper_http_v3 = named_edge_count(s, proj, "HTTP_CALLS", "loadWrapperOrders", "/v3/orders"); o.wrapper_http_v3_inv = named_edge_count(s, proj, "HTTP_CALLS", "loadWrapperOrders", "/v3/invoices"); o.wrapper_raw_http = named_edge_count(s, proj, "HTTP_CALLS", "loadWrapperOrders", "/orders"); o.wrapper_route_reg = named_edge_count(s, proj, "CALLS", "loadWrapperOrders", "/orders"); o.named_http = named_edge_count(s, proj, "HTTP_CALLS", "loadNamedItems", "/v2/items"); o.local_http = named_edge_count(s, proj, "HTTP_CALLS", "loadLocal", "/local/x"); o.local_raw_http = named_edge_count(s, proj, "HTTP_CALLS", "loadLocal", "/x"); o.dynamic_http = named_edge_count(s, proj, "HTTP_CALLS", "loadDynamic", "/dynamic"); o.dynamic_route_reg = named_edge_count(s, proj, "CALLS", "loadDynamic", "/dynamic"); o.direct_http = named_edge_count(s, proj, "HTTP_CALLS", "loadDirectOrders", "/direct"); o.fake_http = named_edge_count(s, proj, "HTTP_CALLS", "loadFake", "/api/fake"); o.provider_handles = named_edge_count(s, proj, "HANDLES", "provideOrders", "/api/orders"); } if (s) { cbm_store_close(s); } cbm_pipeline_free(p); return o; } /* #1916: `const api = axios.create({ baseURL: '/api' }); export default api` * in one module and `api.get('/orders')` in another is THE Vue-admin wrapper * pattern. The call must become HTTP_CALLS to GET /api/orders — before the * fix the `.get` suffix made it an Express route REGISTRATION (a CALLS edge * to a phantom server route /orders) and cross-repo matching found nothing. * 52 fillers put the default run on the parallel resolver; the * single-thread run is the sequential oracle; a same-DB reindex after the * wrapper's base changes must converge with the fresh index. */ TEST(pipeline_axios_wrapper_baseurl_composes_http_calls_issue1916) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_axios_wrapper_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "src/client.ts", "import axios from 'axios';\n" "const api = axios.create({ baseURL: '/api', timeout: 5000 });\n" "export default api;\n"); write_temp_file( tmp, "src/orders.ts", "import api from './client';\n" "import axios from 'axios';\n" "export function loadWrapperOrders(): unknown { return api.get('/orders'); }\n" "export function loadDirectOrders(): unknown { return axios.get('/direct'); }\n"); write_temp_file(tmp, "src/named-client.ts", "import axios from 'axios';\n" "export const http = axios.create({ baseURL: '/v2/' });\n"); write_temp_file(tmp, "src/named-caller.ts", "import { http } from './named-client';\n" "export function loadNamedItems(): unknown { return http.post('/items'); }\n"); write_temp_file(tmp, "src/local.ts", "import axios from 'axios';\n" "const axiosInstance = axios.create({ baseURL: '/local' });\n" "export function loadLocal(): unknown { return axiosInstance.get('/x'); }\n"); write_temp_file(tmp, "src/dynamic-client.ts", "import axios from 'axios';\n" "const dynamicApi = axios.create({ baseURL: process.env.API_BASE });\n" "export default dynamicApi;\n"); write_temp_file( tmp, "src/dynamic-caller.ts", "import dynamicApi from './dynamic-client';\n" "export function loadDynamic(): unknown { return dynamicApi.get('/dynamic'); }\n"); write_temp_file(tmp, "src/fake-client.ts", "const factory = { create: (config: unknown) => config };\n" "const fakeApi = factory.create({ baseURL: '/api' });\n" "export default fakeApi;\n"); write_temp_file(tmp, "src/fake-caller.ts", "import fakeApi from './fake-client';\n" "export function loadFake(): unknown { return fakeApi.get('/fake'); }\n"); write_temp_file(tmp, "src/provider.ts", "import express from 'express';\n" "const app = express();\n" "function provideOrders(): void {}\n" "app.get('/api/orders', provideOrders);\n"); for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/wrapper_pad_%02d.ts", i); snprintf(body, sizeof(body), "export function wrapperPad%02d(): number { return %d; }\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; char db_seq[512]; char db_par[512]; char db_fresh[512]; char db_fresh2[512]; snprintf(db_seq, sizeof(db_seq), "%s/seq.db", tmp); snprintf(db_par, sizeof(db_par), "%s/par.db", tmp); snprintf(db_fresh, sizeof(db_fresh), "%s/fresh.db", tmp); snprintf(db_fresh2, sizeof(db_fresh2), "%s/fresh2.db", tmp); cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1); AxiosWrapperObs seq = observe_axios_wrapper(tmp, db_seq); cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); cbm_setenv("CBM_WORKERS", "4", 1); AxiosWrapperObs par = observe_axios_wrapper(tmp, db_par); /* Only the wrapper changes; the importing caller is untouched. */ write_temp_file(tmp, "src/client.ts", "import axios from 'axios';\n" "const api = axios.create({ baseURL: '/v3' });\n" "export default api;\n"); AxiosWrapperObs incr = observe_axios_wrapper(tmp, db_par); AxiosWrapperObs fresh = observe_axios_wrapper(tmp, db_fresh); /* Then only the caller changes; the (unchanged) wrapper's base must * still be found when the caller alone is re-resolved. */ write_temp_file( tmp, "src/orders.ts", "import api from './client';\n" "import axios from 'axios';\n" "export function loadWrapperOrders(): unknown { return api.get('/invoices'); }\n" "export function loadDirectOrders(): unknown { return axios.get('/direct'); }\n"); AxiosWrapperObs incr2 = observe_axios_wrapper(tmp, db_par); AxiosWrapperObs fresh2 = observe_axios_wrapper(tmp, db_fresh2); if (saved_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); free(saved_workers); } else { cbm_unsetenv("CBM_WORKERS"); } if (saved_single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); free(saved_single); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } th_rmtree(tmp); ASSERT_EQ(seq.run_rc, 0); ASSERT_EQ(par.run_rc, 0); /* Controls: direct axios and an Express registration are unchanged, and * a look-alike factory never becomes a client. */ ASSERT_EQ(seq.direct_http, 1); ASSERT_EQ(seq.provider_handles, 1); ASSERT_EQ(seq.fake_http, 0); /* The issue: default-imported wrapper composes base + path. */ ASSERT_EQ(seq.wrapper_http, 1); ASSERT_EQ(seq.wrapper_raw_http, 0); ASSERT_EQ(seq.wrapper_route_reg, 0); ASSERT_EQ(seq.named_http, 1); ASSERT_EQ(seq.local_http, 1); ASSERT_EQ(seq.local_raw_http, 0); ASSERT_EQ(seq.dynamic_http, 1); ASSERT_EQ(seq.dynamic_route_reg, 0); /* Sequential and parallel resolvers agree edge for edge. */ ASSERT_MEM_EQ(&seq, &par, sizeof(seq)); /* Reindex after the base moved converges with a fresh index. */ ASSERT_EQ(fresh.run_rc, 0); ASSERT_EQ(fresh.wrapper_http_v3, 1); ASSERT_EQ(fresh.wrapper_http, 0); ASSERT_EQ(incr.run_rc, 0); ASSERT_MEM_EQ(&incr, &fresh, sizeof(fresh)); ASSERT_EQ(fresh2.run_rc, 0); ASSERT_EQ(fresh2.wrapper_http_v3_inv, 1); ASSERT_EQ(fresh2.wrapper_http_v3, 0); ASSERT_MEM_EQ(&incr2, &fresh2, sizeof(fresh2)); PASS(); } /* Issue #1354: a method call on an instance built with `new ImportedClass()` * produced no CALLS edge (only the constructor edge), while the identical * shape in the class's own file resolved via lsp_ts_method. End to end: * both cross-file shapes must now carry the type-aware lsp_ts_method edge, * and the same-file control must keep it. */ TEST(pipeline_ts_crossfile_new_instance_method_call_issue1354) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_ts_1354_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } /* write_temp_file creates one directory level only. */ write_temp_file(tmp, "lib/toast.service.ts", "export class ToastService {\n" " openSuccessUniqueXyz(msg: string): void {\n" " console.log(msg);\n" " }\n" "}\n" "\n" "export function sameFileControl(): void {\n" " const t = new ToastService();\n" " t.openSuccessUniqueXyz('same-file control');\n" "}\n"); write_temp_file(tmp, "app/variants.ts", "import { ToastService } from '../lib/toast.service';\n" "\n" "export function crossNewLocal(): void {\n" " const t = new ToastService();\n" " t.openSuccessUniqueXyz('cross-file');\n" "}\n" "\n" "export function crossNewChain(): void {\n" " new ToastService().openSuccessUniqueXyz('chained');\n" "}\n"); /* File named after its class (`NotifierService.ts` exports * `NotifierService`): the imported module QN already ends in the class * name, which must not be mistaken for the class QN. */ write_temp_file(tmp, "lib/NotifierService.ts", "export class NotifierService {\n" " notifyUniqueAbc(): void {}\n" "}\n"); write_temp_file(tmp, "app/notify.ts", "import { NotifierService } from '../lib/NotifierService';\n" "\n" "export function crossSameNameNew(): void {\n" " const n = new NotifierService();\n" " n.notifyUniqueAbc();\n" "}\n" "\n" "export function crossSameNameTyped(n: NotifierService): void {\n" " n.notifyUniqueAbc();\n" "}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/ts_1354.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); bool same_file = cross_file_call_has_strategy(s, project, "sameFileControl", "openSuccessUniqueXyz", "lsp_ts_method"); bool cross_local = cross_file_call_has_strategy(s, project, "crossNewLocal", "openSuccessUniqueXyz", "lsp_ts_method"); bool cross_chain = cross_file_call_has_strategy(s, project, "crossNewChain", "openSuccessUniqueXyz", "lsp_ts_method"); bool same_name_new = cross_file_call_has_strategy(s, project, "crossSameNameNew", "notifyUniqueAbc", "lsp_ts_method"); bool same_name_typed = cross_file_call_has_strategy(s, project, "crossSameNameTyped", "notifyUniqueAbc", "lsp_ts_method"); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); ASSERT_TRUE(same_file); ASSERT_TRUE(cross_local); ASSERT_TRUE(cross_chain); ASSERT_TRUE(same_name_new); ASSERT_TRUE(same_name_typed); PASS(); } /* A query failure is distinct from a valid zero-edge result. */ static int ts514_pipeline_count(cbm_store_t *store, const char *sql, const char **values, int value_count) { sqlite3_stmt *stmt = NULL; sqlite3 *db = cbm_store_get_db(store); if (!db || sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { sqlite3_finalize(stmt); return -1; } bool bound = sqlite3_bind_parameter_count(stmt) == value_count; for (int i = 0; bound && i < value_count; i++) bound = values[i] && sqlite3_bind_text(stmt, i + 1, values[i], -1, SQLITE_TRANSIENT) == SQLITE_OK; int count = -1; if (bound && sqlite3_step(stmt) == SQLITE_ROW) { count = sqlite3_column_int(stmt, 0); if (sqlite3_step(stmt) != SQLITE_DONE) count = -1; } if (sqlite3_finalize(stmt) != SQLITE_OK) count = -1; return count; } /* #514: constructor injection and new-initialized fields resolve through both * drivers. Same-named methods make wrong-owner and self-loop claims meaningful. * Every negative has a real extracted call, source and target, plus successful * queries and typed positive controls in the same run. */ TEST(pipeline_ts_param_property_injection_issue514) { static const char *paths[] = {"cats/service.ts", "cats/controller.ts", "cats/reporter.ts"}; static const char *sources[] = { "export class BaseService { findAll(): string[] { return []; } }\n" "export class CatService extends BaseService { findAll(): string[] { return ['cat']; } }\n", "import { CatService } from './service';\n" "export class CatController {\n" " constructor(private readonly catService: CatService, plain: CatService) {}\n" " findAll() { return this.catService.findAll(); }\n" " direct(control: CatService) { return control.findAll(); }\n" " viaPlain() { return this.plain.findAll(); }\n" "}\n", "import { CatService, BaseService } from './service';\n" "function makeService(): CatService { return new CatService(); }\n" "export class CatReporter {\n" " private fresh = new CatService();\n" " private annotated: BaseService = new CatService();\n" " private factory = makeService();\n" " constructor(protected cats: CatService) {}\n" " reportInjected() { return this.cats.findAll(); }\n" " reportLocal() { return this.fresh.findAll(); }\n" " reportAnnotated() { return this.annotated.findAll(); }\n" " reportFactory() { return this.factory.findAll(); }\n" "}\n", }; static const struct { int file; const char *symbol; const char *expression; const char *callee; const char *target; int want; } cases[] = { {1, "CatController.direct", "control.findAll()", "control.findAll", "CatService.findAll", 1}, {1, "CatController.findAll", "this.catService.findAll()", "this.catService.findAll", "CatService.findAll", 1}, {2, "CatReporter.reportInjected", "this.cats.findAll()", "this.cats.findAll", "CatService.findAll", 1}, {2, "CatReporter.reportLocal", "this.fresh.findAll()", "this.fresh.findAll", "CatService.findAll", 1}, {2, "CatReporter.reportAnnotated", "this.annotated.findAll()", "this.annotated.findAll", "BaseService.findAll", 1}, {1, "CatController.viaPlain", "this.plain.findAll()", "this.plain.findAll", "CatService.findAll", 0}, {2, "CatReporter.reportFactory", "this.factory.findAll()", "this.factory.findAll", "CatService.findAll", 0}, }; enum { NCASES = (int)(sizeof(cases) / sizeof(cases[0])) }; static const char node_sql[] = "SELECT count(*) FROM nodes WHERE project=?1 AND " "qualified_name=?2 AND file_path=?3 AND label='Method'"; static const char edge_sql[] = "SELECT count(*) FROM edges e JOIN nodes s ON s.id=e.source_id JOIN nodes t ON " "t.id=e.target_id " "WHERE e.project=?1 AND e.type='CALLS' AND s.qualified_name=?2 AND s.file_path=?3 " "AND t.qualified_name=?4 AND t.file_path=?5 " "AND (?6='' OR json_extract(e.properties,'$.strategy')=?6)"; CBMFileResult *fr[3] = {0}; bool extraction_ok = true; for (int i = 0; i < 3; i++) { fr[i] = cbm_extract_file(sources[i], (int)strlen(sources[i]), CBM_LANG_TYPESCRIPT, "ts514_probe", paths[i], 0, NULL, NULL); extraction_ok = extraction_ok && fr[i] && !fr[i]->has_error; } int occurrences[NCASES] = {0}; if (extraction_ok) { for (int i = 0; i < NCASES; i++) { int file = cases[i].file; const char *site = strstr(sources[file], cases[i].expression); char *caller = cbm_pipeline_fqn_compute("ts514_probe", paths[file], cases[i].symbol); for (int c = 0; site && caller && c < fr[file]->calls.count; c++) { const CBMCall *call = &fr[file]->calls.items[c]; uint32_t start = (uint32_t)(site - sources[file]); if (call->enclosing_func_qn && strcmp(call->enclosing_func_qn, caller) == 0 && call->callee_name && strcmp(call->callee_name, cases[i].callee) == 0 && call->site_start_byte == start && call->site_end_byte == start + strlen(cases[i].expression) && call->start_line > 0) occurrences[i]++; } free(caller); } } for (int i = 0; i < 3; i++) cbm_free_result(fr[i]); const char *old_workers = getenv("CBM_WORKERS"); const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; char *saved_single = old_single ? strdup(old_single) : NULL; if ((old_workers && !saved_workers) || (old_single && !saved_single)) { free(saved_workers); free(saved_single); FAIL("save environment"); } char tmp[256] = "/tmp/cbm_ts_514_XXXXXX"; if (!cbm_mkdtemp(tmp)) { free(saved_workers); free(saved_single); FAIL("tmpdir"); } for (int i = 0; i < 3; i++) write_temp_file(tmp, paths[i], sources[i]); for (int i = 0; i < 52; i++) { char name[64], body[128]; snprintf(name, sizeof(name), "cats/filler%d.ts", i); snprintf(body, sizeof(body), "export function filler%d(): number { return %d; }\n", i, i); write_temp_file(tmp, name, body); } int run_rc[2] = {-1, -1}, files[2] = {-1, -1}; int edges[2][NCASES], source_nodes[2][NCASES], target_nodes[2][NCASES]; int unresolved_calls[2][2] = {{-1, -1}, {-1, -1}}; int self_loop[2] = {-1, -1}, annotation_wrong[2] = {-1, -1}; bool configured[2] = {false, false}, opened[2] = {false, false}; for (int mode = 0; mode < 2; mode++) { for (int i = 0; i < NCASES; i++) edges[mode][i] = source_nodes[mode][i] = target_nodes[mode][i] = -1; cbm_setenv("CBM_WORKERS", "4", 1); if (mode == 0) cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1); else cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); const char *workers = getenv("CBM_WORKERS"); const char *single = getenv("CBM_INDEX_SINGLE_THREAD"); configured[mode] = workers && strcmp(workers, "4") == 0 && (mode == 0 ? single && strcmp(single, "1") == 0 : !single); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/ts514_%d.db", tmp, mode); cbm_pipeline_t *pipeline = configured[mode] ? cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL) : NULL; run_rc[mode] = pipeline ? cbm_pipeline_run(pipeline) : -1; cbm_store_t *store = run_rc[mode] == 0 ? cbm_store_open_path(db_path) : NULL; opened[mode] = store != NULL; if (store) { const char *project = cbm_pipeline_project_name(pipeline); const char *file_values[] = {project}; files[mode] = ts514_pipeline_count( store, "SELECT count(*) FROM nodes WHERE project=?1 AND label='File'", file_values, 1); for (int i = 0; i < NCASES; i++) { char *src = cbm_pipeline_fqn_compute(project, paths[cases[i].file], cases[i].symbol); char *tgt = cbm_pipeline_fqn_compute(project, paths[0], cases[i].target); const char *src_values[] = {project, src, paths[cases[i].file]}; const char *tgt_values[] = {project, tgt, paths[0]}; source_nodes[mode][i] = ts514_pipeline_count(store, node_sql, src_values, 3); target_nodes[mode][i] = ts514_pipeline_count(store, node_sql, tgt_values, 3); const char *edge_values[] = { project, src, paths[cases[i].file], tgt, paths[0], cases[i].want ? "lsp_ts_method" : ""}; edges[mode][i] = ts514_pipeline_count(store, edge_sql, edge_values, 6); if (!cases[i].want) unresolved_calls[mode][i - 5] = ts514_pipeline_count( store, "SELECT count(*) FROM edges e JOIN nodes s ON s.id=e.source_id " "WHERE e.project=?1 AND e.type='CALLS' " "AND s.qualified_name=?2 AND s.file_path=?3", src_values, 3); if (i == 1) { edge_values[3] = src; edge_values[4] = paths[cases[i].file]; edge_values[5] = ""; self_loop[mode] = ts514_pipeline_count(store, edge_sql, edge_values, 6); } if (i == 4) { char *wrong = cbm_pipeline_fqn_compute(project, paths[0], "CatService.findAll"); edge_values[3] = wrong; edge_values[5] = ""; annotation_wrong[mode] = ts514_pipeline_count(store, edge_sql, edge_values, 6); free(wrong); } free(src); free(tgt); } cbm_store_close(store); } cbm_pipeline_free(pipeline); } if (saved_workers) cbm_setenv("CBM_WORKERS", saved_workers, 1); else cbm_unsetenv("CBM_WORKERS"); if (saved_single) cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); else cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); const char *restored_workers = getenv("CBM_WORKERS"); const char *restored_single = getenv("CBM_INDEX_SINGLE_THREAD"); bool restored = (saved_workers ? restored_workers && strcmp(saved_workers, restored_workers) == 0 : !restored_workers) && (saved_single ? restored_single && strcmp(saved_single, restored_single) == 0 : !restored_single); free(saved_workers); free(saved_single); th_rmtree(tmp); ASSERT_TRUE(extraction_ok); ASSERT_TRUE(restored); for (int i = 0; i < NCASES; i++) ASSERT_EQ(occurrences[i], 1); for (int mode = 0; mode < 2; mode++) { ASSERT_TRUE(configured[mode]); ASSERT_EQ(run_rc[mode], 0); ASSERT_TRUE(opened[mode]); ASSERT_GTE(files[mode], 55); for (int i = 0; i < NCASES; i++) { ASSERT_EQ(source_nodes[mode][i], 1); ASSERT_EQ(target_nodes[mode][i], 1); ASSERT_EQ(edges[mode][i], cases[i].want); } ASSERT_EQ(unresolved_calls[mode][0], 0); ASSERT_EQ(unresolved_calls[mode][1], 0); ASSERT_EQ(self_loop[mode], 0); ASSERT_EQ(annotation_wrong[mode], 0); } PASS(); } TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_tsjs_recv_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } /* The lone project symbol named "test" — a real method. */ write_temp_file(tmp, "src/client.ts", "export class SalesforceRestClient {\n" " test(): boolean {\n" " return true;\n" " }\n" "}\n"); /* Regex receiver: `re.test(s)` calls RegExp.prototype.test, NOT the method. * The TS-LSP cannot bind it to a project symbol → the registry would guess * "test" by short name (weak). This is the false edge to suppress. */ write_temp_file(tmp, "src/caller.ts", "const re = /^[a-z]+$/;\n" "export function checkFormat(s: string): boolean {\n" " return re.test(s);\n" "}\n"); /* Typed receiver: `c` is annotated SalesforceRestClient, so the TS-LSP * resolves c.test() to the method (lsp_ts_method, conf 0.95) BEFORE the * registry runs — the guard's explicit drop-list keeps every lsp_* edge. */ write_temp_file(tmp, "src/typed.ts", "import { SalesforceRestClient } from './client';\n" "export function runTyped(c: SalesforceRestClient): boolean {\n" " return c.test();\n" "}\n"); /* Bare local call: same-module resolution, unaffected by the guard. */ write_temp_file(tmp, "src/local.ts", "function localHelper(): number {\n" " return 1;\n" "}\n" "export function callsLocal(): number {\n" " return localHelper();\n" "}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/tsjs_recv.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* (1) The false edge is suppressed (reproduce-first: RED before the fix). */ ASSERT_GTE(fixture_node_count(s, project, "src/caller.ts", "checkFormat", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "src/client.ts", "test", "Method"), 1); ASSERT_FALSE(cross_file_call_exists(s, project, "checkFormat", "test")); /* (2) The type-resolved receiver call survives (LSP wins before the guard). */ ASSERT_TRUE(cross_file_call_exists(s, project, "runTyped", "test")); /* (3) The bare local call survives (same-module / lsp_ts_local). */ ASSERT_TRUE(cross_file_call_exists(s, project, "callsLocal", "localHelper")); /* (4) breadth insurance: the real edges are still emitted. */ ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "CALLS"), 2); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* Python counterpart to the TS/JS receiver guard (#1276), sequential path. * Pins BOTH directions. NEGATIVE: an attribute call on an unknown receiver * (a parameter) must not bind the lone same-named project method through * unique_name. POSITIVE: an import-bound receiver (helper.compute) and a bare * local call must still produce their CALLS edges. * Fewer than 50 files exercises pass_calls.c. */ TEST(pipeline_python_receiver_suppresses_weak_method_edge) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_python_recv_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "worker.py", "class Worker:\n" " def backward(self):\n" " return 1\n"); write_temp_file(tmp, "pkg/__init__.py", "from .helper import compute\n"); write_temp_file(tmp, "pkg/helper.py", "def compute(value):\n" " return value * 2\n"); write_temp_file(tmp, "caller.py", "from pkg import helper\n" "\n" "def external_call(accelerator):\n" " return accelerator.backward()\n" "\n" "def imported_call():\n" " return helper.compute(42)\n" "\n" "def local_helper():\n" " return 1\n" "\n" "def bare_call():\n" " return local_helper()\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/python_recv.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* NEGATIVE: `accelerator` is a parameter — Worker.backward must not bind. */ ASSERT_GTE(fixture_node_count(s, project, "caller.py", "external_call", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "worker.py", "backward", "Method"), 1); ASSERT_FALSE(cross_file_call_exists(s, project, "external_call", "backward")); /* POSITIVE: import-bound receiver and bare local call both survive. */ ASSERT_TRUE(cross_file_call_exists(s, project, "imported_call", "compute")); ASSERT_TRUE(cross_file_call_exists(s, project, "bare_call", "local_helper")); /* Tripwire: a run that emitted no edges at all would satisfy the negative * assertion vacuously. */ ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "CALLS"), 1); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* #606: a receiver typed as an unindexed (vendor/) class must not weakly bind a * same-named project method. */ static void write_php_receiver_fixture(const char *tmp) { write_temp_file(tmp, "src/Item.php", "getUid();\n" " }\n" "\n" " public function selfCall(): int\n" " {\n" " return $this->localHelper();\n" " }\n" "\n" " private function localHelper(): int\n" " {\n" " return 1;\n" " }\n" "}\n"); write_temp_file(tmp, "src/ChainListener.php", "findFile()->getUid();\n" " }\n" "\n" " public function projectCall(Repository $repository): Item\n" " {\n" " return $repository->findItem();\n" " }\n" "}\n"); } /* Fewer than 50 files exercises pass_calls.c. */ TEST(pipeline_php_receiver_suppresses_weak_method_edge) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_php_recv_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_php_receiver_fixture(tmp); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/php_recv.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_GTE(fixture_node_count(s, project, "src/Item.php", "getUid", "Method"), 1); ASSERT_FALSE(cross_file_call_exists(s, project, "vendorCall", "getUid")); ASSERT_FALSE(cross_file_call_exists(s, project, "chainedVendorCall", "getUid")); ASSERT_TRUE(cross_file_call_exists(s, project, "projectCall", "findItem")); ASSERT_GTE(named_edge_count(s, project, "CALLS", "selfCall", "localHelper"), 1); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* >= 50 files forces pass_parallel.c. */ TEST(pipeline_php_receiver_parallel_suppresses_weak_method_edge) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_php_par_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_php_receiver_fixture(tmp); for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/Filler%d.php", i); snprintf(body, sizeof(body), " a project ListMixin.extend) stay suppressed. * Fewer than 50 files exercises pass_calls.c. */ TEST(pipeline_python_receiver_keeps_specific_unique_name_member_call) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_python_uniq_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "compiler.py", "class SQLCompiler:\n" " def apply_converters(self, rows):\n" " return rows\n"); write_temp_file(tmp, "mixin.py", "class ListMixin:\n" " def extend(self, other):\n" " return other\n"); write_temp_file(tmp, "caller.py", "class RawIterable:\n" " def __init__(self, compiler):\n" " self.compiler = compiler\n" "\n" " def iterate(self):\n" " return self.compiler.apply_converters([])\n" "\n" "\n" "def collect(parts):\n" " parts.extend([1])\n" " return parts\n" "\n" "\n" "def handed_in(compiler):\n" " return compiler.apply_converters([])\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/python_uniq.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* POSITIVE: the only apply_converters in the project binds through * self.compiler, an object the class owns. */ ASSERT_TRUE(cross_file_call_exists(s, project, "iterate", "apply_converters")); /* NEGATIVE: the same unique name through a bare parameter carries no * ownership evidence and stays suppressed (#1276's own shape). */ ASSERT_FALSE(cross_file_call_exists(s, project, "handed_in", "apply_converters")); /* NEGATIVE: list.extend on a parameter must not bind ListMixin.extend. */ ASSERT_FALSE(cross_file_call_exists(s, project, "collect", "extend")); /* Tripwire against a vacuous run. */ ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "CALLS"), 1); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* Embedded-script hosts (2026-09-16 probe on JetBrains/Exposed): a member call * inside an HTML \n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/html_member.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* NEGATIVE: localStorage's own method must not bind the docs bundle. */ ASSERT_FALSE(cross_file_call_exists(s, project, "index", "getItem")); /* POSITIVE: the bare project call in storage.js resolves as before. */ ASSERT_TRUE(cross_file_call_exists(s, project, "loadTheme", "getItem")); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* CALLS edges whose source and target QNs end with the given suffixes and * whose serialized properties contain `props_fragment` (NULL: any edge). QN * suffixes pin the exact node, not just a same-named one. */ static int go_calls_between(cbm_store_t *s, const char *project, const char *caller_suffix, const char *callee_suffix, const char *props_fragment) { cbm_edge_t *edges = NULL; int edge_count = 0; cbm_store_find_edges_by_type(s, project, "CALLS", &edges, &edge_count); size_t cs = strlen(caller_suffix); size_t ts = strlen(callee_suffix); int hits = 0; for (int i = 0; i < edge_count; i++) { cbm_node_t src = {0}; cbm_node_t tgt = {0}; if (cbm_store_find_node_by_id(s, edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK && src.qualified_name && tgt.qualified_name) { size_t sl = strlen(src.qualified_name); size_t tl = strlen(tgt.qualified_name); if (sl >= cs && tl >= ts && strcmp(src.qualified_name + sl - cs, caller_suffix) == 0 && strcmp(tgt.qualified_name + tl - ts, callee_suffix) == 0 && (!props_fragment || (edges[i].properties_json && strstr(edges[i].properties_json, props_fragment)))) { hits++; } } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (edges) { cbm_store_free_edges(edges, edge_count); } return hits; } TEST(pipeline_go_method_caller_keeps_lsp_join) { /* #1909: receiver-qualifying method QNs moves the def/textual-call QN to * package.Type.method — go_lsp's enclosing-function QN must move with it * (the ONE-formula contract), or every LSP resolution sourced from inside * a method body loses its caller join and the edge dies. The callee name * collides across receivers so no short-name fallback can mask the loss. */ char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_mcall_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "go.mod", "module example.com/fxmcall\n\ngo 1.22\n"); write_temp_file(tmp, "svc/repo.go", "package svc\n" "\n" "type Repo struct {\n" "\tn int\n" "}\n" "\n" "func (r *Repo) Install() int {\n" "\treturn r.helper()\n" "}\n"); write_temp_file(tmp, "svc/helper.go", "package svc\n" "\n" "func (r *Repo) helper() int {\n" "\treturn r.n\n" "}\n"); write_temp_file(tmp, "other/other.go", "package other\n" "\n" "type Decoy struct{}\n" "\n" "func (d *Decoy) helper() int { return 2 }\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/go_mcall.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_TRUE(cross_file_call_exists(s, project, "Install", "helper")); /* Pinned to the receiver-qualified target in svc/helper.go and to an LSP * strategy: a name-only binding, or one onto other.Decoy.helper, would * still satisfy the by-name check above. */ ASSERT_GTE(go_calls_between(s, project, ".svc.Repo.Install", ".svc.Repo.helper", "\"strategy\":\"lsp_"), 1); ASSERT_EQ(go_calls_between(s, project, ".svc.Repo.Install", ".other.Decoy.helper", NULL), 0); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* Fixture for the #1906 Go selector-guard probes (sequential and parallel * twins). pad_files > 0 adds filler Go files to push the index over * MIN_FILES_FOR_PARALLEL, because the guard is wired into both resolvers * (pass_calls.c and pass_parallel.c) and each needs its own probe. */ static void write_go_receiver_fixture(const char *tmp, int pad_files) { /* go.mod makes project imports resolvable — real Go repos always have one, * and import reachability (the unique_name penalty) depends on it. */ write_temp_file(tmp, "go.mod", "module example.com/myapp\n\ngo 1.22\n"); /* The lone project symbol named "Close" — a real method. */ write_temp_file(tmp, "storage/storage.go", "package storage\n" "\n" "type Storage struct{ open bool }\n" "\n" "func NewStorage() *Storage { return &Storage{open: true} }\n" "\n" "func (s *Storage) Close() {\n" "\ts.open = false\n" "}\n" "\n" "func Boot() {\n" "\ts := NewStorage()\n" "\ts.Close()\n" "}\n"); /* Cross-package control target: imported by hash.go, so the caller file * has a non-empty import map (like any real Go file) and unreachable * unique_name candidates get the import penalty. */ write_temp_file(tmp, "util/util.go", "package util\n" "\n" "func Tag() string { return \"t\" }\n"); /* Stdlib receiver: `f.Close()` closes an *os.File, NOT the project method. * The Go LSP cannot bind it to a project symbol → the registry would guess * Close by short name (weak). This is the false edge to suppress — * the exact shape that attached every file/rows/gzip Close in a real Go * repo to one unrelated project method. */ write_temp_file(tmp, "hash/hash.go", "package hash\n" "\n" "import (\n" "\t\"os\"\n" "\n" "\t\"example.com/myapp/util\"\n" ")\n" "\n" "func FileLen(path string) int64 {\n" "\tf, err := os.Open(path)\n" "\tif err != nil {\n" "\t\treturn 0\n" "\t}\n" "\tdefer f.Close()\n" "\tst, err := f.Stat()\n" "\tif err != nil {\n" "\t\treturn 0\n" "\t}\n" "\treturn st.Size()\n" "}\n" "\n" "func localHelper() int { return 1 }\n" "\n" "func CallsLocal() int { return localHelper() }\n" "\n" "func UsesUtil() string { return util.Tag() }\n"); /* F-G9: two project methods named Run, one under a directory whose QN * carries an uppercase T. `t.Run` on a *testing.T must bind neither. The * parallel resolver field-type hint matched the capitalized receiver * variable ("T") as a QN substring and produced a par-only edge. */ write_temp_file(tmp, "TBench/bench.go", "package tbench\n" "\n" "type Bench struct{}\n" "\n" "func (b *Bench) Run(name string) bool { return name != \"\" }\n"); write_temp_file(tmp, "worker/worker.go", "package worker\n" "\n" "type Worker struct{}\n" "\n" "func (w *Worker) Run(name string) bool { return false }\n"); write_temp_file(tmp, "hash/hash_test.go", "package hash\n" "\n" "import \"testing\"\n" "\n" "func TestSubRun(t *testing.T) {\n" "\tt.Run(\"a\", func(t *testing.T) {})\n" "}\n"); for (int i = 0; i < pad_files; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "filler/filler%d.go", i); snprintf(body, sizeof(body), "package filler\n\nfunc Filler%d() int { return %d }\n", i, i); write_temp_file(tmp, name, body); } } TEST(pipeline_go_receiver_suppresses_weak_method_edge) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_recv_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_go_receiver_fixture(tmp, 0); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/go_recv.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* (1) The false edge is suppressed (reproduce-first: RED before the fix). */ ASSERT_FALSE(cross_file_call_exists(s, project, "FileLen", "Close")); /* (2) The same-package typed-receiver call survives (LSP / same_module — * both outside the weak drop-list). */ ASSERT_TRUE(cross_file_call_exists(s, project, "Boot", "Close")); /* (3) The bare local call survives (is_method stays false for bare calls). */ ASSERT_TRUE(cross_file_call_exists(s, project, "CallsLocal", "localHelper")); /* (4) The import-qualified cross-package call survives (import-aware * strategies are outside the drop-list). */ ASSERT_TRUE(cross_file_call_exists(s, project, "UsesUtil", "Tag")); /* (5) F-G9: the stdlib *testing.T Run binds no project Run. */ ASSERT_FALSE(cross_file_call_exists(s, project, "TestSubRun", "Run")); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* Parallel twin of the test above. The guard is wired into pass_parallel.c as * well as pass_calls.c, but the four-file fixture only ever runs the * sequential resolver, so the parallel hunk was bound by no test. Real * repositories take the parallel path: >= MIN_FILES_FOR_PARALLEL (50) files * plus CBM_WORKERS=4 force it, the shape of * pipeline_python_receiver_parallel_suppresses_weak_method_edges. Same * both-directions pin as the sequential test. */ TEST(pipeline_go_receiver_parallel_suppresses_weak_method_edge) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_recv_par_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_go_receiver_fixture(tmp, 52); char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/go_recv_par.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* NEGATIVE: the stdlib *os.File Close must not bind the project method. */ ASSERT_FALSE(cross_file_call_exists(s, project, "FileLen", "Close")); /* POSITIVE: typed same-package, bare local and import-qualified calls * survive the parallel resolver too. */ ASSERT_TRUE(cross_file_call_exists(s, project, "Boot", "Close")); ASSERT_TRUE(cross_file_call_exists(s, project, "CallsLocal", "localHelper")); ASSERT_TRUE(cross_file_call_exists(s, project, "UsesUtil", "Tag")); /* F-G9: the parallel-only field-type hint must not bind `t.Run` either. */ ASSERT_FALSE(cross_file_call_exists(s, project, "TestSubRun", "Run")); cbm_store_close(s); cbm_pipeline_free(p); if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); PASS(); } /* Fixture for the #1928 cross-language reference-guard probes (sequential and * parallel twins). pad_files > 0 adds filler files to push the index over the * parallel-pipeline threshold, since USAGE/WRITES/READS have one resolver per * path and both must consult the guard. */ static void write_go_c_ref_guard_fixture(const char *tmp, int pad_files) { write_temp_file(tmp, "go.mod", "module example.com/fxguard\n\ngo 1.22\n"); /* C targets must be indexed: block-local C variables are not emitted, * so keep event at file scope for the cross-language reference guard. */ write_temp_file(tmp, "probe/probe.c", "static int total_events = 0;\n" "static int event = 0;\n" "\n" "static int handle(void) {\n" " event = 0;\n" " total_events += event;\n" " return event;\n" "}\n"); /* Go local writes and value uses must not bind to the C variables or * functions in this fixture. */ write_temp_file(tmp, "app/app.go", "package app\n" "\n" "func TrackEvent() int {\n" "\tevent := 1\n" "\treturn event\n" "}\n" "\n" "func UsesHandle() int {\n" "\th := handle\n" "\t_ = h\n" "\treturn 4\n" "}\n" "\n" "func WriteTotal() {\n" "\ttotal_events := 5\n" "\t_ = total_events\n" "}\n"); /* Same-language control: a Go package-level var written from another * file must keep its WRITES edge. */ write_temp_file(tmp, "state/vars.go", "package state\n" "\n" "var Counter int\n"); write_temp_file(tmp, "state/state.go", "package state\n" "\n" "func Bump() {\n" "\tCounter = 2\n" "}\n"); for (int i = 0; i < pad_files; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "pad/filler%d.go", i); snprintf(body, sizeof(body), "package pad\n\nfunc filler%d() int { return %d }\n", i, i); write_temp_file(tmp, name, body); } } /* F-G8 D1: a variable bound by a CROSS-file multi-value or named-result call * is typed. The cross-file registrar received the raw result text * `(*Encoder, error)`, which did not parse, so `e, err := enc.NewEncoder()` * left e untyped and e.EncodeTuple() fell to a name guess. A single result * keeps its raw text: `[]Sample` must stay a slice so that `xs[0].TS()` is * typed (the extractor's cleaned return_types read it as `Sample`). Every * target method has two same-named decoys, so a guess cannot pass. * pad_files > 0 forces the parallel resolver. */ static void write_go_multi_return_fixture(const char *tmp, int pad_files) { write_temp_file(tmp, "go.mod", "module example.com/fx\n\ngo 1.22\n"); write_temp_file(tmp, "enc/enc.go", "package enc\n" "\n" "type Encoder struct{ n int }\n" "\n" "func NewEncoder() (*Encoder, error) { return &Encoder{}, nil }\n" "\n" "func NewNamed() (e *Encoder) { e = &Encoder{}; return }\n" "\n" "func NewPair() (a, b *Encoder) { return &Encoder{}, &Encoder{} }\n" "\n" "func (e *Encoder) EncodeTuple() int { return e.n }\n" "\n" "func (e *Encoder) EncodeNamed() int { return e.n }\n" "\n" "func (e *Encoder) EncodeGrouped() int { return e.n }\n"); write_temp_file(tmp, "samples/samples.go", "package samples\n" "\n" "type Sample interface{ TS() int64 }\n" "\n" "type sample struct{}\n" "\n" "func (sample) TS() int64 { return 0 }\n" "\n" "func Generate() []Sample { return []Sample{sample{}} }\n" "\n" "func List() ([]Sample, error) { return []Sample{sample{}}, nil }\n"); write_temp_file(tmp, "diag/diag.go", "package diag\n" "\n" "type Diagnostic struct{ msg string }\n" "\n" "type Diagnostics []Diagnostic\n" "\n" "func (d Diagnostics) HasErrorsSlice() bool { return len(d) > 0 }\n" "\n" "func Run() (int, Diagnostics) { return 0, nil }\n"); write_temp_file(tmp, "app/types.go", "package app\n" "\n" "type isolation struct{}\n" "\n" "func (i *isolation) LowWatermarkField() int { return 0 }\n" "\n" "type Head struct{ iso *isolation }\n" "\n" "func newHead() (*Head, error) { return &Head{iso: &isolation{}}, nil }\n"); write_temp_file(tmp, "app/app.go", "package app\n" "\n" "import (\n" "\t\"example.com/fx/diag\"\n" "\t\"example.com/fx/enc\"\n" "\t\"example.com/fx/samples\"\n" ")\n" "\n" "func UseIndex() int64 {\n" "\txs := samples.Generate()\n" "\treturn xs[0].TS()\n" "}\n" "\n" "func UseTupleSlice() int64 {\n" "\tys, _ := samples.List()\n" "\treturn ys[0].TS()\n" "}\n" "\n" "func UseGrouped() int {\n" "\tx, _ := enc.NewPair()\n" "\treturn x.EncodeGrouped()\n" "}\n" "\n" "func UseTuple() int {\n" "\te, err := enc.NewEncoder()\n" "\t_ = err\n" "\treturn e.EncodeTuple()\n" "}\n" "\n" "func UseNamed() int {\n" "\te := enc.NewNamed()\n" "\treturn e.EncodeNamed()\n" "}\n" "\n" "func UseField() int {\n" "\th, _ := newHead()\n" "\treturn h.iso.LowWatermarkField()\n" "}\n" "\n" "func UseSlice() bool {\n" "\t_, d := diag.Run()\n" "\treturn d.HasErrorsSlice()\n" "}\n"); write_temp_file(tmp, "decoy/decoy.go", "package decoy\n" "\n" "type D1 struct{}\n" "\n" "type D2 struct{}\n" "\n" "func (D1) EncodeTuple() int { return 0 }\n" "func (D2) EncodeTuple() int { return 0 }\n" "func (D1) EncodeNamed() int { return 0 }\n" "func (D2) EncodeNamed() int { return 0 }\n" "func (D1) LowWatermarkField() int { return 0 }\n" "func (D2) LowWatermarkField() int { return 0 }\n" "func (D1) HasErrorsSlice() bool { return false }\n" "func (D2) HasErrorsSlice() bool { return false }\n" "func (D1) EncodeGrouped() int { return 0 }\n" "func (D2) EncodeGrouped() int { return 0 }\n" "func (D1) TS() int64 { return 0 }\n" "func (D2) TS() int64 { return 0 }\n"); for (int i = 0; i < pad_files; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "filler/filler%d.go", i); snprintf(body, sizeof(body), "package filler\n\nfunc Filler%d() int { return %d }\n", i, i); write_temp_file(tmp, name, body); } } static int go_multi_return_case(int pad_files) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_multiret_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_go_multi_return_fixture(tmp, pad_files); char *saved = NULL; if (pad_files > 0) { char *old_workers = getenv("CBM_WORKERS"); saved = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/go_multiret.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); int rc = p ? cbm_pipeline_run(p) : -1; if (pad_files > 0) { if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } } ASSERT_NOT_NULL(p); ASSERT_EQ(rc, 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); static const struct { const char *caller; const char *method; const char *file; } want[] = { {"UseTuple", "EncodeTuple", "enc/enc.go"}, /* e, err := enc.NewEncoder() */ {"UseNamed", "EncodeNamed", "enc/enc.go"}, /* named result (e *Encoder) */ {"UseField", "LowWatermarkField", "app/types.go"}, /* h, _ := newHead(); h.iso.X() */ {"UseSlice", "HasErrorsSlice", "diag/diag.go"}, /* _, d := diag.Run() */ {"UseGrouped", "EncodeGrouped", "enc/enc.go"}, /* (a, b *Encoder) */ {"UseIndex", "TS", "samples/samples.go"}, /* single []Sample, xs[0] */ {"UseTupleSlice", "TS", "samples/samples.go"}, /* ([]Sample, error) */ }; for (size_t i = 0; i < sizeof(want) / sizeof(want[0]); i++) { int right = named_edge_to_file_count(s, project, "CALLS", want[i].caller, want[i].method, want[i].file); int decoy = named_edge_to_file_count(s, project, "CALLS", want[i].caller, want[i].method, "decoy/decoy.go"); bool typed = cross_file_call_has_strategy(s, project, want[i].caller, want[i].method, "\"strategy\":\"lsp_"); if (right < 1 || decoy != 0 || !typed) { printf(" %s -> %s: right=%d decoy=%d lsp=%d\n", want[i].caller, want[i].method, right, decoy, typed); FAIL("multi-value / named-result receiver not typed"); } } cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } TEST(pipeline_go_multi_return_receiver_is_typed) { return go_multi_return_case(0); } TEST(pipeline_go_multi_return_receiver_is_typed_parallel) { return go_multi_return_case(52); } TEST(pipeline_go_rw_usage_never_cross_into_c) { /* #1928: USAGE and WRITES/READS resolve through the same short-name * registry as CALLS but never consulted the #725 cross-language guard. * On a Go tree with eBPF C probes every Go identifier spelled like a C * one produced an edge into the C file — 31.5% of all WRITES on the * measured repo crossed the Go/C boundary. Go code cannot write a C * automatic variable, so every edge in that class is false. This is the * SEQUENTIAL-path twin; the parallel twin follows. */ char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_rw_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_go_c_ref_guard_fixture(tmp, 0); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/go_rw.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* Reproduce-first: RED before the fix — the Go local write binds the C * probe's global, and the Go value use binds the C `handle`. */ ASSERT_GTE(fixture_node_count(s, project, "app/app.go", "TrackEvent", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "app/app.go", "UsesHandle", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "app/app.go", "WriteTotal", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "probe/probe.c", "handle", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "probe/probe.c", "event", "Variable"), 1); ASSERT_GTE(fixture_node_count(s, project, "probe/probe.c", "total_events", "Variable"), 1); ASSERT_FALSE(cross_file_edge_exists(s, project, "TrackEvent", "event", "WRITES")); ASSERT_FALSE(cross_file_edge_exists(s, project, "TrackEvent", "event", "READS")); ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "WRITES")); ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "READS")); ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "USAGE")); ASSERT_FALSE(cross_file_edge_exists(s, project, "WriteTotal", "total_events", "WRITES")); ASSERT_FALSE(cross_file_edge_exists(s, project, "WriteTotal", "total_events", "USAGE")); /* Same-language reference edges survive the guard. */ ASSERT_TRUE(cross_file_edge_exists(s, project, "Bump", "Counter", "WRITES")); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* The Rust registries were built from the mixed-language def set: a Perl * `sub new` became a Rust free function, and the project-wide * lsp_short_name_unique fallback bound `Store::new()` to it at 0.90 (found by * the generic-engine lane on its 12-language fixture). A Rust call never * targets another language's definition. pad_files > 0 forces the parallel * resolver, whose Rust registry is the shared all-defs build. */ /* Pads a Rust fixture with pad_files filler modules (pad_files > 0 forces the * parallel resolver: CBM_WORKERS=4 for the run), runs the full pipeline into * / and opens the store. NULL store = the run failed. */ static cbm_store_t *rust_fixture_run(const char *tmp, int pad_files, const char *db_name, cbm_pipeline_t **out_p) { for (int i = 0; i < pad_files; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/filler%d.rs", i); snprintf(body, sizeof(body), "pub fn filler%d() -> u32 {\n %d\n}\n", i, i); write_temp_file(tmp, name, body); } char *saved = NULL; if (pad_files > 0) { char *old_workers = getenv("CBM_WORKERS"); saved = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/%s", tmp, db_name); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); int rc = p ? cbm_pipeline_run(p) : -1; if (pad_files > 0) { if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } } *out_p = p; return rc == 0 ? cbm_store_open_path(db_path) : NULL; } static int rust_registry_rust_only_case(int pad_files) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_rust_only_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "Cargo.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n"); write_temp_file(tmp, "src/store.rs", "pub struct Store {\n" " n: u32,\n" "}\n" "\n" "impl Store {\n" " pub fn new() -> Store {\n" " Store { n: 0 }\n" " }\n" "\n" " pub fn get(&self, k: u32) -> u32 {\n" " self.n + k\n" " }\n" "}\n"); write_temp_file(tmp, "src/main.rs", "mod store;\n" "\n" "use crate::store::Store;\n" "\n" "fn run() -> u32 {\n" " let s = Store::new();\n" " s.get(1)\n" "}\n" "\n" "fn main() {\n" " run();\n" "}\n"); write_temp_file(tmp, "perlsrc/Lib.pm", "package Lib;\n" "\n" "sub new {\n" " my ($class) = @_;\n" " return bless {}, $class;\n" "}\n" "\n" "1;\n"); cbm_pipeline_t *p = NULL; cbm_store_t *s = rust_fixture_run(tmp, pad_files, "rust_only.db", &p); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Never onto the Perl constructor ... */ ASSERT_EQ(named_edge_to_file_count(s, project, "CALLS", "run", "new", "perlsrc/Lib.pm"), 0); /* ... and the Rust call still binds the Rust associated function. */ ASSERT_GTE(named_edge_to_file_count(s, project, "CALLS", "run", "new", "src/store.rs"), 1); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } TEST(pipeline_rust_call_never_binds_another_language) { return rust_registry_rust_only_case(0); } TEST(pipeline_rust_call_never_binds_another_language_parallel) { return rust_registry_rust_only_case(52); } /* `Opts::write(true)` is an associated function. The LSP leaves it to the * registry (import_map -> fsx.Opts.write), but its free-function short-name * fallback bound it to the project's only free `write` (cache.write) first, and * that LSP row pre-empted the right answer. With the cross-language namesakes * gone (above), this hijack replaced 600+ correct qualified_suffix/import_map * edges on the Rust corpus. A type-qualified call never takes that fallback. */ static int rust_assoc_call_free_namesake_case(int pad_files) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_rust_assoc_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "Cargo.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n"); write_temp_file(tmp, "src/fsx.rs", "pub struct Opts {\n" " pub w: bool,\n" "}\n" "\n" "impl Opts {\n" " pub fn write(w: bool) -> Opts {\n" " Opts { w }\n" " }\n" "}\n"); write_temp_file(tmp, "src/cache.rs", "pub fn write(p: &str) -> usize {\n" " p.len()\n" "}\n"); write_temp_file(tmp, "src/main.rs", "mod cache;\n" "mod fsx;\n" "\n" "use crate::fsx::Opts;\n" "\n" "fn run() -> bool {\n" " let o = Opts::write(true);\n" " o.w\n" "}\n" "\n" "fn main() {\n" " run();\n" " cache::write(\"x\");\n" "}\n"); cbm_pipeline_t *p = NULL; cbm_store_t *s = rust_fixture_run(tmp, pad_files, "rust_assoc.db", &p); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* The associated function, never the free namesake ... */ ASSERT_GTE(named_edge_to_file_count(s, project, "CALLS", "run", "write", "src/fsx.rs"), 1); ASSERT_EQ(named_edge_to_file_count(s, project, "CALLS", "run", "write", "src/cache.rs"), 0); /* ... while the module-qualified free call keeps its target. */ ASSERT_GTE(named_edge_to_file_count(s, project, "CALLS", "main", "write", "src/cache.rs"), 1); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } TEST(pipeline_rust_assoc_call_never_binds_free_namesake) { return rust_assoc_call_free_namesake_case(0); } TEST(pipeline_rust_assoc_call_never_binds_free_namesake_parallel) { return rust_assoc_call_free_namesake_case(52); } /* F-R4 (#763, Rust): calls inside a method of a generic impl (`impl Gen`, * `impl<'a> Life<'a>`) were sourced from the File node: the call scope kept the * generic arguments the def side strips. Each method must be the source of its * own call. pad_files > 0 forces the parallel resolver. */ static int rust_generic_impl_attribution_case(int pad_files) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_rust_gimpl_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "Cargo.toml", "[package]\nname = \"fx\"\nversion = \"0.1.0\"\n"); write_temp_file( tmp, "src/lib.rs", "pub struct Gen { v: T }\n" "impl Gen {\n" " pub fn in_generic(&self) -> u32 { helper_a() + self.other_g() + Self::assoc_g() }\n" " pub fn other_g(&self) -> u32 { 3 }\n" " pub fn assoc_g() -> u32 { 4 }\n" "}\n" "pub struct Life<'a> { s: &'a str }\n" "impl<'a> Life<'a> {\n" " pub fn in_lifetime(&self) -> u32 { helper_b() + self.other_l() }\n" " pub fn other_l(&self) -> u32 { 5 }\n" "}\n" "pub fn helper_a() -> u32 { 1 }\n" "pub fn helper_b() -> u32 { 2 }\n"); for (int i = 0; i < pad_files; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/filler%d.rs", i); snprintf(body, sizeof(body), "pub fn filler%d() -> u32 {\n %d\n}\n", i, i); write_temp_file(tmp, name, body); } char *saved = NULL; if (pad_files > 0) { char *old_workers = getenv("CBM_WORKERS"); saved = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/rust_gimpl.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_TRUE(cross_file_call_exists(s, project, "in_generic", "helper_a")); ASSERT_TRUE(cross_file_call_exists(s, project, "in_lifetime", "helper_b")); /* ... and not from the file. */ ASSERT_FALSE(cross_file_call_exists(s, project, "lib.rs", "helper_a")); ASSERT_FALSE(cross_file_call_exists(s, project, "lib.rs", "helper_b")); /* The LSP side names the impl's self type without its arguments too: its * rows join these callers (lsp_*, not a registry fallback), and `self.` / * `Self::` dispatch inside a generic impl resolves (Self::assoc_g had no * edge at all). */ ASSERT_TRUE( cross_file_call_has_strategy(s, project, "in_generic", "helper_a", "\"strategy\":\"lsp_")); ASSERT_TRUE( cross_file_call_has_strategy(s, project, "in_generic", "other_g", "\"strategy\":\"lsp_")); ASSERT_TRUE(cross_file_call_exists(s, project, "in_generic", "assoc_g")); ASSERT_TRUE( cross_file_call_has_strategy(s, project, "in_lifetime", "other_l", "\"strategy\":\"lsp_")); cbm_store_close(s); cbm_pipeline_free(p); if (pad_files > 0) { if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } } th_rmtree(tmp); PASS(); } TEST(pipeline_rust_generic_impl_method_sources_its_calls) { return rust_generic_impl_attribution_case(0); } TEST(pipeline_rust_generic_impl_method_sources_its_calls_parallel) { return rust_generic_impl_attribution_case(52); } TEST(pipeline_go_rw_usage_never_cross_into_c_parallel) { /* Parallel twin of the test above: USAGE and WRITES/READS each have a * second, independent resolver in pass_parallel.c (resolve_file_usages / * resolve_file_rw) that must consult the same guard — the field census * that motivated this caught the sequential-only fix leaving 344 Go→C * WRITES alive on a ~1150-file repo. >= 50 files forces the parallel * pipeline. */ char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_rwp_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_go_c_ref_guard_fixture(tmp, 52); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/go_rwp.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_GTE(fixture_node_count(s, project, "app/app.go", "TrackEvent", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "app/app.go", "UsesHandle", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "app/app.go", "WriteTotal", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "probe/probe.c", "handle", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "probe/probe.c", "event", "Variable"), 1); ASSERT_GTE(fixture_node_count(s, project, "probe/probe.c", "total_events", "Variable"), 1); ASSERT_FALSE(cross_file_edge_exists(s, project, "TrackEvent", "event", "WRITES")); ASSERT_FALSE(cross_file_edge_exists(s, project, "TrackEvent", "event", "READS")); ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "WRITES")); ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "READS")); ASSERT_FALSE(cross_file_edge_exists(s, project, "UsesHandle", "handle", "USAGE")); ASSERT_FALSE(cross_file_edge_exists(s, project, "WriteTotal", "total_events", "WRITES")); ASSERT_FALSE(cross_file_edge_exists(s, project, "WriteTotal", "total_events", "USAGE")); ASSERT_TRUE(cross_file_edge_exists(s, project, "Bump", "Counter", "WRITES")); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* Fixture for the #2053 Rust std-receiver probes (sequential and parallel * twins). The Rust LSP types `root: &Path` and dispatches `root.join(..)` to * std's Path::join — a symbol with no graph node. Before the fix the pipeline * then fell back to the textual registry, which bound the call to the only * project method named `join` (EvidenceTier::join, unique_name). The * call-expression receiver (`root.to_path_buf().join(..)`) is the same class * one hop later: it needs to_path_buf's return type and PathBuf's Deref to * Path. POSITIVE tripwires: the typed project call keeps its LSP edge, and a * closure-parameter receiver the LSP cannot type still reaches the registry, * so the fix removes only calls the LSP positively placed outside the * project. `m.get(k)` on a std HashMap guards the parallel route fallback, * which must not turn the skipped call into a self-call. pad_files > 0 * crosses the parallel-pipeline threshold. */ static void write_rust_std_receiver_fixture(const char *tmp, int pad_files) { write_temp_file(tmp, "Cargo.toml", "[package]\nname = \"stdrecv\"\nversion = \"0.1.0\"\nedition = \"2021\"\n"); write_temp_file(tmp, "src/lib.rs", "use std::collections::HashMap;\n" "use std::path::{Path, PathBuf};\n" "\n" "pub struct EvidenceTier(pub u8);\n" "impl EvidenceTier {\n" " pub fn join(&self, other: &EvidenceTier) -> EvidenceTier {\n" " EvidenceTier(self.0.max(other.0))\n" " }\n" "}\n" "\n" "pub struct Gauge(pub u8);\n" "impl Gauge {\n" " pub fn recalibrate_gauge(&self) -> u8 {\n" " self.0\n" " }\n" "}\n" "\n" "pub fn real_caller() -> EvidenceTier {\n" " EvidenceTier(1).join(&EvidenceTier(2))\n" "}\n" "\n" "pub fn stdlib_receiver(root: &Path) -> PathBuf {\n" " root.join(\"subdir\")\n" "}\n" "\n" "pub fn call_expr_receiver(root: &Path) -> PathBuf {\n" " root.to_path_buf().join(\"nested\")\n" "}\n" "\n" "pub fn untyped_receiver(gauges: &[Gauge]) -> u8 {\n" " gauges.iter().map(|g| g.recalibrate_gauge()).sum()\n" "}\n" "\n" "pub fn std_map_lookup(m: &HashMap, k: &str) -> Option {\n" " m.get(k).copied()\n" "}\n"); for (int i = 0; i < pad_files; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/filler%d.rs", i); snprintf(body, sizeof(body), "pub fn filler%d() -> i32 {\n %d\n}\n", i, i); write_temp_file(tmp, name, body); } } static int run_rust_std_receiver_probe(int pad_files, const char *tag) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_rs_%s_XXXXXX", tag); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_rust_std_receiver_fixture(tmp, pad_files); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/rs_std_recv.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* NEGATIVE: std's Path::join / PathBuf::join are not project symbols. */ ASSERT_EQ(named_edge_count(s, project, "CALLS", "stdlib_receiver", "join"), 0); ASSERT_EQ(named_edge_count(s, project, "CALLS", "call_expr_receiver", "join"), 0); /* NEGATIVE: a std `map.get(k)` with no route path is no self-call (the * parallel empty-resolution route fallback must not bind source->source). */ ASSERT_EQ(named_edge_count(s, project, "CALLS", "std_map_lookup", "std_map_lookup"), 0); /* POSITIVE: the typed project call keeps its edge. */ ASSERT_EQ(named_edge_count(s, project, "CALLS", "real_caller", "join"), 1); /* POSITIVE: an untyped receiver still reaches the registry fallback. */ ASSERT_EQ(named_edge_count(s, project, "CALLS", "untyped_receiver", "recalibrate_gauge"), 1); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } TEST(pipeline_rust_std_receiver_never_binds_project_method) { return run_rust_std_receiver_probe(0, "seq"); } TEST(pipeline_rust_std_receiver_never_binds_project_method_parallel) { return run_rust_std_receiver_probe(52, "par"); } static int count_nodes_named(cbm_store_t *s, const char *project, const char *name); /* Fixture for the #1942 bare-reference-vs-Field probes: a Go struct field * named like the commonest local (`err`), and a function whose local of the * same name must NOT bind it — in Go a field is only reachable through a * selector expression, never a bare identifier. Needs Go Field extraction * (#1935) to have anything to falsely bind. */ static void write_go_bare_field_fixture(const char *tmp, int pad_files) { write_temp_file(tmp, "go.mod", "module example.com/fxbare\n\ngo 1.22\n"); write_temp_file(tmp, "state/state.go", "package state\n" "\n" "type Tracker struct {\n" "\terr error\n" "\tn int\n" "}\n"); write_temp_file(tmp, "app/app.go", "package app\n" "\n" "import \"errors\"\n" "\n" "func Run() error {\n" "\terr := errors.New(\"x\")\n" "\treturn err\n" "}\n"); /* #1962: genuine selector references from a sibling file of the same * package. `t.err = nil` writes the field through a selector; `t.n` reads * it. The extractor strips the receiver on both paths, so only the * is_member_access signal can distinguish these from Run's bare local. */ write_temp_file(tmp, "state/reset.go", "package state\n" "\n" "func (t *Tracker) Reset() int {\n" "\tt.err = nil\n" "\treturn t.n\n" "}\n"); for (int i = 0; i < pad_files; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "pad/filler%d.go", i); snprintf(body, sizeof(body), "package pad\n\nfunc filler%d() int { return %d }\n", i, i); write_temp_file(tmp, name, body); } } TEST(pipeline_go_bare_ref_never_binds_field) { /* #1942: the READS/WRITES resolvers and the USAGE registry fallback hand * bare reference text to the short-name registry, which contains Field * nodes — so every Go local `err := …` bound whichever struct field was * named err (21308 USAGE / 5191 WRITES onto Go fields on the measured * repo, top target a test struct's field collecting 3013 edges). * Sequential-path twin; the parallel twin follows. */ char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_bare_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_go_bare_field_fixture(tmp, 0); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/go_bare.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* The field must exist for the probe to mean anything (#1935's fix). */ ASSERT_GTE(fixture_node_count(s, project, "app/app.go", "Run", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "state/state.go", "err", "Field"), 1); /* Reproduce-first: RED before the fix — the bare local binds the field. */ ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "WRITES")); ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "READS")); ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "USAGE")); /* #1962, reproduce-first: RED while the guard is a blanket veto — genuine * selector references must reach the field (write via `t.err = nil`, * value use via `t.n`). */ ASSERT_TRUE(cross_file_edge_exists(s, project, "Reset", "err", "WRITES")); ASSERT_TRUE(cross_file_edge_exists(s, project, "Reset", "n", "USAGE")); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } TEST(pipeline_go_bare_ref_never_binds_field_parallel) { /* Parallel twin: resolve_file_rw / resolve_file_usages are independent * resolvers and must consult the same predicate (#1928's lesson). */ char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_barep_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_go_bare_field_fixture(tmp, 52); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/go_barep.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_GTE(fixture_node_count(s, project, "app/app.go", "Run", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "state/state.go", "err", "Field"), 1); ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "WRITES")); ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "READS")); ASSERT_FALSE(cross_file_edge_exists(s, project, "Run", "err", "USAGE")); /* #1962 parallel twin: resolve_file_rw / resolve_file_usages must honour * the member-access signal exactly like the sequential resolvers. */ ASSERT_TRUE(cross_file_edge_exists(s, project, "Reset", "err", "WRITES")); ASSERT_TRUE(cross_file_edge_exists(s, project, "Reset", "n", "USAGE")); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* A C caller of a header function that shares its name with a struct member * elsewhere (kernel: include/linux/mm.h put_page beside the arm64 * kvm_pgtable_mm_ops.put_page member). pad_files pushes the run past * MIN_FILES_FOR_PARALLEL for the parallel twin. */ static void write_c_header_namesake_fixture(const char *tmp, int pad_files) { write_temp_file(tmp, "include/linux/mm.h", "struct page;\n" "static inline void put_page(struct page *page)\n" "{\n" "\t(void)page;\n" "}\n"); write_temp_file(tmp, "arch/arm64/include/asm/kvm_pgtable.h", "struct kvm_pgtable_mm_ops {\n" "\tvoid (*put_page)(void *addr);\n" "};\n"); write_temp_file(tmp, "arch/arm64/kernel/elfcore.c", "#include \n" "#include \n" "int mte_dump_tag_range(struct page *page)\n" "{\n" "\tput_page(page);\n" "\treturn 0;\n" "}\n"); write_temp_file(tmp, "drivers/net/rx.c", "#include \n" "void recv_buffer_free(struct page *page)\n" "{\n" "\tput_page(page);\n" "}\n"); for (int i = 0; i < pad_files; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "pad/filler%d.c", i); snprintf(body, sizeof(body), "int filler%d(void) { return %d; }\n", i, i); write_temp_file(tmp, name, body); } } /* CALLS edges from the nodes named src_name into nodes named tgt_name that * carry tgt_label: the name-only probes cannot tell the header Function from * its struct-member namesake. */ static int labelled_call_count(cbm_store_t *s, const char *project, const char *src_name, const char *tgt_name, const char *tgt_label) { cbm_node_t *srcs = NULL; cbm_node_t *tgts = NULL; int sc = 0; int tc = 0; int n = 0; cbm_store_find_nodes_by_name(s, project, src_name, &srcs, &sc); cbm_store_find_nodes_by_name(s, project, tgt_name, &tgts, &tc); for (int i = 0; i < sc; i++) { cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(s, srcs[i].id, "CALLS", &edges, &ec); for (int j = 0; j < ec; j++) { for (int k = 0; k < tc; k++) { if (edges[j].target_id == tgts[k].id && tgts[k].label && strcmp(tgts[k].label, tgt_label) == 0) { n++; } } } if (edges) { cbm_store_free_edges(edges, ec); } } if (srcs) { cbm_store_free_nodes(srcs, sc); } if (tgts) { cbm_store_free_nodes(tgts, tc); } return n; } TEST(pipeline_c_call_reaches_header_function_beside_field) { /* 2026-10-13 kernel audit: `put_page(page)` 345 -> 9 callers. The struct * member in the short-name registry makes the bare name two candidates, * so the lookup answers by suffix_match; .h is C++ in the extension table * and the #725 guard dropped the C caller's edge as cross-language. * Reproduce-first: RED before the fix. Sequential-path twin. */ char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_c_hdr_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_c_header_namesake_fixture(tmp, 0); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/c_hdr.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* Both namesakes must exist for the probe to mean anything. */ ASSERT_GTE(fixture_node_count(s, project, "include/linux/mm.h", "put_page", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "arch/arm64/include/asm/kvm_pgtable.h", "put_page", "Field"), 1); ASSERT_EQ(labelled_call_count(s, project, "mte_dump_tag_range", "put_page", "Function"), 1); ASSERT_EQ(labelled_call_count(s, project, "recv_buffer_free", "put_page", "Function"), 1); /* A bare C call never names the struct member. */ ASSERT_EQ(labelled_call_count(s, project, "mte_dump_tag_range", "put_page", "Field"), 0); ASSERT_EQ(labelled_call_count(s, project, "recv_buffer_free", "put_page", "Field"), 0); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } TEST(pipeline_c_call_reaches_header_function_beside_field_parallel) { /* Parallel twin: the kernel resolves through pass_parallel.c, which asks * the same #725 guard. */ char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_c_hdrp_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_c_header_namesake_fixture(tmp, 52); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/c_hdrp.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_GTE(fixture_node_count(s, project, "include/linux/mm.h", "put_page", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "arch/arm64/include/asm/kvm_pgtable.h", "put_page", "Field"), 1); ASSERT_EQ(labelled_call_count(s, project, "mte_dump_tag_range", "put_page", "Function"), 1); ASSERT_EQ(labelled_call_count(s, project, "recv_buffer_free", "put_page", "Function"), 1); ASSERT_EQ(labelled_call_count(s, project, "mte_dump_tag_range", "put_page", "Field"), 0); ASSERT_EQ(labelled_call_count(s, project, "recv_buffer_free", "put_page", "Field"), 0); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* True when a node named src_name has an edge of edge_type to the Field * `field` OWNED BY `owner` (qualified name ends in ".owner.field"). The * name-only cross_file_edge_exists cannot tell two same-named fields apart, * which is exactly what the C member-access probes below must do. */ static bool field_edge_exists(cbm_store_t *s, const char *project, const char *src_name, const char *owner, const char *field, const char *edge_type) { char suffix[256]; snprintf(suffix, sizeof(suffix), ".%s.%s", owner, field); size_t suffix_len = strlen(suffix); cbm_node_t *srcs = NULL; cbm_node_t *tgts = NULL; int sc = 0; int tc = 0; cbm_store_find_nodes_by_name(s, project, src_name, &srcs, &sc); cbm_store_find_nodes_by_name(s, project, field, &tgts, &tc); bool found = false; for (int i = 0; i < sc && !found; i++) { cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(s, srcs[i].id, edge_type, &edges, &ec); for (int j = 0; j < ec && !found; j++) { for (int k = 0; k < tc && !found; k++) { const char *qn = tgts[k].qualified_name; size_t qn_len = qn ? strlen(qn) : 0; found = edges[j].target_id == tgts[k].id && tgts[k].label && strcmp(tgts[k].label, "Field") == 0 && qn_len >= suffix_len && strcmp(qn + qn_len - suffix_len, suffix) == 0; } } if (edges) { cbm_store_free_edges(edges, ec); } } if (srcs) { cbm_store_free_nodes(srcs, sc); } if (tgts) { cbm_store_free_nodes(tgts, tc); } return found; } /* Number of edges of edge_type that END on the Field `field` owned by `owner`, * whatever their source: the call probes below must hold for a caller in any * language, including a shell script whose source node has no stable name. */ static int field_inbound_edge_count(cbm_store_t *s, const char *project, const char *owner, const char *field, const char *edge_type) { char suffix[256]; snprintf(suffix, sizeof(suffix), ".%s.%s", owner, field); size_t suffix_len = strlen(suffix); cbm_node_t *tgts = NULL; int tc = 0; cbm_store_find_nodes_by_name(s, project, field, &tgts, &tc); int total = 0; for (int k = 0; k < tc; k++) { const char *qn = tgts[k].qualified_name; size_t qn_len = qn ? strlen(qn) : 0; if (!tgts[k].label || strcmp(tgts[k].label, "Field") != 0 || qn_len < suffix_len || strcmp(qn + qn_len - suffix_len, suffix) != 0) { continue; } cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_target_type(s, tgts[k].id, edge_type, &edges, &ec); total += ec; if (edges) { cbm_store_free_edges(edges, ec); } } if (tgts) { cbm_store_free_nodes(tgts, tc); } return total; } /* Fixture for the C member-access probes: two structs share the field name * `count`, and each function touches exactly one of them. A bare-name lookup * of `count` can only pick one Field for both functions; only the type of the * object expression says which struct is meant. `use_opaque` reaches `count` * through a type the project never defines, so no Field may be bound at all; * `beta_only` through that same untyped object is the one Field of that name * in the project, so nothing has to be picked and it binds. * * Calls follow the same rule. The structs carry a function-pointer member * `notify` (declared through a typedef: a member spelled `void (*notify)(int)` * has no Field node today, a separate extraction gap). `use_remote` calls * Beta's `notify` from a file that declares its own struct with a `notify` * member -- the candidate a bare-name lookup prefers, being in the same * module. `call_param` calls a PARAMETER named like the field `alpha_only`, * and `tool.sh` runs a command of that name: neither calls a struct member. */ static void write_c_member_field_fixture(const char *tmp, int pad_files) { write_temp_file(tmp, "alpha.h", "#ifndef ALPHA_H\n" "#define ALPHA_H\n" "typedef void (*alpha_notify_fn)(int);\n" "struct Alpha {\n" " int count;\n" " int alpha_only;\n" " alpha_notify_fn notify;\n" "};\n" "#endif\n"); write_temp_file(tmp, "beta.h", "#ifndef BETA_H\n" "#define BETA_H\n" "typedef void (*beta_notify_fn)(int);\n" "typedef struct {\n" " int count;\n" " int beta_only;\n" " beta_notify_fn notify;\n" "} Beta;\n" "#endif\n"); write_temp_file(tmp, "use_alpha.c", "#include \"alpha.h\"\n" "\n" "int use_alpha(struct Alpha *a) {\n" " a->count = 1;\n" " return a->count + a->alpha_only;\n" "}\n" "\n" "int call_param(int (*alpha_only)(void)) {\n" " return alpha_only();\n" "}\n"); write_temp_file(tmp, "mix.c", "#include \"beta.h\"\n" "\n" "typedef void (*local_notify_fn)(int);\n" "struct Local {\n" " local_notify_fn notify;\n" "};\n" "\n" "void use_remote(Beta b) {\n" " b.notify(3);\n" "}\n"); write_temp_file(tmp, "tool.sh", "#!/bin/sh\n" "alpha_only --check\n"); write_temp_file(tmp, "use_beta.c", "#include \"beta.h\"\n" "\n" "int use_beta(Beta b) {\n" " b.count = 2;\n" " return b.count + b.beta_only;\n" "}\n"); write_temp_file(tmp, "use_opaque.c", "struct Opaque;\n" "\n" "int use_opaque(struct Opaque *o) {\n" " return o->count + o->beta_only;\n" "}\n"); /* Inline functions in a header. A `.h` is parsed as C++, and its member * accesses mean what they mean in the .c files that include it. */ write_temp_file(tmp, "inline.h", "#ifndef INLINE_H\n" "#define INLINE_H\n" "#include \"alpha.h\"\n" "#include \"beta.h\"\n" "static inline int hdr_alpha(struct Alpha *a) {\n" " return a->count;\n" "}\n" "static inline int hdr_beta(Beta *b) {\n" " b->count = 1;\n" " return b->count;\n" "}\n" "/* A struct the header itself declares, through a typedef. */\n" "typedef struct {\n" " int count;\n" " char *buf;\n" "} HdrBuf;\n" "struct HdrNode {\n" " int count;\n" "};\n" "static inline int hdr_own_typedef(HdrBuf *s) {\n" " return s->count;\n" "}\n" "static inline int hdr_own_struct(struct HdrNode *n) {\n" " return n->count;\n" "}\n" "#endif\n"); /* Functions inside preprocessor branches of a .c file are functions like * any other. */ write_temp_file(tmp, "guarded.c", "#include \"alpha.h\"\n" "#include \"beta.h\"\n" "\n" "#ifdef SOME_CONFIG\n" "int guarded_alpha(struct Alpha *a) {\n" " return a->count;\n" "}\n" "#else\n" "int guarded_beta(Beta *b) {\n" " return b->count;\n" "}\n" "#endif\n"); /* A member call on an object nothing types. In C the one member of that * name in the project may still bind; in a C++ source such a call is a * method of some type the project does not hold, and binds nothing. */ write_temp_file(tmp, "cb.h", "#ifndef CB_H\n" "#define CB_H\n" "struct CbTable {\n" " int (*only_cb)(int);\n" "};\n" "#endif\n"); write_temp_file(tmp, "untyped_c.c", "struct Hidden;\n" "\n" "int c_untyped_call(struct Hidden *h) {\n" " return h->only_cb(1);\n" "}\n"); write_temp_file(tmp, "untyped_cpp.cpp", "template int cpp_untyped_call(T *h) {\n" " return h->only_cb(1);\n" "}\n"); /* A global variable declared behind an `#ifndef` types the accesses made * through it, whether its type is spelled with `struct` or a typedef. */ write_temp_file(tmp, "globals.c", "#include \"alpha.h\"\n" "#include \"beta.h\"\n" "\n" "#ifndef NO_GLOBALS\n" "static struct Alpha g_alpha;\n" "static Beta g_beta;\n" "#endif\n" "\n" "int read_alpha_global(void) {\n" " return g_alpha.count;\n" "}\n" "\n" "int read_beta_global(void) {\n" " return g_beta.count;\n" "}\n"); /* Ops tables: pointer-to-function members written inline, called through * `->`. Two tables share both member names, so only the object's type * says which is called; `arrow_chain` reaches its table through a member * whose type is spelled `struct Ops *`. */ write_temp_file(tmp, "ops.h", "#ifndef OPS_H\n" "#define OPS_H\n" "struct Dev;\n" "struct Ops {\n" " void (*open)(int fd);\n" " int (*shut)(int fd);\n" " struct Dev *(*owner)(int fd);\n" "};\n" "struct Dev {\n" " struct Ops *ops;\n" " int id;\n" "};\n" "struct Bank {\n" " struct Dev *devs, *spare;\n" " struct Dev fixed[2];\n" " int id;\n" "};\n" "#endif\n"); write_temp_file(tmp, "other_ops.h", "#ifndef OTHER_OPS_H\n" "#define OTHER_OPS_H\n" "struct OtherOps {\n" " void (*open)(int fd);\n" " int (*shut)(int fd);\n" " int id;\n" "};\n" "#endif\n"); write_temp_file(tmp, "use_ops.c", "#include \"ops.h\"\n" "#include \"other_ops.h\"\n" "\n" "void arrow_ops(struct Ops *o) {\n" " o->open(1);\n" "}\n" "\n" "void arrow_other(struct OtherOps *x) {\n" " x->open(2);\n" "}\n" "\n" "int arrow_chain(struct Dev *d) {\n" " return d->ops->shut(3);\n" "}\n" "\n" "int arrow_ret(struct Ops *o) {\n" " return o->owner(4)->id;\n" "}\n" "\n" "int index_ptr(struct Bank *b) {\n" " return b->devs[1].id;\n" "}\n" "\n" "int index_arr(struct Bank *b) {\n" " return b->fixed[0].id;\n" "}\n" "\n" "int second_decl(struct Bank *b) {\n" " return b->spare->id;\n" "}\n"); /* Designated initializers name members with no object expression at all: * the type being initialized says whose member `.open` is. At file scope * (the usual ops table), nested, through an array, in a block and in a * compound literal. Each table file initializes ONE of the two tables. */ write_temp_file(tmp, "table_ops.c", "#include \"ops.h\"\n" "\n" "static void my_open(int fd) { (void)fd; }\n" "static int my_shut(int fd) { return fd; }\n" "\n" "static struct Ops table_ops = {\n" " .open = my_open,\n" " .shut = my_shut,\n" "};\n" "\n" "static struct Bank bank_init = {\n" " .fixed = { [0] = { .ops = &table_ops } },\n" " .id = 3,\n" "};\n"); write_temp_file(tmp, "table_other.c", "#include \"other_ops.h\"\n" "\n" "static void their_open(int fd) { (void)fd; }\n" "\n" "static struct OtherOps other_table = {\n" " .open = their_open,\n" "};\n"); write_temp_file(tmp, "init_local.c", "#include \"ops.h\"\n" "#include \"other_ops.h\"\n" "\n" "int init_block(void) {\n" " struct Dev d = { .id = 1 };\n" " return d.id;\n" "}\n" "\n" "void init_literal(struct Bank *b) {\n" " *b->devs = (struct Dev){ .id = 2 };\n" "}\n" "\n" "void init_array(void) {\n" " struct OtherOps pair[2] = { { .id = 1 }, { .id = 2 } };\n" " (void)pair;\n" "}\n"); for (int i = 0; i < pad_files; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "pad/filler%d.c", i); snprintf(body, sizeof(body), "int filler%d(void) { return %d; }\n", i, i); write_temp_file(tmp, name, body); } } /* Shared assertions for the sequential and the parallel twin. */ static int assert_c_member_fields_bind_by_type(cbm_store_t *s, const char *project) { /* The probe means nothing unless both same-named fields exist. */ ASSERT_GTE(fixture_node_count(s, project, "alpha.h", "count", "Field"), 1); ASSERT_GTE(fixture_node_count(s, project, "beta.h", "count", "Field"), 1); /* Reproduce-first: RED while `count` resolves by bare name — both * functions then bind the SAME Field, so one of each pair is wrong. */ ASSERT_TRUE(field_edge_exists(s, project, "use_alpha", "Alpha", "count", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "use_alpha", "Beta", "count", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "use_beta", "Beta", "count", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "use_beta", "Alpha", "count", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "use_alpha", "Alpha", "count", "WRITES")); ASSERT_FALSE(field_edge_exists(s, project, "use_alpha", "Beta", "count", "WRITES")); ASSERT_TRUE(field_edge_exists(s, project, "use_beta", "Beta", "count", "WRITES")); ASSERT_FALSE(field_edge_exists(s, project, "use_beta", "Alpha", "count", "WRITES")); /* A field whose name is unique keeps its edge. */ ASSERT_TRUE(field_edge_exists(s, project, "use_alpha", "Alpha", "alpha_only", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "use_beta", "Beta", "beta_only", "USAGE")); /* An object whose type the project never defines binds no Field by a * name several structs share... */ ASSERT_FALSE(field_edge_exists(s, project, "use_opaque", "Alpha", "count", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "use_opaque", "Beta", "count", "USAGE")); /* ...and still binds a member name the project holds exactly once. */ ASSERT_TRUE(field_edge_exists(s, project, "use_opaque", "Beta", "beta_only", "USAGE")); /* A call through a function-pointer member lands on the member of its * object's type, not on the same-named member its own file declares. */ ASSERT_GTE(fixture_node_count(s, project, "mix.c", "notify", "Field"), 1); ASSERT_TRUE(field_edge_exists(s, project, "use_remote", "Beta", "notify", "CALLS")); ASSERT_FALSE(field_edge_exists(s, project, "use_remote", "Local", "notify", "CALLS")); /* A bare C call of a parameter named `alpha_only` and a shell command of * that name are not calls of the struct member, however unique the name. */ ASSERT_EQ(field_inbound_edge_count(s, project, "Alpha", "alpha_only", "CALLS"), 0); /* `p->f(...)` through an inline pointer-to-function member: the call lands * on the member of the pointed-to struct, never on the same-named member * of the other table. */ ASSERT_GTE(fixture_node_count(s, project, "ops.h", "open", "Field"), 1); ASSERT_GTE(fixture_node_count(s, project, "other_ops.h", "open", "Field"), 1); ASSERT_TRUE(field_edge_exists(s, project, "arrow_ops", "Ops", "open", "CALLS")); ASSERT_FALSE(field_edge_exists(s, project, "arrow_ops", "OtherOps", "open", "CALLS")); ASSERT_TRUE(field_edge_exists(s, project, "arrow_other", "OtherOps", "open", "CALLS")); ASSERT_FALSE(field_edge_exists(s, project, "arrow_other", "Ops", "open", "CALLS")); /* The object of the call is itself a member access whose member is typed * `struct Ops *`: both steps of the chain are typed. */ ASSERT_TRUE(field_edge_exists(s, project, "arrow_chain", "Dev", "ops", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "arrow_chain", "Ops", "shut", "CALLS")); ASSERT_FALSE(field_edge_exists(s, project, "arrow_chain", "OtherOps", "shut", "CALLS")); /* A call through a pointer-to-function member has that member's return * type: `o->owner(4)->id` is the `id` of struct Dev. */ ASSERT_TRUE(field_edge_exists(s, project, "arrow_ret", "Ops", "owner", "CALLS")); ASSERT_TRUE(field_edge_exists(s, project, "arrow_ret", "Dev", "id", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "arrow_ret", "OtherOps", "id", "USAGE")); /* A pointer member and an array member have an element type, and the * second declarator of `struct Dev *devs, *spare;` is a member too. `id` * is a name three structs share, so each of these needs the full chain. */ ASSERT_TRUE(field_edge_exists(s, project, "index_ptr", "Bank", "devs", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "index_ptr", "Dev", "id", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "index_ptr", "Bank", "id", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "index_arr", "Bank", "fixed", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "index_arr", "Dev", "id", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "second_decl", "Bank", "spare", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "second_decl", "Dev", "id", "USAGE")); /* The same rule inside a header's inline functions. */ ASSERT_TRUE(field_edge_exists(s, project, "hdr_alpha", "Alpha", "count", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "hdr_alpha", "Beta", "count", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "hdr_beta", "Beta", "count", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "hdr_beta", "Alpha", "count", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "hdr_beta", "Beta", "count", "WRITES")); ASSERT_FALSE(field_edge_exists(s, project, "hdr_beta", "Alpha", "count", "WRITES")); /* A struct the header declares itself, by typedef or by tag. */ ASSERT_TRUE(field_edge_exists(s, project, "hdr_own_typedef", "HdrBuf", "count", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "hdr_own_typedef", "Alpha", "count", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "hdr_own_struct", "HdrNode", "count", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "hdr_own_struct", "Beta", "count", "USAGE")); /* ...and inside `#ifdef` / `#else` branches. */ ASSERT_TRUE(field_edge_exists(s, project, "guarded_alpha", "Alpha", "count", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "guarded_alpha", "Beta", "count", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "guarded_beta", "Beta", "count", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "guarded_beta", "Alpha", "count", "USAGE")); /* An untyped member call binds the project's one member of that name in * C, and nothing in a C++ source. */ ASSERT_GTE(fixture_node_count(s, project, "cb.h", "only_cb", "Field"), 1); ASSERT_TRUE(field_edge_exists(s, project, "c_untyped_call", "CbTable", "only_cb", "CALLS")); ASSERT_FALSE(field_edge_exists(s, project, "cpp_untyped_call", "CbTable", "only_cb", "CALLS")); /* A global behind `#ifndef` types the accesses through it. */ ASSERT_TRUE(field_edge_exists(s, project, "read_alpha_global", "Alpha", "count", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "read_alpha_global", "Beta", "count", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "read_beta_global", "Beta", "count", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "read_beta_global", "Alpha", "count", "USAGE")); /* Designated initializers at file scope: the source is the file. */ ASSERT_TRUE(field_edge_exists(s, project, "table_ops.c", "Ops", "open", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "table_ops.c", "Ops", "shut", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "table_ops.c", "OtherOps", "open", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "table_other.c", "OtherOps", "open", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "table_other.c", "Ops", "open", "USAGE")); /* Nested: `.fixed = { [0] = { .ops = ... } }` initializes a Dev. */ ASSERT_TRUE(field_edge_exists(s, project, "table_ops.c", "Bank", "fixed", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "table_ops.c", "Dev", "ops", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "table_ops.c", "Bank", "id", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "table_ops.c", "Dev", "id", "USAGE")); /* In a block, in a compound literal, and per element of an array. */ ASSERT_TRUE(field_edge_exists(s, project, "init_block", "Dev", "id", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "init_block", "Bank", "id", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "init_literal", "Dev", "id", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "init_literal", "OtherOps", "id", "USAGE")); ASSERT_TRUE(field_edge_exists(s, project, "init_array", "OtherOps", "id", "USAGE")); ASSERT_FALSE(field_edge_exists(s, project, "init_array", "Dev", "id", "USAGE")); return 0; } TEST(pipeline_c_member_access_binds_field_by_object_type) { /* `x.f` / `x->f` handed only the member name `f` to the short-name * registry, which returned whichever Field was called `f`: 9,150 USAGE * and 2,810 WRITES edges onto Fields on this repository, 3,976 of the * USAGE edges onto a name several structs share. Sequential-path twin. */ char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_c_field_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_c_member_field_fixture(tmp, 0); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/c_field.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); int rc = assert_c_member_fields_bind_by_type(s, project); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); if (rc != 0) { return rc; } PASS(); } TEST(pipeline_c_member_access_binds_field_by_object_type_parallel) { /* Parallel twin: resolve_file_usages / resolve_file_rw are independent * resolvers and must take the same typed join (#1928's lesson). */ char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_c_fieldp_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_c_member_field_fixture(tmp, 52); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/c_fieldp.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); int rc = assert_c_member_fields_bind_by_type(s, project); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); if (rc != 0) { return rc; } PASS(); } /* Count nodes with the given exact name in the project (e.g. a Route path). */ static int count_nodes_named(cbm_store_t *s, const char *project, const char *name) { cbm_node_t *ns = NULL; int n = 0; cbm_store_find_nodes_by_name(s, project, name, &ns, &n); if (ns) { cbm_store_free_nodes(ns, n); } return n; } /* Parallel-resolver regression for the TS/JS receiver guard (>= 50 files forces * pass_parallel.c's resolve_file_calls). The guard must not drop a weak member * match before the service classification runs — it suppresses ONLY the plain * CALLS fall-through, so every service edge (HTTP_CALLS via the #523 callee * bypass or emit_service_edge's unconditional detect_url_in_args, Route via the * ROUTE_REG fall-through, …) is emitted exactly as on main. These callees are * classified by main's verb-suffix + URL-arg heuristic, NOT by an HTTP library * name in the callee — a duplicated predicate keyed on the resolved QN lost them * (axios.get, api.patch on a renamed-axios instance, supertest request(app).get). * The regex false edge must stay suppressed in parallel too. CBM_WORKERS forces * >1 worker so the parallel path is taken regardless of the host core count. */ TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_tsjs_par_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } /* The lone project symbols named get()/patch()/test() — weak short-name * targets the registry mis-binds the member calls below to. */ write_temp_file(tmp, "src/thing.ts", "export class ApiThing {\n" " get(): number {\n" " return 1;\n" " }\n" " patch(): number {\n" " return 2;\n" " }\n" " load(): number {\n" " return 3;\n" " }\n" "}\n" "export class Other {\n" " test(): boolean {\n" " return true;\n" " }\n" "}\n"); /* Untyped axios: the HTTP signal is in the callee name, not the resolved QN * (the registry mis-binds "get" to ApiThing.get). Must yield HTTP_CALLS. */ write_temp_file(tmp, "src/http.ts", "export function callApi() {\n" " return axios.get('/api/orders');\n" "}\n"); /* Renamed axios instance `api`: no HTTP lib id in the callee — classified by * the .patch verb suffix + URL arg. Must yield HTTP_CALLS (was lost when the * exemption keyed on the resolved QN only). */ write_temp_file(tmp, "src/http2.ts", "export function callPatch(): unknown {\n" " return api.patch('/plans/:id', {});\n" "}\n"); /* Supertest-style chained receiver `request(app).get('/y')` — untyped, verb * suffix + URL arg. */ write_temp_file(tmp, "src/supertest.ts", "export function callSup(app: unknown): unknown {\n" " return request(app).get('/y');\n" "}\n"); /* `dev.load('/data')`: `.load` is NOT a route suffix and `dev` is not an HTTP * lib, so main classifies it via the unconditional detect_url_in_args (URL * arg) -> HTTP_CALLS, while the weak plain match to ApiThing.load is the false * edge that must be suppressed. This is exactly the detect_url_in_args path * the previous (predicate-duplicating) guard skipped by dropping the call * before emit_service_edge ran — the class of ~399 HTTP_CALLS it lost. */ write_temp_file(tmp, "src/load.ts", "export function callLoad(dev: unknown): unknown {\n" " return dev.load('/api/data');\n" "}\n"); /* Route registration by callee suffix + a '/'-path arg. Must yield a Route. */ write_temp_file(tmp, "src/routes.ts", "function handler() {}\n" "export function reg(router: any) {\n" " router.get('/users', handler);\n" "}\n"); /* Regex receiver: the false edge that must stay suppressed in parallel too. */ write_temp_file(tmp, "src/re.ts", "const re = /^[a-z]+$/;\n" "export function checkFormat(s: string): boolean {\n" " return re.test(s);\n" "}\n"); /* Pad past MIN_FILES_FOR_PARALLEL (50) so the parallel resolver runs. */ for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/filler%d.ts", i); snprintf(body, sizeof(body), "export function filler%d(): number {\n return %d;\n}\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); /* force parallel regardless of host cores */ char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/tsjs_par.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* (1) Genuine HTTP_CALLS survive under the guard: * - axios.get('/api/orders') -> 1 edge, typed GET (recognized lib #523 * callee bypass; no method-less arg-URL twin), and * - dev.load('/api/data') -> 1 edge via detect_url_in_args, which runs * after emit_service_edge's branch even when the plain fall-through is * suppressed. * dev.load is the class the predicate-duplicating guard lost: `.load` is not * a route suffix and `dev` is not an HTTP lib, so it was dropped before * emit_service_edge ran (RED on that guard: only axios's edge). */ ASSERT_EQ(http_calls_to_route(s, project, "callApi", "__route__GET__"), 1); ASSERT_EQ(http_calls_to_route(s, project, "callApi", "__route__"), 1); ASSERT_EQ(http_calls_to_route(s, project, "callLoad", "__route__ANY__"), 1); ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 2); /* (2) The verb-suffix + route-path member calls keep their route * registrations (edge type CALLS -> a Route node named by the path). These * classify as route_registration on main, NOT HTTP_CALLS — Option A preserves * that by construction. Assert the three Route paths survive: * api.patch('/plans/:id'), request(app).get('/y'), router.get('/users'). */ ASSERT_GTE(count_nodes_named(s, project, "/plans/:id"), 1); ASSERT_GTE(count_nodes_named(s, project, "/y"), 1); ASSERT_GTE(count_nodes_named(s, project, "/users"), 1); /* (3) The false plain-CALLS edges are suppressed in parallel: the regex * receiver and the dev.load weak match to ApiThing.load. */ ASSERT_GTE(fixture_node_count(s, project, "src/re.ts", "checkFormat", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "src/thing.ts", "test", "Method"), 1); ASSERT_GTE(fixture_node_count(s, project, "src/load.ts", "callLoad", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "src/thing.ts", "load", "Method"), 1); ASSERT_FALSE(cross_file_call_exists(s, project, "checkFormat", "test")); ASSERT_FALSE(cross_file_call_exists(s, project, "callLoad", "load")); cbm_store_close(s); cbm_pipeline_free(p); if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); PASS(); } /* Route-registration CALLS edges (via=route_registration) out of the function * `source_name`. */ static int route_registrations_from(cbm_store_t *s, const char *project, const char *source_name) { cbm_edge_t *edges = NULL; int edge_count = 0; if (cbm_store_find_edges_by_type(s, project, "CALLS", &edges, &edge_count) != CBM_STORE_OK) { return -1; } int matches = 0; for (int i = 0; i < edge_count; i++) { cbm_node_t source = {0}; int source_ok = cbm_store_find_node_by_id(s, edges[i].source_id, &source) == CBM_STORE_OK; if (source_ok && source.name && strcmp(source.name, source_name) == 0 && edges[i].properties_json && strstr(edges[i].properties_json, "\"via\":\"route_registration\"")) { matches++; } cbm_node_free_fields(&source); } if (edges) { cbm_store_free_edges(edges, edge_count); } return matches; } /* `import Axios from "axios"` -- the default-import binding capitalized, a * common convention -- must classify `Axios.get/post/delete(url)` as HTTP * client calls exactly like `axios.*`. The library table matched only the * lowercase id, so the calls fell through to the `.get`/`.post` route-suffix * fallback and became route REGISTRATIONS: on * microsoft/Game-Control-Puzzle-Event-Administration-Tools all 69 client * calls masqueraded as server Routes. A real Express registration in the same * project stays a registration. `fillers` > 0 forces the parallel resolver, * 0 keeps the sequential one; both classify through the same table. */ static int capitalized_axios_case(int fillers) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_axios_cap_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "src/service.ts", "import Axios from \"axios\";\n" "export function getSettings(eventId: string) {\n" " return Axios.get(`/api/admin/events/${eventId}/settings`);\n" "}\n" "export function addUser(eventId: string, participant: unknown) {\n" " return Axios.post(`/api/admin/users/${eventId}`, participant);\n" "}\n" "export function removeUser(id: string) {\n" " return Axios.delete(`/api/admin/users/${id}`);\n" "}\n"); write_temp_file(tmp, "src/server.ts", "function listUsers(req: unknown, res: unknown) {}\n" "export function mount(router: any) {\n" " router.get('/api/admin/users', listUsers);\n" "}\n"); for (int i = 0; i < fillers; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/filler%d.ts", i); snprintf(body, sizeof(body), "export function filler%d(): number {\n return %d;\n}\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/axios_cap.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); static const struct { const char *caller; const char *typed_route; } client[] = {{"getSettings", "__route__GET__"}, {"addUser", "__route__POST__"}, {"removeUser", "__route__DELETE__"}}; for (size_t i = 0; i < sizeof(client) / sizeof(client[0]); i++) { ASSERT_EQ(route_registrations_from(s, project, client[i].caller), 0); ASSERT_EQ(http_calls_to_route(s, project, client[i].caller, client[i].typed_route), 1); } /* Control: the server-side registration is untouched. */ ASSERT_EQ(route_registrations_from(s, project, "mount"), 1); ASSERT_EQ(http_calls_to_route(s, project, "mount", "__route__"), 0); cbm_store_close(s); cbm_pipeline_free(p); if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); PASS(); } TEST(pipeline_capitalized_axios_import_is_http_client) { return capitalized_axios_case(52); } TEST(pipeline_capitalized_axios_import_is_http_client_sequential) { return capitalized_axios_case(0); } /* FastAPI/Starlette WebSocket route registration (distilled from #1245). The * route-registration suffix table knew `.add_api_websocket_route` but not the * decorator forms `.websocket` / `.websocket_route`, so a WebSocket endpoint * registered via `@router.websocket('/ws')` produced no Route node at all — * the endpoint was invisible to the graph. `@app.get('/health')` is the * positive control (already a route suffix). Fewer than 50 files exercises * pass_calls.c; the same suffix table drives the parallel resolver. */ TEST(pipeline_python_websocket_decorators_register_routes) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_ws_routes_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "app.py", "from fastapi import APIRouter, FastAPI, WebSocket\n\n" "app = FastAPI()\n" "router = APIRouter()\n\n" "@app.get('/health')\n" "def health():\n" " return {'ok': True}\n\n" "@router.websocket('/ws')\n" "async def ws_endpoint(websocket: WebSocket):\n" " await websocket.accept()\n\n" "@app.websocket_route('/events')\n" "async def events_endpoint(websocket: WebSocket):\n" " await websocket.accept()\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/py_ws.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* Positive control: the HTTP decorator mints its Route node. */ ASSERT_GTE(count_nodes_named(s, project, "/health"), 1); /* The two WebSocket decorator forms must mint Route nodes too. */ ASSERT_GTE(count_nodes_named(s, project, "/ws"), 1); ASSERT_GTE(count_nodes_named(s, project, "/events"), 1); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* Python bare-call local-binding suppression, sequential path. The bare-call * counterpart of the receiver guard above: `run` is a PARAMETER, so `run()` * cannot be the module-level `run` and must not bind SatoriLive.run. * * The positive control is deliberately a CROSS-FILE bare call with no import, * so it resolves by a weak short-name strategy — one this guard could have * killed. Asserting a same-file (same_module) edge instead would prove nothing, * because no guard in this codebase touches same_module for any input. * Fewer than 50 files exercises pass_calls.c. */ TEST(pipeline_python_bare_local_binding_suppresses_weak_edge) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_bare_seq_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "live.py", "class SatoriLive:\n" " def run(self):\n" " return 1\n"); write_temp_file(tmp, "helpers.py", "def compute_widget_total():\n" " return 7\n"); write_temp_file(tmp, "gate.py", "def _run_with_heavy_slot(run):\n" " return run()\n" "\n" "def uses_free_function():\n" " return compute_widget_total()\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/py_bare.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* NEGATIVE: the callee is shadowed by a parameter. */ ASSERT_FALSE(cross_file_call_exists(s, project, "_run_with_heavy_slot", "run")); /* POSITIVE: an unshadowed cross-file bare call survives. */ ASSERT_TRUE(cross_file_call_exists(s, project, "uses_free_function", "compute_widget_total")); /* Tripwire: a run that emitted no edges at all would satisfy the negative * assertion vacuously. */ ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "CALLS"), 1); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* Parallel Python regression for #1276. The field-type heuristic capitalizes * the receiver token and previously promoted accelerator.print() to * MockAccelerator.print at 0.85; ordinary suffix matching also selected one * arbitrary backward()/step() implementation. All are unresolved-receiver * calls and must remain absent, while the import-bound call and the bare local * call remain — the same both-directions pin as the sequential test above. * >= 50 files forces pass_parallel.c and try_field_type_hint(), so this is also * the guard against a sequential/parallel divergence. */ TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_python_par_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "targets.py", "class MockAccelerator:\n" " def print(self, value):\n" " return value\n" "\n" "class OtherPrinter:\n" " def print(self, value):\n" " return value\n" "\n" "class FlashAttentionFunction:\n" " def backward(self, loss):\n" " return loss\n" "\n" "class OtherBackward:\n" " def backward(self, loss):\n" " return loss\n" "\n" "class EulerScheduler:\n" " def step(self):\n" " return 1\n" "\n" "class OtherScheduler:\n" " def step(self):\n" " return 1\n"); write_temp_file(tmp, "pkg/__init__.py", "from .helper import compute\n"); write_temp_file(tmp, "pkg/helper.py", "def compute(value):\n" " return value * 2\n"); write_temp_file(tmp, "caller.py", "from pkg import helper\n" "\n" "def local_helper():\n" " return 1\n" "\n" "def train(accelerator, trainer):\n" " accelerator.print('hello')\n" " accelerator.backward(1)\n" " trainer.lr_scheduler.step()\n" " helper.compute(42)\n" " return local_helper()\n"); for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "filler%d.py", i); snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/python_par.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* NEGATIVE: every receiver here is a parameter or an attribute of one. */ ASSERT_GTE(fixture_node_count(s, project, "caller.py", "train", "Function"), 1); ASSERT_GTE(fixture_node_count(s, project, "targets.py", "print", "Method"), 2); ASSERT_GTE(fixture_node_count(s, project, "targets.py", "backward", "Method"), 2); ASSERT_GTE(fixture_node_count(s, project, "targets.py", "step", "Method"), 2); ASSERT_FALSE(cross_file_call_exists(s, project, "train", "print")); ASSERT_FALSE(cross_file_call_exists(s, project, "train", "backward")); ASSERT_FALSE(cross_file_call_exists(s, project, "train", "step")); /* POSITIVE: import-bound and bare local calls survive the parallel path too. */ ASSERT_TRUE(cross_file_call_exists(s, project, "train", "compute")); ASSERT_TRUE(cross_file_call_exists(s, project, "train", "local_helper")); cbm_store_close(s); cbm_pipeline_free(p); if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); PASS(); } /* Parallel counterpart. >= 50 files forces pass_parallel.c, which is wired with * the same gate: a guard wired on only one resolver produces an edge on the * sequential path and not the parallel one, breaking MT determinism. #1386 * wired both and tested only the sequential path, and the `parallel` suite is * exactly what catches that. Same both-directions pin as the sequential test. */ TEST(pipeline_python_bare_local_binding_parallel_suppresses_weak_edge) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_bare_par_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "live.py", "class SatoriLive:\n" " def run(self):\n" " return 1\n" "\n" "class BatchJob:\n" " def execute(self):\n" " return 2\n"); write_temp_file(tmp, "helpers.py", "def compute_widget_total():\n" " return 7\n"); write_temp_file(tmp, "gate.py", "def _run_with_heavy_slot(run, execute):\n" " run()\n" " return execute()\n" "\n" "def uses_free_function():\n" " return compute_widget_total()\n"); for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "filler%d.py", i); snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/py_bare_par.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* NEGATIVE: both callees are shadowed by parameters. */ ASSERT_FALSE(cross_file_call_exists(s, project, "_run_with_heavy_slot", "run")); ASSERT_FALSE(cross_file_call_exists(s, project, "_run_with_heavy_slot", "execute")); /* POSITIVE: the unshadowed cross-file bare call survives the parallel path. */ ASSERT_TRUE(cross_file_call_exists(s, project, "uses_free_function", "compute_widget_total")); /* Tripwire against a vacuous pass. */ ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "CALLS"), 1); cbm_store_close(s); cbm_pipeline_free(p); if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); PASS(); } /* CALLS edges from a caller to a callee, both matched by QN suffix (start each * suffix with '.' to anchor it at a segment boundary), any strategy. *guessed * is set when a matching edge came from a name guess rather than an import * binding or the LSP. */ static int py_calls_between(cbm_store_t *s, const char *project, const char *caller_suffix, const char *callee_suffix, bool *guessed) { cbm_edge_t *edges = NULL; int edge_count = 0; cbm_store_find_edges_by_type(s, project, "CALLS", &edges, &edge_count); int hits = 0; size_t cs = strlen(caller_suffix); size_t ts = strlen(callee_suffix); for (int i = 0; i < edge_count; i++) { cbm_node_t src = {0}; cbm_node_t tgt = {0}; if (cbm_store_find_node_by_id(s, edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK) { size_t sl = strlen(src.qualified_name); size_t tl = strlen(tgt.qualified_name); if (sl >= cs && tl >= ts && strcmp(src.qualified_name + sl - cs, caller_suffix) == 0 && strcmp(tgt.qualified_name + tl - ts, callee_suffix) == 0) { hits++; const char *pj = edges[i].properties_json ? edges[i].properties_json : ""; if (guessed && !strstr(pj, "\"import_map\"") && !strstr(pj, "\"lsp")) { *guessed = true; } } } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (edges) cbm_store_free_edges(edges, edge_count); return hits; } /* Pin CBM_WORKERS=4 for a padded (> 50 files) parallel run; restore after. */ typedef struct { char *saved; bool had; int rc; } py_workers_pin_t; static py_workers_pin_t py_pin_workers(bool parallel) { py_workers_pin_t pin = {0}; if (!parallel) { return pin; } const char *old = getenv("CBM_WORKERS"); pin.had = old != NULL; pin.saved = old ? strdup(old) : NULL; pin.rc = (old && !pin.saved) ? -1 : cbm_setenv("CBM_WORKERS", "4", 1); return pin; } static int py_unpin_workers(bool parallel, py_workers_pin_t *pin) { if (!parallel) { return 0; } int rc = pin->had ? cbm_setenv("CBM_WORKERS", pin->saved, 1) : cbm_unsetenv("CBM_WORKERS"); free(pin->saved); pin->saved = NULL; return rc; } static void py_write_padding(const char *tmp, const char *stem, bool parallel) { for (int i = 0; parallel && i < 52; i++) { char name[64]; char body[96]; snprintf(name, sizeof(name), "%s%d.py", stem, i); snprintf(body, sizeof(body), "def %s%d():\n return %d\n", stem, i, i); write_temp_file(tmp, name, body); } } /* `from . import x`, `from .. import x` and `from . import x as y` name the * importing file's own (or parent) package. The extractor recorded them one * package too high ("..x"), so the import fell to pass_pkgmap's symbol-name * guess -- the lexicographically first same-named project definition -- and * every call became a confident edge into comp.aaa, through the import map and * the Python LSP's alias binding alike (home-assistant: 7,175 call pairs). * Both resolvers: <= 50 files sequential, padded > 50 parallel. */ static int check_python_package_relative_imports(bool parallel) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_relpkg_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "comp/__init__.py", ""); write_temp_file(tmp, "comp/aaa/__init__.py", "def async_register(hass):\n" " return 1\n\n" "def local_fn():\n" " return 2\n"); write_temp_file(tmp, "comp/web/__init__.py", "def async_register(hass):\n" " return 3\n"); write_temp_file(tmp, "comp/web/trigger.py", "from . import async_register\n\n" "def attach(hass):\n" " return async_register(hass)\n"); write_temp_file(tmp, "comp/web/sub/__init__.py", "def local_fn():\n" " return 4\n"); write_temp_file(tmp, "comp/web/sub/mod.py", "from .. import async_register\n" "from . import local_fn as lf\n\n" "def deep(hass):\n" " async_register(hass)\n" " return lf()\n"); py_write_padding(tmp, "relpad", parallel); py_workers_pin_t pin = py_pin_workers(parallel); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/relpkg.db", tmp); cbm_pipeline_t *p = pin.rc == 0 ? cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL) : NULL; int run_rc = p ? cbm_pipeline_run(p) : -1; cbm_store_t *s = run_rc == 0 ? cbm_store_open_path(db_path) : NULL; bool opened = s != NULL; bool guessed = false; int own = -1, own_decoy = -1, parent = -1, parent_decoy = -1, alias = -1, alias_decoy = -1; if (s) { const char *project = cbm_pipeline_project_name(p); own = py_calls_between(s, project, ".comp.web.trigger.attach", ".comp.web.async_register", &guessed); own_decoy = py_calls_between(s, project, ".comp.web.trigger.attach", ".comp.aaa.async_register", NULL); parent = py_calls_between(s, project, ".comp.web.sub.mod.deep", ".comp.web.async_register", &guessed); parent_decoy = py_calls_between(s, project, ".comp.web.sub.mod.deep", ".comp.aaa.async_register", NULL); alias = py_calls_between(s, project, ".comp.web.sub.mod.deep", ".comp.web.sub.local_fn", &guessed); alias_decoy = py_calls_between(s, project, ".comp.web.sub.mod.deep", ".comp.aaa.local_fn", NULL); cbm_store_close(s); } cbm_pipeline_free(p); int unpin_rc = py_unpin_workers(parallel, &pin); th_rmtree(tmp); ASSERT_EQ(pin.rc, 0); ASSERT_EQ(unpin_rc, 0); ASSERT_EQ(run_rc, 0); ASSERT_TRUE(opened); ASSERT_EQ(own, 1); /* from . import x: the file's own package */ ASSERT_EQ(own_decoy, 0); /* never the lexicographically first namesake */ ASSERT_EQ(parent, 1); /* from .. import x: the parent package */ ASSERT_EQ(parent_decoy, 0); ASSERT_EQ(alias, 1); /* from . import x as y */ ASSERT_EQ(alias_decoy, 0); ASSERT_FALSE(guessed); /* bound by the import, not by a name guess */ PASS(); } TEST(pipeline_python_package_relative_imports) { return check_python_package_relative_imports(false); } TEST(pipeline_python_package_relative_imports_parallel) { return check_python_package_relative_imports(true); } /* #1237: a plain `from pkg.db import get_row_or_none` and bare calls resolve to * the imported module-level def (function and method callers, plus the * same-file caller), never to a same-named def elsewhere that a name guess * would rank first (legacy.compat), and a re-index that only ADDS an unrelated * file (the reporter's watcher re-index after writing benchmark scripts into the * indexed tree) keeps exactly the same CALLS. Main already resolved this shape * when the guard landed: it pins the shape and has no RED on revert. */ static int check_python_from_import_bare_call_issue1237(bool parallel) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_1237_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "pkg/__init__.py", ""); write_temp_file(tmp, "pkg/db.py", "def get_row_or_none(model, **filters):\n" " return None\n\n" "def other_helper():\n" " return get_row_or_none(1, id=2)\n"); write_temp_file(tmp, "pkg/views.py", "from pkg.db import get_row_or_none\n\n" "def show(pk):\n" " return get_row_or_none(object, id=pk)\n\n" "class Detail:\n" " def get(self, pk):\n" " return get_row_or_none(object, pk=pk)\n"); write_temp_file(tmp, "legacy/__init__.py", ""); write_temp_file(tmp, "legacy/compat.py", "def get_row_or_none(model):\n" " return model\n"); py_write_padding(tmp, "rowpad", parallel); py_workers_pin_t pin = py_pin_workers(parallel); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/rows.db", tmp); int run_rc[2] = {-1, -1}; int show[2] = {-1, -1}; int method[2] = {-1, -1}; int same[2] = {-1, -1}; int decoy[2] = {-1, -1}; int calls[2] = {-1, -1}; bool guessed = false; for (int run = 0; run < 2 && pin.rc == 0; run++) { if (run == 1) { /* Second run on the same db: routes to the incremental re-index. */ write_temp_file(tmp, "bench_rows.py", "def bench():\n return 0\n"); } cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); run_rc[run] = p ? cbm_pipeline_run(p) : -1; cbm_store_t *s = run_rc[run] == 0 ? cbm_store_open_path(db_path) : NULL; if (s) { const char *project = cbm_pipeline_project_name(p); const char *target = ".pkg.db.get_row_or_none"; const char *namesake = ".legacy.compat.get_row_or_none"; show[run] = py_calls_between(s, project, ".pkg.views.show", target, &guessed); method[run] = py_calls_between(s, project, ".pkg.views.Detail.get", target, &guessed); same[run] = py_calls_between(s, project, ".pkg.db.other_helper", target, &guessed); decoy[run] = py_calls_between(s, project, ".pkg.views.show", namesake, NULL) + py_calls_between(s, project, ".pkg.views.Detail.get", namesake, NULL); calls[run] = cbm_store_count_edges_by_type(s, project, "CALLS"); cbm_store_close(s); } cbm_pipeline_free(p); } int unpin_rc = py_unpin_workers(parallel, &pin); th_rmtree(tmp); ASSERT_EQ(pin.rc, 0); ASSERT_EQ(unpin_rc, 0); for (int run = 0; run < 2; run++) { ASSERT_EQ(run_rc[run], 0); ASSERT_EQ(show[run], 1); /* cross-file bare call through a plain from-import */ ASSERT_EQ(method[run], 1); /* the same from a method */ ASSERT_EQ(same[run], 1); /* same-file bare call */ ASSERT_EQ(decoy[run], 0); /* never the namesake a name guess would pick */ } ASSERT_EQ(calls[1], calls[0]); /* adding a call-free file moves no CALLS edge */ ASSERT_FALSE(guessed); PASS(); } TEST(pipeline_python_from_import_bare_call_issue1237) { return check_python_from_import_bare_call_issue1237(false); } TEST(pipeline_python_from_import_bare_call_issue1237_parallel) { return check_python_from_import_bare_call_issue1237(true); } /* Reproduce-first: pass_parallel's fused cross-LSP eligibility currently counts * only parser-backed calls/call references. A Python binary operator has no * parser CBMCall; its __add__ semantic record and carrier are created together * only by the cross resolver. Consequently combine.py looks like it has zero * semantic sites and the fused pass skips it before that carrier can exist. * * `visible()` is deliberately in a different file. Its ordinary method-call * carrier makes THAT file cross-LSP eligible and its lsp_* edge proves that the * Alpha definition, import map, shared registry, and cross resolver are all * healthy. Putting the control call in combine.py would hide the bug by making * the broken file eligible. The 52 fillers plus three fixture files exceed * MIN_FILES_FOR_PARALLEL (50), and CBM_WORKERS=4 forces the production fused * path independent of host core count. */ TEST(pipeline_parallel_python_cross_only_dunder_gets_synthetic_carrier) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_syn_par_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "alpha.py", "class Alpha:\n" " def __add__(self, other):\n" " return self\n"); write_temp_file(tmp, "visible.py", "from .alpha import Alpha\n\n" "def visible(left: Alpha, right: Alpha):\n" " return left.__add__(right)\n"); write_temp_file(tmp, "combine.py", "from .alpha import Alpha\n\n" "def combine(left: Alpha, right: Alpha):\n" " return left + right\n"); for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "py_pad_%02d.py", i); snprintf(body, sizeof(body), "def py_pad_%02d():\n return %d\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/python_parallel.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int run_rc = cbm_pipeline_run(p); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); bool store_opened = s != NULL; bool control_lsp = false; bool combine_to_add = false; if (s) { control_lsp = cross_file_call_has_strategy(s, project, "visible", "__add__", "\"strategy\":\"lsp_"); combine_to_add = cross_file_call_exists(s, project, "combine", "__add__"); cbm_store_close(s); } cbm_pipeline_free(p); if (saved_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); free(saved_workers); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); ASSERT_EQ(run_rc, 0); ASSERT_TRUE(store_opened); ASSERT_TRUE(control_lsp); /* non-vacuity: cross LSP + target are healthy */ ASSERT_TRUE(combine_to_add); /* RED: fused eligibility skips combine.py */ PASS(); } /* Same fused-eligibility hole for Rust built-in macro arguments. The parser * exposes format! but not render() inside its token tree. Per-file LSP already * qualifies every parser-backed site in hidden.rs, so the fused heuristic sees * no remaining work and never runs the cross pass that would discover both the * semantic render target and its exact synthetic carrier. * * The exact same 55-file fixture is indexed once through the sequential path * first. A hidden() -> render CALLS edge there is a strong non-vacuity control: * only the sequential cross-LSP pass can discover both the macro-hidden call * and its synthetic carrier. The parallel run must preserve that result. */ TEST(pipeline_parallel_rust_cross_only_macro_hidden_gets_synthetic_carrier) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_rs_syn_par_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "lib.rs", "pub fn render() -> &'static str { \"ok\" }\n"); write_temp_file(tmp, "main.rs", "mod lib;\n\n" "pub fn hidden() -> String { format!(\"{}\", lib::render()) }\n"); for (int i = 0; i < 53; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "rust_pad_%02d.rs", i); snprintf(body, sizeof(body), "pub fn rust_pad_%02d() -> i32 { %d }\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1); char seq_db_path[512]; snprintf(seq_db_path, sizeof(seq_db_path), "%s/rust_sequential.db", tmp); cbm_pipeline_t *seq = cbm_pipeline_new(tmp, seq_db_path, CBM_MODE_FULL); int seq_run_rc = seq ? cbm_pipeline_run(seq) : -1; const char *seq_project = seq ? cbm_pipeline_project_name(seq) : NULL; cbm_store_t *seq_store = cbm_store_open_path(seq_db_path); bool seq_store_opened = seq_store != NULL; bool seq_hidden_to_render = false; if (seq_store && seq_project) { seq_hidden_to_render = cross_file_call_exists(seq_store, seq_project, "hidden", "render"); cbm_store_close(seq_store); } cbm_pipeline_free(seq); /* The parallel half must not inherit a caller-level single-thread override, * even when the surrounding test process was launched with one. Restore it * after both halves complete. */ cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); cbm_setenv("CBM_WORKERS", "4", 1); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/rust_parallel.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int run_rc = cbm_pipeline_run(p); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); bool store_opened = s != NULL; bool render_target_present = false; bool hidden_to_render = false; if (s) { render_target_present = count_nodes_named(s, project, "render") > 0; hidden_to_render = cross_file_call_exists(s, project, "hidden", "render"); cbm_store_close(s); } cbm_pipeline_free(p); if (saved_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); free(saved_workers); } else { cbm_unsetenv("CBM_WORKERS"); } if (saved_single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); free(saved_single); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } th_rmtree(tmp); ASSERT_EQ(seq_run_rc, 0); ASSERT_TRUE(seq_store_opened); ASSERT_TRUE(seq_hidden_to_render); /* same fixture is semantically resolvable */ ASSERT_EQ(run_rc, 0); ASSERT_TRUE(store_opened); ASSERT_TRUE(render_target_present); /* target is present in the parallel graph */ ASSERT_TRUE(hidden_to_render); /* RED: fused eligibility skips hidden.rs */ PASS(); } /* Slash-prefixed call arguments are not necessarily HTTP routes. Keep the * parallel arg-url heuristic from minting Route nodes for filesystem paths or * regex-replacement operands, while preserving a genuine API path. */ TEST(pipeline_arg_url_rejects_non_http_slash_arguments) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_arg_url_guard_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "src/args.py", "import requests\n" "TMP = '/tmp/pgv_fuzz.bin'\n" "def run_copy(path):\n" " return path\n" "def write_fixture():\n" " return run_copy(TMP)\n" "def load_api():\n" " return requests.get('/api/data')\n"); write_temp_file(tmp, "src/regex.js", "function sink(value) { return value; }\n" "export function sanitize(template) {\n" " sink(/ 50 files), where the arg-url heuristic lives. A config object * without a `url` key must not mint a route. */ TEST(pipeline_ts_config_object_url_http_calls_issue2235) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_i2235_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "src/http.ts", "export const http = (config: { url: string; method: string }): " "Promise => {\n" " return fetch(config.url, { method: config.method }).then((r) => r.json());\n" "};\n"); write_temp_file( tmp, "src/gen/customer-controller.ts", "import { http } from '../http';\n" "export const get1 = (id: string, signal?: AbortSignal) => {\n" " return http({url: `/api/customers/${id}`, method: 'GET', signal});\n" "};\n" "export const noUrl = () => {\n" " return http({path: '/api/ignored/x', method: 'GET'});\n" "};\n"); for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/filler%d.ts", i); snprintf(body, sizeof(body), "export function filler%d(): number { return %d; }\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/i2235.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* RED before the fix: the url inside the config object was never read. */ ASSERT_EQ(named_edge_count(s, project, "HTTP_CALLS", "get1", "/api/customers/{}"), 1); /* Only the `url` key names the request URL. */ ASSERT_EQ(count_nodes_named(s, project, "/api/ignored/x"), 0); cbm_store_close(s); cbm_pipeline_free(p); if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); PASS(); } /* Count HTTP_CALLS-style edges whose url_path property contains `keyword`. */ static int count_edges_with_url_path(cbm_store_t *s, const char *project, const char *keyword) { cbm_edge_t *es = NULL; int n = 0; cbm_store_find_edges_by_url_path(s, project, keyword, &es, &n); if (es) { cbm_store_free_edges(es, n); } return n; } /* Distilled from PR #1245: detect_url_in_args (parallel resolver, >= 50 files) * runs for EVERY resolved call with no callee gating, so a filesystem path * with a document/log extension passed to a project-defined open/read/write * helper passed normalize_url_arg (second slash, no space) and the route * literal guard, minting a Route node + an HTTP_CALLS edge for a file. The * callees are project functions on purpose: a bare builtin `open(...)` never * resolves to a target and so never reaches emit_service_edge at all. The * first path segments avoid the filesystem-root list (etc/var/tmp/...) so * only the extension guard can reject them. requests.get('/api/items') is * the positive control that must keep its Route + HTTP_CALLS. */ TEST(pipeline_arg_url_rejects_document_file_paths) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_arg_url_docs_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "src/files.py", "import requests\n" "def open_file(path):\n" " return path\n" "def read_file(path):\n" " return path\n" "def write_file(path):\n" " return path\n" "def load_docs():\n" " open_file('/new/file.txt')\n" " read_file('/docs/guide.md')\n" " write_file('/data/app.log')\n" " read_file('/reports/summary.pdf')\n" " open_file('/docs/index.rst')\n" "def fetch_items():\n" " return requests.get('/api/items')\n"); for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/filler%d.ts", i); snprintf(body, sizeof(body), "export function filler%d(): number { return %d; }\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/arg_url_docs.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* Positive control first: the genuine HTTP call still mints its route. */ ASSERT_GTE(count_nodes_named(s, project, "/api/items"), 1); ASSERT_GTE(count_edges_with_url_path(s, project, "/api/items"), 1); /* No Route node for any filesystem document path... */ ASSERT_EQ(count_nodes_named(s, project, "/new/file.txt"), 0); ASSERT_EQ(count_nodes_named(s, project, "/docs/guide.md"), 0); ASSERT_EQ(count_nodes_named(s, project, "/data/app.log"), 0); ASSERT_EQ(count_nodes_named(s, project, "/reports/summary.pdf"), 0); ASSERT_EQ(count_nodes_named(s, project, "/docs/index.rst"), 0); /* ...and no HTTP_CALLS edge carrying one as its url_path either. */ ASSERT_EQ(count_edges_with_url_path(s, project, "/new/file.txt"), 0); ASSERT_EQ(count_edges_with_url_path(s, project, "/docs/guide.md"), 0); ASSERT_EQ(count_edges_with_url_path(s, project, "/data/app.log"), 0); ASSERT_EQ(count_edges_with_url_path(s, project, "/reports/summary.pdf"), 0); ASSERT_EQ(count_edges_with_url_path(s, project, "/docs/index.rst"), 0); cbm_store_close(s); cbm_pipeline_free(p); if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); PASS(); } /* A failed SQL query must never look like an absent false edge. */ static int navigation_probe_count(cbm_store_t *store, const char *sql, const char *project, const char *value, const char *source_name) { sqlite3_stmt *st = NULL; sqlite3 *db = cbm_store_get_db(store); if (!db || sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) { sqlite3_finalize(st); return -1; } int count = -1; bool bound = sqlite3_bind_text(st, 1, project, -1, SQLITE_TRANSIENT) == SQLITE_OK && sqlite3_bind_text(st, 2, value, -1, SQLITE_TRANSIENT) == SQLITE_OK; if (bound && source_name) { bound = sqlite3_bind_text(st, 3, source_name, -1, SQLITE_TRANSIENT) == SQLITE_OK; } if (bound && sqlite3_step(st) == SQLITE_ROW) { count = sqlite3_column_int(st, 0); if (sqlite3_step(st) != SQLITE_DONE) { count = -1; } } if (sqlite3_finalize(st) != SQLITE_OK) { count = -1; } return count; } /* #1250: exercise the generic URL heuristic with real extracted occurrences * and real source nodes. More than 50 files selects the parallel resolver. * HTTP clients, custom navigateApi wrappers, Python and ASYNC keep their edges. */ TEST(pipeline_arg_url_skips_client_navigation) { static const char page[] = "import axios from 'axios';\n" "import { get as navigate } from 'axios';\n" "import { navigateTo, http, navigateApi } from './nav';\n" "export class PaymentsPage {\n" " constructor(private router: any, private navCtrl: any, private location: any,\n" " private $router: any, private history: any) {}\n" " goPayments(): void { this.router.navigateByUrl('/payments/list'); }\n" " goBenefits(): void { this.navCtrl.navigateForward('/benefits/detail'); }\n" " goBack(): void { this.location.go('/orders/back'); }\n" " goVue(): void { this.$router.push('/dashboard/settings'); }\n" " goHist(): void { this.history.pushState(null, '', '/account/history'); }\n" " goWidget(id: string): void { navigateTo(`/widgetTypes/view/${id}`); }\n" " load(): unknown { return http('/api/customers'); }\n" " orders(): unknown { return axios.get('/api/orders'); }\n" " items(): unknown { return fetch('/api/items'); }\n" " custom(): unknown { return navigateApi('/api/navigation'); }\n" " explicitAlias(): unknown { return navigate('/api/alias'); }\n" " publish(): void { mqtt.publish('/tenant/events', 'payload'); }\n" "}\n"; static const struct { const char *callee; const char *source; const char *path; } navigation[] = { {"this.router.navigateByUrl", "goPayments", "/payments/list"}, {"this.navCtrl.navigateForward", "goBenefits", "/benefits/detail"}, {"this.location.go", "goBack", "/orders/back"}, {"this.$router.push", "goVue", "/dashboard/settings"}, {"this.history.pushState", "goHist", "/account/history"}, {"navigateTo", "goWidget", "/widgetTypes/view/{}"}, }; static const char edge_sql[] = "SELECT count(*) FROM edges e JOIN nodes s ON s.id=e.source_id " "WHERE e.project=?1 AND e.type='HTTP_CALLS' " "AND json_extract(e.properties,'$.url_path')=?2 AND s.name=?3"; static const char route_sql[] = "SELECT count(*) FROM nodes WHERE project=?1 AND label='Route' AND name=?2"; static const char source_sql[] = "SELECT count(*) FROM nodes WHERE project=?1 AND name=?2 AND file_path=?3"; static const struct { const char *source; const char *path; } positive[] = {{"load", "/api/customers"}, {"orders", "/api/orders"}, {"items", "/api/items"}, {"custom", "/api/navigation"}, {"load_items", "/api/python/items"}, {"explicitAlias", "/api/alias"}}; int extracted[6] = {0}; CBMFileResult *fr = cbm_extract_file(page, (int)strlen(page), CBM_LANG_TYPESCRIPT, "navigation_probe", "src/page.ts", 0, NULL, NULL); bool extraction_ok = fr && !fr->has_error; if (extraction_ok) { for (int c = 0; c < fr->calls.count; c++) { const CBMCall *call = &fr->calls.items[c]; for (size_t i = 0; i < sizeof(navigation) / sizeof(navigation[0]); i++) { if (call->callee_name && strcmp(call->callee_name, navigation[i].callee) == 0 && call->enclosing_func_qn && call->start_line > 0 && call->site_end_byte > call->site_start_byte && call->arg_count > 0) { extracted[i]++; } } } } cbm_free_result(fr); const char *old_workers = getenv("CBM_WORKERS"); const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; char *saved_single = old_single ? strdup(old_single) : NULL; if ((old_workers && !saved_workers) || (old_single && !saved_single)) { free(saved_workers); free(saved_single); FAIL("save environment"); } char tmp[256] = "/tmp/cbm_arg_url_nav_XXXXXX"; if (!cbm_mkdtemp(tmp)) { free(saved_workers); free(saved_single); FAIL("tmpdir"); } write_temp_file(tmp, "src/nav.ts", "export class AppRouter {\n" " navigateByUrl(url: string): void {}\n" " push(url: string): void {}\n" "}\n" "export class NavController {\n" " navigateForward(url: string): void {}\n" "}\n" "export class AppLocation { go(url: string): void {} }\n" "export class AppHistory {\n" " pushState(state: unknown, title: string, url: string): void {}\n" "}\n" "export function navigateTo(url: string): void {}\n" "export function http(url: string): unknown { return url; }\n" "export function navigateApi(url: string): unknown { return url; }\n"); write_temp_file(tmp, "src/page.ts", page); write_temp_file(tmp, "src/py_client.py", "def navigate(url):\n return url\n" "def load_items():\n return navigate('/api/python/items')\n"); for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/filler%d.ts", i); snprintf(body, sizeof(body), "export function filler%d(): number { return %d; }\n", i, i); write_temp_file(tmp, name, body); } cbm_setenv("CBM_WORKERS", "4", 1); cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); const char *workers = getenv("CBM_WORKERS"); bool configured = workers && strcmp(workers, "4") == 0 && getenv("CBM_INDEX_SINGLE_THREAD") == NULL; char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/arg_url_nav.db", tmp); cbm_pipeline_t *p = configured ? cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL) : NULL; int run_rc = p ? cbm_pipeline_run(p) : -1; cbm_store_t *store = run_rc == 0 ? cbm_store_open_path(db_path) : NULL; bool observed = store != NULL; int nav_edges[6], nav_routes[6], sources[6], good_edges[6]; int files = -1, async_edges = -1, async_http = -1; for (size_t i = 0; i < 6; i++) { nav_edges[i] = nav_routes[i] = sources[i] = -1; } for (size_t i = 0; i < 6; i++) { good_edges[i] = -1; } if (store) { const char *project = cbm_pipeline_project_name(p); files = navigation_probe_count(store, "SELECT count(*) FROM nodes WHERE project=?1 AND label=?2", project, "File", NULL); for (size_t i = 0; i < 6; i++) { nav_edges[i] = navigation_probe_count(store, edge_sql, project, navigation[i].path, navigation[i].source); nav_routes[i] = navigation_probe_count(store, route_sql, project, navigation[i].path, NULL); sources[i] = navigation_probe_count(store, source_sql, project, navigation[i].source, "src/page.ts"); } for (size_t i = 0; i < 6; i++) { good_edges[i] = navigation_probe_count(store, edge_sql, project, positive[i].path, positive[i].source); } async_edges = navigation_probe_count( store, "SELECT count(*) FROM edges e JOIN nodes s ON s.id=e.source_id " "JOIN nodes t ON t.id=e.target_id WHERE e.project=?1 AND e.type='ASYNC_CALLS' " "AND t.name=?2 AND s.name=?3", project, "/tenant/events", "publish"); async_http = navigation_probe_count(store, edge_sql, project, "/tenant/events", "publish"); cbm_store_close(store); } cbm_pipeline_free(p); if (saved_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); } else { cbm_unsetenv("CBM_WORKERS"); } if (saved_single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } const char *restored_workers = getenv("CBM_WORKERS"); const char *restored_single = getenv("CBM_INDEX_SINGLE_THREAD"); bool restored = (saved_workers ? restored_workers && strcmp(saved_workers, restored_workers) == 0 : !restored_workers) && (saved_single ? restored_single && strcmp(saved_single, restored_single) == 0 : !restored_single); free(saved_workers); free(saved_single); th_rmtree(tmp); ASSERT_TRUE(extraction_ok); ASSERT_TRUE(configured); ASSERT_TRUE(restored); ASSERT_EQ(run_rc, 0); ASSERT_TRUE(observed); ASSERT_GTE(files, 55); for (size_t i = 0; i < 6; i++) { ASSERT_EQ(extracted[i], 1); ASSERT_EQ(sources[i], 1); ASSERT_EQ(nav_edges[i], 0); ASSERT_EQ(nav_routes[i], 0); } for (size_t i = 0; i < 6; i++) { ASSERT_GTE(good_edges[i], 1); } ASSERT_GTE(async_edges, 1); ASSERT_EQ(async_http, 0); PASS(); } /* Native `fetch()` (#856), sequential path (< 50 files → pass_calls.c). A bare * unqualified call to the global fetch API has no import and no local * definition anywhere in this project, so registry resolution comes back * empty; classify it as HTTP_CALLS via the same #523-style empty-resolution * fallback used for axios/requests. A member call on an unrelated receiver * (`repo.fetch()`) must NOT be swept in — its callee_name is "repo.fetch", * not the bare "fetch" this check matches exactly. */ TEST(pipeline_native_fetch_classified_as_http_calls) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_fetch_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "src/api.ts", "export function loadData(): unknown {\n" " return fetch('/api/data');\n" "}\n"); /* `repo.fetch()` — a method call, not the global. Must not over-match. */ write_temp_file(tmp, "src/repo.ts", "export function useRepo(repo: { fetch: () => unknown }): unknown {\n" " return repo.fetch();\n" "}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/fetch.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1); /* Exactly the bare call, not the method call too. */ ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* #1892: Swift produced no Route node and no HTTP_CALLS edge, because the * Swift grammar has no "arguments" field and the generic lookup therefore read * no call arguments at all. Alamofire/URLSession were already in the service * pattern table; the URL simply never reached it. This is the Swift twin of * the TypeScript fetch case above. */ /* The shape real Swift actually writes: the URL is built by a constructor and * force-unwrapped, so the literal is two levels below the argument list. This * is what issue #1892 reported from a real project. */ TEST(pipeline_swift_nested_url_makes_route_issue1892) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_swiftnested_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "Sources/Client.swift", "import Foundation\n" "final class Client {\n" " func listWidgets() {\n" " URLSession.shared.dataTask(with: " "URL(string: \"/api/v1/widgets\")!)\n" " }\n" "}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/swiftnested.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1); cbm_node_t *routes = NULL; int route_count = 0; cbm_store_find_nodes_by_label(s, project, "Route", &routes, &route_count); int widget_routes = 0; for (int i = 0; i < route_count; i++) { if (routes[i].qualified_name && strstr(routes[i].qualified_name, "/api/v1/widgets")) { widget_routes++; } } cbm_store_free_nodes(routes, route_count); ASSERT_GTE(widget_routes, 1); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* HANDLES edges from the function named `source_name` to Routes whose * qualified name equals `route_qn`. */ static int handles_to_route(cbm_store_t *s, const char *project, const char *source_name, const char *route_qn) { cbm_edge_t *edges = NULL; int edge_count = 0; if (cbm_store_find_edges_by_type(s, project, "HANDLES", &edges, &edge_count) != CBM_STORE_OK) { return -1; } int matches = 0; for (int i = 0; i < edge_count; i++) { cbm_node_t source = {0}; cbm_node_t target = {0}; int source_ok = cbm_store_find_node_by_id(s, edges[i].source_id, &source) == CBM_STORE_OK; int target_ok = cbm_store_find_node_by_id(s, edges[i].target_id, &target) == CBM_STORE_OK; if (source_ok && target_ok && source.name && target.qualified_name && (!source_name || strcmp(source.name, source_name) == 0) && strcmp(target.qualified_name, route_qn) == 0) { matches++; } cbm_node_free_fields(&source); cbm_node_free_fields(&target); } if (edges) { cbm_store_free_edges(edges, edge_count); } return matches; } /* fastapi (2026-10-05): one binary indexed the same tree to 3,253 or to * 1,119,241 HANDLES edges. A prefix Route took the directory of whichever * registrar's CALLS edge came first (worker order) and linked every decorated * handler under it, with no filter for the root prefix `/`; every infra URL * then copied the handlers of the root route. Now: every registrar counts, a * root mount bridges nothing, and an infra URL matches a root route only of * its own service. */ TEST(pipeline_route_prefix_bridge_is_order_free_and_root_bridges_nothing) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_routebridge_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } /* two apps mounted at the root and under /api, from two directories */ write_temp_file(tmp, "svc/a/main.py", "from fastapi import FastAPI\n" "app = FastAPI()\n" "app.mount(\"/\", None)\n" "app.mount(\"/api\", None)\n" "\n" "@app.get(\"/items\")\n" "def read_items():\n" " return []\n"); write_temp_file(tmp, "svc/a/api/routes.py", "from fastapi import APIRouter\n" "router = APIRouter()\n" "\n" "@router.get(\"/ax\")\n" "def api_a():\n" " return 1\n"); write_temp_file(tmp, "svc/b/main.py", "from fastapi import FastAPI\n" "app = FastAPI()\n" "app.mount(\"/\", None)\n" "app.mount(\"/api\", None)\n" "app.mount(\"/c\", None)\n" "\n" "@app.get(\"/users\")\n" "def read_users():\n" " return []\n"); write_temp_file(tmp, "svc/b/api/routes.py", "from fastapi import APIRouter\n" "router = APIRouter()\n" "\n" "@router.get(\"/by\")\n" "def api_b():\n" " return 2\n"); /* a real root handler, and a documentation site's link to a chat server */ write_temp_file(tmp, "web/site.py", "from flask import Flask\n" "app = Flask(__name__)\n" "\n" "@app.route(\"/\")\n" "def index():\n" " return \"ok\"\n" "\n" "@app.route(\"/c\")\n" "def contact():\n" " return \"c\"\n"); write_temp_file(tmp, "mkdocs.yml", "site_name: demo\n" "extra:\n" " social:\n" " - icon: fontawesome/brands/discord\n" " link: https://discord.com/invite/abc123\n" " - icon: fontawesome/brands/github\n" " link: https://github.com/carol\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/routebridge.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* decorator routes are untouched */ ASSERT_EQ(handles_to_route(s, project, "read_items", "__route__GET__/items"), 1); ASSERT_EQ(handles_to_route(s, project, "index", "__route__ANY__/"), 1); /* a root mount names no part of a handler's path: nothing is bridged */ ASSERT_EQ(handles_to_route(s, project, "read_items", "__route__ANY__/"), 0); ASSERT_EQ(handles_to_route(s, project, "read_users", "__route__ANY__/"), 0); /* /api is registered from both directories: both api handlers, whatever * order the registrations' CALLS edges landed in */ ASSERT_EQ(handles_to_route(s, project, "api_a", "__route__ANY__/api"), 1); ASSERT_EQ(handles_to_route(s, project, "api_b", "__route__ANY__/api"), 1); /* a prefix and a URL path match on whole segments only: `c` is no segment * of svc/b/main.py, and `/c` is no route of github.com/carol */ ASSERT_EQ(handles_to_route(s, project, "read_users", "__route__ANY__/c"), 0); ASSERT_EQ(handles_to_route(s, project, "contact", "__route__ANY__/c"), 1); ASSERT_EQ(handles_to_route(s, project, NULL, "__route__infra__https://github.com/carol"), 0); /* the chat link is no route of this repository's root handler */ ASSERT_EQ( handles_to_route(s, project, NULL, "__route__infra__https://discord.com/invite/abc123"), 0); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } TEST(pipeline_swift_http_call_makes_route_issue1892) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_swifthttp_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } /* URLSession, not Alamofire's `AF` shorthand: the service pattern table * matches the library name in the callee text, and "AF.request" contains * no such name. */ write_temp_file(tmp, "Sources/Client.swift", "import Foundation\n" "final class Client {\n" " func listWidgets() {\n" " URLSession.shared.dataTask(with: \"/api/v1/widgets\")\n" " }\n" "}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/swifthttp.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1); /* The edge carries the URL, so pass_route_nodes can mint the Route the * cross-repo matcher joins a server route against. */ cbm_node_t *routes = NULL; int route_count = 0; cbm_store_find_nodes_by_label(s, project, "Route", &routes, &route_count); int widget_routes = 0; for (int i = 0; i < route_count; i++) { if (routes[i].qualified_name && strstr(routes[i].qualified_name, "/api/v1/widgets")) { widget_routes++; } } cbm_store_free_nodes(routes, route_count); ASSERT_GTE(widget_routes, 1); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* Native `fetch()` (#856), parallel path (>= 50 files -> pass_parallel.c's * resolve_file_calls). Mirrors pipeline_native_fetch_classified_as_http_calls * but forces the parallel resolver, since the empty-resolution fallback is a * separate implementation there (resolve_file_calls calls * emit_http_async_service_edge directly rather than through emit_service_edge, * which would otherwise re-derive CBM_SVC_NONE for "fetch" and silently fall * through to a plain CALLS edge). */ TEST(pipeline_native_fetch_parallel_classified_as_http_calls) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_fetch_par_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "src/api.ts", "export function loadData(): unknown {\n" " return fetch('/api/data');\n" "}\n"); for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/filler%d.ts", i); snprintf(body, sizeof(body), "export function filler%d(): number {\n return %d;\n}\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); /* force parallel regardless of host cores */ char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/fetch_par.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1); cbm_store_close(s); cbm_pipeline_free(p); if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); PASS(); } /* Native `fetch()` (#856): a LOCAL `fetch` definition must win over the * global-API guess. Registry resolution finds this project's own top-level * `fetch` function, so the call is a plain CALLS edge to it — never * HTTP_CALLS. Isolated in its own project: the registry's project-wide * unique-name fallback means a local `fetch` anywhere in a project can * legitimately capture bare `fetch()` calls project-wide, so this must not * share a project with the genuine-native-fetch test above. */ TEST(pipeline_local_fetch_shadow_not_classified_as_http) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_fetch_shadow_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "src/local_fetch.ts", "function fetch(url: string): string {\n" " return 'mock:' + url;\n" "}\n" "export function useLocalFetch(): string {\n" " return fetch('/api/data');\n" "}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/fetch_shadow.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 0); ASSERT_TRUE(cross_file_call_exists(s, project, "useLocalFetch", "fetch")); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* A call the HTTP service patterns classify (axios, an axios.create instance) * gets exactly ONE HTTP_CALLS edge, typed with the verb its callee names. The * arg-URL heuristic (detect_url_in_args, via=arg_url) exists for calls the * patterns do not know -- a local fetch wrapper such as callApi('/api/orders') * -- and must not add a second, method-less __route__ANY__ edge for the same * call site. On microsoft/healthcare-ai-model-evaluator 59 of 129 HTTP_CALLS * were such twins, and the cross-repo matcher bound them to server handlers of * any method. `fillers` > 0 forces the parallel resolver (the RED case); 0 * keeps the sequential one, which must agree. */ static int typed_http_no_url_twin_case(int fillers) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_http_twin_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "src/api.ts", "import axios from 'axios';\n" "const axiosInstance = axios.create({ baseURL: 'http://localhost:5000' });\n" "export function getModel(id: string) {\n" " return axiosInstance.get(`/api/models/${id}`);\n" "}\n" "export function addModel(body: unknown) {\n" " return axiosInstance.post('/api/models', body);\n" "}\n" "export function removeModel(id: string) {\n" " return axios.delete(`/api/models/${id}`);\n" "}\n"); write_temp_file(tmp, "src/wrapper.ts", "function callApi(path: string): Promise {\n" " return fetch(path);\n" "}\n" "export function listOrders() {\n" " return callApi('/api/orders');\n" "}\n"); for (int i = 0; i < fillers; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/filler%d.ts", i); snprintf(body, sizeof(body), "export function filler%d(): number {\n return %d;\n}\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/http_twin.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); static const struct { const char *caller; const char *typed_route; } typed[] = {{"getModel", "__route__GET__"}, {"addModel", "__route__POST__"}, {"removeModel", "__route__DELETE__"}}; for (size_t i = 0; i < sizeof(typed) / sizeof(typed[0]); i++) { /* The typed edge stays ... */ ASSERT_EQ(http_calls_to_route(s, project, typed[i].caller, typed[i].typed_route), 1); /* ... and it is the only HTTP_CALLS edge of that call site. */ ASSERT_EQ(http_calls_to_route(s, project, typed[i].caller, "__route__ANY__"), 0); ASSERT_EQ(http_calls_to_route(s, project, typed[i].caller, "__route__"), 1); } if (fillers > 0) { /* Control: the heuristic still covers the call no pattern knows. The * sequential resolver has no arg-URL heuristic yet (#2235). */ ASSERT_EQ(http_calls_to_route(s, project, "listOrders", "__route__ANY__"), 1); } cbm_store_close(s); cbm_pipeline_free(p); if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); PASS(); } TEST(pipeline_typed_http_call_has_no_arg_url_twin) { return typed_http_no_url_twin_case(52); } TEST(pipeline_typed_http_call_has_no_arg_url_twin_sequential) { return typed_http_no_url_twin_case(0); } /* #706 / #1147 end to end: a URL composed from module constants reaches the * HTTP classifier, so the client call forms an HTTP_CALLS edge. Python * `API_URL = API_BASE_URL + "/orders/create"` (env-derived base, literal path * kept) and a TS object-literal endpoint map over a base constant. The * unresolvable control (`requests.get(u)` on a computed value) adds none. */ TEST(pipeline_url_constant_folding_http_calls_issue706_1147) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_urlfold_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "svc/client.py", "import os\n" "import requests\n" "API_BASE_URL = os.environ[\"API_BASE_URL\"]\n" "API_URL = API_BASE_URL + \"/orders/create\"\n" "def send(payload):\n" " return requests.post(API_URL, json=payload)\n" "def dyn():\n" " u = os.environ.get(\"X\")\n" " return requests.get(u)\n"); write_temp_file(tmp, "web/api.ts", "const BASE = '/api'\n" "export const API_ENDPOINTS = {\n" " USER: `${BASE}/users/me`,\n" "}\n" "export function loadUser(): unknown {\n" " return fetch(API_ENDPOINTS.USER);\n" "}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/urlfold.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); /* Asserted per edge, not by count: a client call may also carry a second, * arg_url-sourced HTTP_CALLS edge for the same path. */ cbm_edge_t *edges = NULL; int edge_count = 0; ASSERT_EQ(cbm_store_find_edges_by_type(s, project, "HTTP_CALLS", &edges, &edge_count), CBM_STORE_OK); int py_edges = 0; int ts_edges = 0; int control_edges = 0; for (int i = 0; i < edge_count; i++) { const char *props = edges[i].properties_json ? edges[i].properties_json : ""; cbm_node_t src = {0}; cbm_store_find_node_by_id(s, edges[i].source_id, &src); if (src.name && strcmp(src.name, "dyn") == 0) { control_edges++; } else if (src.name && strcmp(src.name, "send") == 0 && strstr(props, "\"url_path\":\"/orders/create\"")) { py_edges++; } else if (src.name && strcmp(src.name, "loadUser") == 0 && strstr(props, "\"url_path\":\"/api/users/me\"")) { ts_edges++; } cbm_node_free_fields(&src); } cbm_store_free_edges(edges, edge_count); ASSERT_GTE(py_edges, 1); ASSERT_GTE(ts_edges, 1); ASSERT_EQ(control_edges, 0); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* Run the same identities through both service resolvers. An import alias is * deliberately absent from the callee-name pattern table until resolution. */ TEST(pipeline_folded_topics_preserve_identity_sequential_parallel) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_topicfold_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "src/topics.ts", "const TENANT = 'tenant';\n" "const TOPIC = `${TENANT}/events`;\n" "const TOPICS = { EVENTS: `${TENANT}/events` };\n" "const PREFIX = 'tenant/';\n" "const DOUBLE = `${PREFIX}/events`;\n" "const UNKNOWN = `${tenant}/events`;\n" "export function literal() { mqtt.publish('tenant/events', 'payload'); }\n" "export function direct() { mqtt.publish(`${TENANT}/events`, 'payload'); }\n" "export function constant() { mqtt.publish(TOPIC, 'payload'); }\n" "export function member() { mqtt.publish(TOPICS.EVENTS, 'payload'); }\n" "export function doubled() { mqtt.publish(DOUBLE, 'payload'); }\n" "export function unknown() { mqtt.publish(UNKNOWN, 'payload'); }\n"); write_temp_file(tmp, "src/aliases.py", "from paho.mqtt import publish as send\n" "TENANT = 'tenant'\n" "TOPIC = f'{TENANT}/events'\n" "ABS = f'/{TENANT}/events'\n" "def alias():\n send(TOPIC, 'payload')\n" "def keyword():\n send(topic=TOPIC, payload='x')\n" "def slash_topic():\n send(ABS, 'payload')\n"); for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/padding%d.ts", i); snprintf(body, sizeof(body), "export function padding%d() { return %d; }\n", i, i); write_temp_file(tmp, name, body); } const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; const char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; int run_rc[2] = {-1, -1}; bool opened[2] = {false, false}; int matches[2][9] = {{0}}; int http_edges[2] = {-1, -1}; int invented[2] = {-1, -1}; const char *functions[] = {"literal", "direct", "constant", "member", "doubled", "unknown", "alias", "keyword", "slash_topic"}; const char *topics[] = {"tenant/events", "tenant/events", "tenant/events", "tenant/events", "tenant//events", "{}/events", "tenant/events", "tenant/events", "/tenant/events"}; for (int mode = 0; mode < 2; mode++) { if (mode == 0) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); cbm_setenv("CBM_WORKERS", "4", 1); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/topics_%d.db", tmp, mode); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); run_rc[mode] = p ? cbm_pipeline_run(p) : -1; const char *project = p ? cbm_pipeline_project_name(p) : NULL; cbm_store_t *s = cbm_store_open_path(db_path); opened[mode] = s != NULL; if (s && project) { for (size_t i = 0; i < sizeof(functions) / sizeof(functions[0]); i++) { matches[mode][i] = named_edge_count(s, project, "ASYNC_CALLS", functions[i], topics[i]); } http_edges[mode] = cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"); invented[mode] = count_nodes_named(s, project, "/events"); /* Only slash_topic really contains a leading slash. */ for (size_t i = 0; i < 8; i++) { invented[mode] += named_edge_count(s, project, "ASYNC_CALLS", functions[i], "/tenant/events"); } } if (s) { cbm_store_close(s); } cbm_pipeline_free(p); } if (saved_single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); free(saved_single); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } if (saved_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); free(saved_workers); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); for (int mode = 0; mode < 2; mode++) { ASSERT_EQ(run_rc[mode], 0); ASSERT_TRUE(opened[mode]); for (size_t i = 0; i < sizeof(functions) / sizeof(functions[0]); i++) { ASSERT_GTE(matches[mode][i], 1); } ASSERT_EQ(http_edges[mode], 0); ASSERT_EQ(invented[mode], 0); } PASS(); } /* Relative endpoint maps still supply the parallel wrapper-client heuristic * through their URL candidate, including the request-config slot from #2235. */ TEST(pipeline_relative_folded_wrapper_url_candidates) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_relativefold_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file( tmp, "src/wrapper.ts", "const REL = 'api';\n" "const API_ENDPOINTS = { GAME_LAUNCH_URL: `${REL}/games/launch` };\n" "function client(url: string) { return url; }\n" "function http(config: {url: string}) { return config; }\n" "export function launch() { return client(API_ENDPOINTS.GAME_LAUNCH_URL); }\n" "export function config() { return http({url: API_ENDPOINTS.GAME_LAUNCH_URL}); }\n"); for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/padding%d.ts", i); snprintf(body, sizeof(body), "export function padding%d() { return %d; }\n", i, i); write_temp_file(tmp, name, body); } const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; const char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); cbm_setenv("CBM_WORKERS", "4", 1); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/wrappers.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); int run_rc = p ? cbm_pipeline_run(p) : -1; const char *project = p ? cbm_pipeline_project_name(p) : NULL; cbm_store_t *s = cbm_store_open_path(db_path); bool opened = s != NULL; int launch_edges = 0; int config_edges = 0; if (s && project) { launch_edges = named_edge_count(s, project, "HTTP_CALLS", "launch", "/api/games/launch"); config_edges = named_edge_count(s, project, "HTTP_CALLS", "config", "/api/games/launch"); } if (s) { cbm_store_close(s); } cbm_pipeline_free(p); if (saved_single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); free(saved_single); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } if (saved_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); free(saved_workers); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); ASSERT_EQ(run_rc, 0); ASSERT_TRUE(opened); ASSERT_GTE(launch_edges, 1); ASSERT_GTE(config_edges, 1); PASS(); } /* HTTP_CALLS + ASYNC_CALLS edges out of the function `source_name`. */ static int service_edges_from(cbm_store_t *s, const char *project, const char *source_name) { static const char *const types[] = {"HTTP_CALLS", "ASYNC_CALLS"}; int matches = 0; for (size_t t = 0; t < sizeof(types) / sizeof(types[0]); t++) { cbm_edge_t *edges = NULL; int edge_count = 0; if (cbm_store_find_edges_by_type(s, project, types[t], &edges, &edge_count) != CBM_STORE_OK) { return -1; } for (int i = 0; i < edge_count; i++) { cbm_node_t source = {0}; if (cbm_store_find_node_by_id(s, edges[i].source_id, &source) == CBM_STORE_OK && source.name && strcmp(source.name, source_name) == 0) { matches++; } cbm_node_free_fields(&source); } cbm_store_free_edges(edges, edge_count); } return matches; } /* IMPORTS edges out of `file_path` that bind `local_name`; -1 on a failed query. */ static int imports_binding_count(cbm_store_t *s, const char *project, const char *file_path, const char *local_name) { static const char sql[] = "SELECT count(*) FROM edges e JOIN nodes f ON f.id=e.source_id WHERE e.project=?1 " "AND e.type='IMPORTS' AND f.file_path=?2 AND json_extract(e.properties,'$.local_name')=?3"; sqlite3 *db = cbm_store_get_db(s); sqlite3_stmt *st = NULL; if (!db || sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) { sqlite3_finalize(st); return -1; } int count = -1; if (sqlite3_bind_text(st, 1, project, -1, SQLITE_TRANSIENT) == SQLITE_OK && sqlite3_bind_text(st, 2, file_path, -1, SQLITE_TRANSIENT) == SQLITE_OK && sqlite3_bind_text(st, 3, local_name, -1, SQLITE_TRANSIENT) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW) { count = sqlite3_column_int(st, 0); } sqlite3_finalize(st); return count; } /* A call through an import of an EXTERNAL client library is that library's * call: `fetchIt` from `{ get as fetchIt } from 'axios'` is a typed GET, * `http_post` from `from requests import post as http_post` a typed POST, * `emitIt` from `{ publish as emitIt } from 'mqtt'` a publish. Precision * controls, each with no HTTP_CALLS/ASYNC_CALLS edge: * - urljoin from urllib.parse: an HTTP library's helper, not a request; * - lodash's `get`, json's `dumps`: libraries no service pattern knows; * - `send` defined in the importing module shadows * `from paho.mqtt import publish as send`; `relay` rebound as a parameter * shadows `... as relay` (a name no project function has, so only the * parameter binding stands between the call and the import); * - `httpx` is a PROJECT package here: its import is materialized (the * precondition is asserted), so a call it cannot resolve stays unresolved * instead of becoming the external httpx.post. * Both resolvers: sequential and parallel (> 50 files). */ TEST(pipeline_external_import_binding_controls_sequential_parallel) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_extimport_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file( tmp, "src/ext_http.ts", "import { get as fetchIt } from 'axios';\n" "import { get as lodashGet } from 'lodash';\n" "import { publish as emitIt } from 'mqtt';\n" "export function viaAxiosAlias(): unknown { return fetchIt('/api/alias-get'); }\n" "export function viaLodashAlias(o: object): unknown {\n" " return lodashGet(o, '/api/lodash');\n" "}\n" "export function viaMqtt(): void { emitIt('tenant/ts', 'payload'); }\n"); write_temp_file(tmp, "src/ext_py.py", "from requests import post as http_post\n" "from urllib.parse import urljoin\n" "from json import dumps as publish_json\n" "from paho.mqtt import publish as send\n" "def py_post():\n return http_post('/api/py-post')\n" "def join_url(base):\n return urljoin(base, '/static/asset.css')\n" "def join_abs(name):\n return urljoin('https://example.com/assets/', name)\n" "def to_json():\n return publish_json('/tenant/json')\n" "def send(topic, payload):\n return topic\n" "def shadowed():\n return send('tenant/shadow', 'payload')\n"); write_temp_file(tmp, "src/ext_param.py", "from paho.mqtt import publish as relay\n" "def param_shadow(relay):\n return relay('tenant/param', 'payload')\n"); write_temp_file(tmp, "httpx/__init__.py", "def get(url):\n return url\n"); write_temp_file(tmp, "src/proj_client.py", "from httpx import post as hx_post\n" "def proj_post():\n return hx_post('/api/project')\n"); for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "src/padding%d.ts", i); snprintf(body, sizeof(body), "export function padding%d() { return %d; }\n", i, i); write_temp_file(tmp, name, body); } static const char *const silent[] = {"viaLodashAlias", "join_url", "join_abs", "to_json", "shadowed", "param_shadow", "proj_post"}; enum { SILENT = sizeof(silent) / sizeof(silent[0]) }; const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; const char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; int run_rc[2] = {-1, -1}; bool opened[2] = {false, false}; int typed_get[2] = {-1, -1}; int typed_post[2] = {-1, -1}; int published[2] = {-1, -1}; int shadow_calls[2] = {-1, -1}; int project_import[2] = {-1, -1}; int silent_edges[2][SILENT]; for (int mode = 0; mode < 2; mode++) { for (int i = 0; i < SILENT; i++) { silent_edges[mode][i] = -1; } if (mode == 0) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); cbm_setenv("CBM_WORKERS", "4", 1); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/extimport_%d.db", tmp, mode); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); run_rc[mode] = p ? cbm_pipeline_run(p) : -1; const char *project = p ? cbm_pipeline_project_name(p) : NULL; cbm_store_t *s = cbm_store_open_path(db_path); opened[mode] = s != NULL; if (s && project) { typed_get[mode] = http_calls_to_route(s, project, "viaAxiosAlias", "__route__GET__"); typed_post[mode] = http_calls_to_route(s, project, "py_post", "__route__POST__"); published[mode] = named_edge_count(s, project, "ASYNC_CALLS", "viaMqtt", "tenant/ts"); shadow_calls[mode] = named_edge_count(s, project, "CALLS", "shadowed", "send"); project_import[mode] = imports_binding_count(s, project, "src/proj_client.py", "hx_post"); for (int i = 0; i < SILENT; i++) { silent_edges[mode][i] = service_edges_from(s, project, silent[i]); } } if (s) { cbm_store_close(s); } cbm_pipeline_free(p); } if (saved_single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); free(saved_single); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } if (saved_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); free(saved_workers); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); for (int mode = 0; mode < 2; mode++) { ASSERT_EQ(run_rc[mode], 0); ASSERT_TRUE(opened[mode]); ASSERT_EQ(typed_get[mode], 1); ASSERT_EQ(typed_post[mode], 1); ASSERT_EQ(published[mode], 1); ASSERT_GTE(shadow_calls[mode], 1); ASSERT_GTE(project_import[mode], 1); for (int i = 0; i < SILENT; i++) { ASSERT_EQ(silent_edges[mode][i], 0); } } PASS(); } /* ── Git history pass tests ─────────────────────────────────────── */ TEST(githistory_is_trackable) { /* Source files → trackable */ ASSERT_TRUE(cbm_is_trackable_file("main.go")); ASSERT_TRUE(cbm_is_trackable_file("src/app.py")); ASSERT_TRUE(cbm_is_trackable_file("README.md")); /* Non-trackable: skip prefixes */ ASSERT_FALSE(cbm_is_trackable_file("node_modules/foo/bar.js")); ASSERT_FALSE(cbm_is_trackable_file("vendor/lib/dep.go")); ASSERT_FALSE(cbm_is_trackable_file(".git/config")); ASSERT_FALSE(cbm_is_trackable_file("__pycache__/mod.pyc")); /* Non-trackable: lock files */ ASSERT_FALSE(cbm_is_trackable_file("package-lock.json")); ASSERT_FALSE(cbm_is_trackable_file("go.sum")); /* Non-trackable: binary/minified extensions */ ASSERT_FALSE(cbm_is_trackable_file("image.png")); ASSERT_FALSE(cbm_is_trackable_file("src/style.min.css")); PASS(); } TEST(githistory_compute_coupling) { /* 5 commits with overlapping files */ char *files_0[] = {"a.go", "b.go", "c.go"}; char *files_1[] = {"a.go", "b.go"}; char *files_2[] = {"a.go", "b.go"}; char *files_3[] = {"a.go", "c.go"}; char *files_4[] = {"d.go", "e.go"}; cbm_commit_files_t commits[] = { {files_0, 3, 0}, {files_1, 2, 0}, {files_2, 2, 0}, {files_3, 2, 0}, {files_4, 2, 0}, }; cbm_change_coupling_t results[100]; int n = cbm_compute_change_coupling(commits, 5, results, 100); /* a.go + b.go co-change 3 times → should appear */ bool found_ab = false; for (int i = 0; i < n; i++) { if ((strcmp(results[i].file_a, "a.go") == 0 && strcmp(results[i].file_b, "b.go") == 0) || (strcmp(results[i].file_a, "b.go") == 0 && strcmp(results[i].file_b, "a.go") == 0)) { found_ab = true; ASSERT_EQ(results[i].co_change_count, 3); ASSERT_TRUE(results[i].coupling_score >= 0.7); } } ASSERT_TRUE(found_ab); /* d.go + e.go co-change only 1 time → below threshold, should NOT appear */ for (int i = 0; i < n; i++) { ASSERT_FALSE(strcmp(results[i].file_a, "d.go") == 0 || strcmp(results[i].file_b, "d.go") == 0); } PASS(); } TEST(githistory_coupling_carries_last_co_change) { /* Coupling output must surface the most recent commit timestamp at which * a pair co-changed, so callers can score recency in addition to * frequency. The pair (a.go, b.go) co-changes at three timestamps; the * resulting last_co_change must be the maximum (newest) of those. */ char *files_old[] = {"a.go", "b.go"}; char *files_mid[] = {"a.go", "b.go"}; char *files_new[] = {"a.go", "b.go"}; /* Unrelated co-change in the middle to make sure we don't accidentally * pick up that pair's timestamp by index. */ char *files_other[] = {"c.go", "d.go"}; cbm_commit_files_t commits[] = { {files_old, 2, 1700000000LL}, /* oldest a.go/b.go co-change */ {files_other, 2, 1750000000LL}, /* unrelated pair */ {files_mid, 2, 1720000000LL}, {files_new, 2, 1800000000LL}, /* newest a.go/b.go co-change */ }; cbm_change_coupling_t results[16]; int n = cbm_compute_change_coupling(commits, 4, results, 16); ASSERT_GTE(n, 1); bool found_ab = false; for (int i = 0; i < n; i++) { bool is_ab = (strcmp(results[i].file_a, "a.go") == 0 && strcmp(results[i].file_b, "b.go") == 0) || (strcmp(results[i].file_a, "b.go") == 0 && strcmp(results[i].file_b, "a.go") == 0); if (!is_ab) { continue; } ASSERT_EQ(results[i].co_change_count, 3); /* last_co_change must be the max of the three a.go/b.go timestamps. */ ASSERT_EQ(results[i].last_co_change, 1800000000LL); found_ab = true; } ASSERT_TRUE(found_ab); PASS(); } TEST(githistory_skip_large_commits) { /* A single commit with 25 files → should be skipped (>20) */ char *files[25]; char bufs[25][32]; for (int i = 0; i < 25; i++) { snprintf(bufs[i], sizeof(bufs[i]), "file%d.go", i); files[i] = bufs[i]; } cbm_commit_files_t commits[] = {{files, 25, 0}}; cbm_change_coupling_t results[100]; int n = cbm_compute_change_coupling(commits, 1, results, 100); ASSERT_EQ(n, 0); PASS(); } TEST(githistory_limits_to_max) { /* Create many commits to generate >100 couplings */ /* 50 files, each pair has 3 commits → C(50,2)=1225 pairs, but only * pairs with ≥3 co-changes and score≥0.3 pass the threshold */ int nfiles = 50; int npairs = nfiles * (nfiles - 1) / 2; int ncommits = npairs * 3; cbm_commit_files_t *commits = calloc(ncommits, sizeof(cbm_commit_files_t)); char **file_strs = calloc(nfiles, sizeof(char *)); for (int i = 0; i < nfiles; i++) { file_strs[i] = malloc(32); snprintf(file_strs[i], 32, "f%d.go", i); } int ci = 0; for (int i = 0; i < nfiles; i++) { for (int j = i + 1; j < nfiles; j++) { for (int k = 0; k < 3; k++) { commits[ci].files = malloc(2 * sizeof(char *)); commits[ci].files[0] = file_strs[i]; commits[ci].files[1] = file_strs[j]; commits[ci].count = 2; ci++; } } } /* max_out = 100 → should cap at 100 */ cbm_change_coupling_t results[100]; int n = cbm_compute_change_coupling(commits, ncommits, results, 100); ASSERT_TRUE(n <= 100); /* Cleanup */ for (int i = 0; i < ncommits; i++) { free(commits[i].files); } for (int i = 0; i < nfiles; i++) { free(file_strs[i]); } free(file_strs); free(commits); PASS(); } /* ── Test detection tests ──────────────────────────────────────── */ TEST(testdetect_is_test_file) { /* Test file patterns (all languages) */ ASSERT_TRUE(cbm_is_test_path("foo_test.go")); ASSERT_TRUE(cbm_is_test_path("test_handler.py")); ASSERT_TRUE(cbm_is_test_path("handler.test.js")); ASSERT_TRUE(cbm_is_test_path("handler.spec.ts")); ASSERT_TRUE(cbm_is_test_path("Component.test.tsx")); ASSERT_TRUE(cbm_is_test_path("OrderTest.java")); ASSERT_TRUE(cbm_is_test_path("handler_test.rs")); ASSERT_TRUE(cbm_is_test_path("handler_test.cpp")); ASSERT_TRUE(cbm_is_test_path("OrderTest.cs")); ASSERT_TRUE(cbm_is_test_path("OrderTest.php")); ASSERT_TRUE(cbm_is_test_path("OrderSpec.scala")); ASSERT_TRUE(cbm_is_test_path("OrderTest.kt")); ASSERT_TRUE(cbm_is_test_path("handler_test.lua")); /* Non-test file patterns */ ASSERT_FALSE(cbm_is_test_path("foo.go")); ASSERT_FALSE(cbm_is_test_path("handler.py")); ASSERT_FALSE(cbm_is_test_path("handler.js")); ASSERT_FALSE(cbm_is_test_path("handler.ts")); ASSERT_FALSE(cbm_is_test_path("Component.tsx")); ASSERT_FALSE(cbm_is_test_path("Order.java")); ASSERT_FALSE(cbm_is_test_path("handler.rs")); ASSERT_FALSE(cbm_is_test_path("handler.cpp")); ASSERT_FALSE(cbm_is_test_path("Order.cs")); ASSERT_FALSE(cbm_is_test_path("Order.php")); ASSERT_FALSE(cbm_is_test_path("Order.scala")); ASSERT_FALSE(cbm_is_test_path("Order.kt")); ASSERT_FALSE(cbm_is_test_path("handler.lua")); PASS(); } TEST(testdetect_is_test_function) { /* Test function patterns */ ASSERT_TRUE(cbm_is_test_func_name("TestCreate")); /* Go */ ASSERT_TRUE(cbm_is_test_func_name("test_create")); /* Python/Rust/Lua */ ASSERT_TRUE(cbm_is_test_func_name("test")); /* JS/TS */ ASSERT_TRUE(cbm_is_test_func_name("describe")); /* JS/TS */ ASSERT_TRUE(cbm_is_test_func_name("it")); /* JS/TS */ ASSERT_TRUE(cbm_is_test_func_name("testCreate")); /* Java/PHP/Scala/Kotlin */ ASSERT_TRUE(cbm_is_test_func_name("TestCreate")); /* C++/C# */ /* Non-test function patterns */ ASSERT_FALSE(cbm_is_test_func_name("create")); ASSERT_FALSE(cbm_is_test_func_name("handleRequest")); ASSERT_FALSE(cbm_is_test_func_name("process")); PASS(); } /* ── Implements pass tests (graph buffer based) ────────────────── */ TEST(implements_creates_override) { /* Port of TestPassImplementsCreatesOverrideEdges. * Set up Interface+methods, Class+methods, verify IMPLEMENTS+OVERRIDE. */ cbm_gbuf_t *gb = cbm_gbuf_new("test-proj", "/tmp/test"); ASSERT_NOT_NULL(gb); /* Interface "Reader" with methods "Read" and "Close" */ int64_t iface_id = cbm_gbuf_upsert_node(gb, "Interface", "Reader", "pkg.Reader", "pkg/reader.go", 1, 5, "{}"); ASSERT_GT(iface_id, 0); int64_t read_method_id = cbm_gbuf_upsert_node(gb, "Method", "Read", "pkg.Reader.Read", "pkg/reader.go", 2, 2, "{}"); int64_t close_method_id = cbm_gbuf_upsert_node(gb, "Method", "Close", "pkg.Reader.Close", "pkg/reader.go", 3, 3, "{}"); ASSERT_GT(read_method_id, 0); ASSERT_GT(close_method_id, 0); /* DEFINES_METHOD edges */ cbm_gbuf_insert_edge(gb, iface_id, read_method_id, "DEFINES_METHOD", "{}"); cbm_gbuf_insert_edge(gb, iface_id, close_method_id, "DEFINES_METHOD", "{}"); /* Class "FileReader" with matching methods */ int64_t class_id = cbm_gbuf_upsert_node(gb, "Class", "FileReader", "pkg.FileReader", "pkg/filereader.go", 1, 10, "{}"); ASSERT_GT(class_id, 0); int64_t fr_read_id = cbm_gbuf_upsert_node(gb, "Method", "Read", "pkg.FileReader.Read", "pkg/filereader.go", 2, 4, "{\"receiver\":\"(f *FileReader)\"}"); int64_t fr_close_id = cbm_gbuf_upsert_node(gb, "Method", "Close", "pkg.FileReader.Close", "pkg/filereader.go", 5, 7, "{\"receiver\":\"(f *FileReader)\"}"); ASSERT_GT(fr_read_id, 0); ASSERT_GT(fr_close_id, 0); /* Run Go-style implements detection */ atomic_int cancelled = 0; cbm_pipeline_ctx_t ctx = { .project_name = "test-proj", .repo_path = "/tmp/test", .gbuf = gb, .registry = NULL, .cancelled = &cancelled, }; int edges_created = cbm_pipeline_implements_go(&ctx); ASSERT_GT(edges_created, 0); /* Verify IMPLEMENTS edge: FileReader → Reader */ const cbm_gbuf_edge_t **impl_edges = NULL; int impl_count = 0; int rc = cbm_gbuf_find_edges_by_source_type(gb, class_id, "IMPLEMENTS", &impl_edges, &impl_count); ASSERT_EQ(rc, 0); ASSERT_EQ(impl_count, 1); ASSERT_EQ(impl_edges[0]->target_id, iface_id); /* Verify OVERRIDE edges: FileReader.Read → Reader.Read */ const cbm_gbuf_edge_t **read_overrides = NULL; int read_override_count = 0; rc = cbm_gbuf_find_edges_by_source_type(gb, fr_read_id, "OVERRIDE", &read_overrides, &read_override_count); ASSERT_EQ(rc, 0); ASSERT_EQ(read_override_count, 1); ASSERT_EQ(read_overrides[0]->target_id, read_method_id); /* Verify OVERRIDE edge: FileReader.Close → Reader.Close */ const cbm_gbuf_edge_t **close_overrides = NULL; int close_override_count = 0; rc = cbm_gbuf_find_edges_by_source_type(gb, fr_close_id, "OVERRIDE", &close_overrides, &close_override_count); ASSERT_EQ(rc, 0); ASSERT_EQ(close_override_count, 1); ASSERT_EQ(close_overrides[0]->target_id, close_method_id); cbm_gbuf_free(gb); PASS(); } TEST(implements_no_match) { /* Port of TestPassImplementsNoOverrideWithoutMatch. * Interface requires Read+Write, struct only has Read → no edges. */ cbm_gbuf_t *gb = cbm_gbuf_new("test-proj", "/tmp/test"); ASSERT_NOT_NULL(gb); /* Interface "ReadWriter" with methods "Read" and "Write" */ int64_t iface_id = cbm_gbuf_upsert_node(gb, "Interface", "ReadWriter", "pkg.ReadWriter", "pkg/rw.go", 1, 5, "{}"); int64_t read_id = cbm_gbuf_upsert_node(gb, "Method", "Read", "pkg.ReadWriter.Read", "pkg/rw.go", 2, 2, "{}"); int64_t write_id = cbm_gbuf_upsert_node(gb, "Method", "Write", "pkg.ReadWriter.Write", "pkg/rw.go", 3, 3, "{}"); cbm_gbuf_insert_edge(gb, iface_id, read_id, "DEFINES_METHOD", "{}"); cbm_gbuf_insert_edge(gb, iface_id, write_id, "DEFINES_METHOD", "{}"); /* Struct "OnlyReader" with only "Read" (missing "Write") */ int64_t class_id = cbm_gbuf_upsert_node(gb, "Class", "OnlyReader", "pkg.OnlyReader", "pkg/onlyreader.go", 1, 10, "{}"); int64_t or_read_id = cbm_gbuf_upsert_node(gb, "Method", "Read", "pkg.OnlyReader.Read", "pkg/onlyreader.go", 2, 4, "{\"receiver\":\"(o *OnlyReader)\"}"); /* Run Go-style implements detection */ atomic_int cancelled = 0; cbm_pipeline_ctx_t ctx = { .project_name = "test-proj", .repo_path = "/tmp/test", .gbuf = gb, .registry = NULL, .cancelled = &cancelled, }; int edges_created = cbm_pipeline_implements_go(&ctx); ASSERT_EQ(edges_created, 0); /* Verify NO IMPLEMENTS edge */ const cbm_gbuf_edge_t **impl_edges = NULL; int impl_count = 0; cbm_gbuf_find_edges_by_source_type(gb, class_id, "IMPLEMENTS", &impl_edges, &impl_count); ASSERT_EQ(impl_count, 0); /* Verify NO OVERRIDE edge */ const cbm_gbuf_edge_t **override_edges = NULL; int override_count = 0; cbm_gbuf_find_edges_by_source_type(gb, or_read_id, "OVERRIDE", &override_edges, &override_count); ASSERT_EQ(override_count, 0); /* Suppress unused warnings */ (void)iface_id; (void)read_id; (void)write_id; cbm_gbuf_free(gb); PASS(); } /* ── Usages pass tests (full pipeline integration) ──────────────── */ /* Helper to create a temp dir with a single source file */ static char g_usages_tmpdir[256]; static int setup_usages_repo(const char *filename, const char *content, const char *extra_file, const char *extra_content) { snprintf(g_usages_tmpdir, sizeof(g_usages_tmpdir), "/tmp/cbm_usage_XXXXXX"); if (!cbm_mkdtemp(g_usages_tmpdir)) return -1; /* Check if filename has subdirectory */ char path[512]; snprintf(path, sizeof(path), "%s/%s", g_usages_tmpdir, filename); /* Create subdirectories if needed */ char dir_path[512]; snprintf(dir_path, sizeof(dir_path), "%s", path); char *last_slash = strrchr(dir_path, '/'); if (last_slash) { *last_slash = '\0'; th_mkdir_p(dir_path); } FILE *f = fopen(path, "w"); if (!f) return -1; fprintf(f, "%s", content); fclose(f); if (extra_file && extra_content) { snprintf(path, sizeof(path), "%s/%s", g_usages_tmpdir, extra_file); f = fopen(path, "w"); if (!f) return -1; fprintf(f, "%s", extra_content); fclose(f); } return 0; } static void teardown_usages_repo(void) { if (g_usages_tmpdir[0]) rm_rf(g_usages_tmpdir); g_usages_tmpdir[0] = '\0'; } /* Real cross-file memo failure, after successful extraction. The failure * must reach the public per-file error list on both production drivers. */ static int pipeline_lsp_failure_case(bool parallel, const char *stage, bool python, bool depth) { const char *src = "struct S { int m() { return 1; } };\n" "void run() { S s; s.m(); }\n"; if (python) src = "class S:\n def m(self):\n return 1\ndef run():\n s = S()\n s.m()\n"; const char *filename = python ? "memo.py" : "memo.cpp"; const char *extra_name = NULL; const char *extra_src = NULL; /* The cross stage needs a call the per-file LSP cannot resolve: the parallel * driver skips the cross walk for a file whose sites are all resolved. */ if (strcmp(stage, "cross") == 0) { src = python ? "from memo_def import S\ndef run():\n s = S()\n s.m()\n" : "void run() { S s; s.m(); }\n"; extra_name = python ? "memo_def.py" : "memo_def.cpp"; extra_src = python ? "class S:\n def m(self):\n return 1\n" : "struct S { int m() { return 1; } };\n"; } if (setup_usages_repo(filename, src, extra_name, extra_src) != 0) FAIL("failed to create memo fixture"); char path[512]; if (parallel) { for (int i = 0; i < 51; i++) { snprintf(path, sizeof(path), "%s/memo_pad_%02d.%s", g_usages_tmpdir, i, python ? "py" : "cpp"); if (th_write_file(path, python ? "# inert parallel routing fixture\n" : "// inert parallel routing fixture\n") != 0) { teardown_usages_repo(); FAIL("failed to create parallel routing fixture"); } } } const char *keys[] = { "CBM_WORKERS", "CBM_INDEX_SINGLE_THREAD", depth ? (python ? "CBM_TEST_PY_LSP_DEPTH_FAIL_STAGE" : "CBM_TEST_C_LSP_DEPTH_FAIL_STAGE") : (python ? "CBM_TEST_PY_LSP_MEMO_FAIL_STAGE" : "CBM_TEST_C_LSP_MEMO_FAIL_STAGE")}; char *saved[3] = {0}; bool env_saved = true; for (int i = 0; i < 3; i++) { const char *old = getenv(keys[i]); saved[i] = old ? strdup(old) : NULL; env_saved = env_saved && (!old || saved[i]); } if (!env_saved) { for (int i = 0; i < 3; i++) free(saved[i]); teardown_usages_repo(); FAIL("failed to save memo fixture environment"); } int set_rc = cbm_setenv(keys[0], parallel ? "4" : "1", 1); set_rc |= parallel ? cbm_unsetenv(keys[1]) : cbm_setenv(keys[1], "1", 1); set_rc |= cbm_setenv(keys[2], stage, 1); snprintf(path, sizeof(path), "%s/memo.db", g_usages_tmpdir); cbm_pipeline_t *p = set_rc == 0 ? cbm_pipeline_new(g_usages_tmpdir, path, CBM_MODE_FULL) : NULL; int run_rc = p ? cbm_pipeline_run(p) : -1; cbm_file_error_t *errors = NULL; int error_count = 0; cbm_pipeline_get_file_errors(p, &errors, &error_count); int memo_errors = 0; const char *expected_phase = strcmp(stage, "cross") == 0 ? "lsp_skipped" : "extract"; bool precise = true; for (int i = 0; i < error_count; i++) { if (errors[i].path && strcmp(errors[i].path, filename) == 0) { memo_errors++; precise = precise && errors[i].phase && strcmp(errors[i].phase, expected_phase) == 0 && errors[i].reason && strcmp(errors[i].reason, depth ? (python ? CBM_PY_LSP_DEPTH_ERROR : CBM_C_LSP_DEPTH_ERROR) : (python ? CBM_PY_LSP_MEMO_ERROR : CBM_C_LSP_MEMO_ERROR)) == 0; } } cbm_pipeline_free(p); int restore_rc = 0; for (int i = 0; i < 3; i++) { restore_rc |= saved[i] ? cbm_setenv(keys[i], saved[i], 1) : cbm_unsetenv(keys[i]); free(saved[i]); } teardown_usages_repo(); ASSERT_EQ(set_rc, 0); ASSERT_EQ(restore_rc, 0); ASSERT_EQ(run_rc, 0); ASSERT_EQ(memo_errors, 1); ASSERT_TRUE(precise); PASS(); } TEST(pipeline_issue1527_cross_memo_failure_sequential) { return pipeline_lsp_failure_case(false, "cross", false, false); } TEST(pipeline_issue1527_cross_memo_failure_parallel) { return pipeline_lsp_failure_case(true, "cross", false, false); } TEST(pipeline_issue1527_raw_memo_failure_sequential) { return pipeline_lsp_failure_case(false, "raw", false, false); } TEST(pipeline_issue1527_raw_memo_failure_parallel) { return pipeline_lsp_failure_case(true, "raw", false, false); } TEST(pipeline_issue1527_python_raw_memo_failure_sequential) { return pipeline_lsp_failure_case(false, "raw", true, false); } TEST(pipeline_issue1527_python_raw_memo_failure_parallel) { return pipeline_lsp_failure_case(true, "raw", true, false); } TEST(pipeline_issue1527_python_cross_memo_failure_sequential) { return pipeline_lsp_failure_case(false, "cross", true, false); } TEST(pipeline_issue1527_python_cross_memo_failure_parallel) { return pipeline_lsp_failure_case(true, "cross", true, false); } TEST(pipeline_issue1527_c_raw_depth_failure_sequential) { return pipeline_lsp_failure_case(false, "raw", false, true); } TEST(pipeline_issue1527_c_cross_depth_failure_sequential) { return pipeline_lsp_failure_case(false, "cross", false, true); } TEST(pipeline_issue1527_c_raw_depth_failure_parallel) { return pipeline_lsp_failure_case(true, "raw", false, true); } TEST(pipeline_issue1527_c_cross_depth_failure_parallel) { return pipeline_lsp_failure_case(true, "cross", false, true); } TEST(pipeline_issue1527_python_raw_depth_failure_sequential) { return pipeline_lsp_failure_case(false, "raw", true, true); } TEST(pipeline_issue1527_python_cross_depth_failure_sequential) { return pipeline_lsp_failure_case(false, "cross", true, true); } TEST(pipeline_issue1527_python_raw_depth_failure_parallel) { return pipeline_lsp_failure_case(true, "raw", true, true); } TEST(pipeline_issue1527_python_cross_depth_failure_parallel) { return pipeline_lsp_failure_case(true, "cross", true, true); } TEST(pipeline_issue1527_cross_memo_failure_fallback) { const char *src = "struct S { int m() { return 1; } }; void run() { S s; s.m(); }"; CBMLSPDef defs[] = { {.qualified_name = "test.S", .short_name = "S", .label = "Class", .lang = CBM_LANG_CPP}, {.qualified_name = "test.S.m", .short_name = "m", .label = "Method", .receiver_type = "test.S", .return_types = "int", .lang = CBM_LANG_CPP}, }; CBMFileResult result = {0}; cbm_arena_init(&result.arena); cbm_c_lsp_test_memo_fail_after(0); cbm_pxc_run_one(CBM_LANG_CPP, &result, src, (int)strlen(src), "test", defs, 2, NULL, NULL, 0); cbm_c_lsp_test_memo_fail_after(-1); bool reported = result.has_error && result.lsp_skipped && result.error_msg && strcmp(result.error_msg, CBM_C_LSP_MEMO_ERROR) == 0 && result.resolved_calls.count == 0; cbm_arena_destroy(&result.arena); ASSERT_TRUE(reported); PASS(); } static bool python_module_write_fixture_file(const char *root, const char *name, const char *text) { char path[768]; int n = snprintf(path, sizeof(path), "%s/%s", root, name); return n > 0 && (size_t)n < sizeof(path) && th_write_file(path, text) == 0; } /* An absent node, failed edge query, or heuristic edge must not satisfy this * integration check. Exact source/target identities precede strategy checks. */ static bool python_module_exact_lsp_edge(cbm_store_t *store, const char *project, const char *caller, const char *caller_label, const char *target, const char *target_label) { char source_qn[768], target_qn[768]; int sn = snprintf(source_qn, sizeof(source_qn), "%s.%s", project, caller); int tn = snprintf(target_qn, sizeof(target_qn), "%s.%s", project, target); if (sn <= 0 || (size_t)sn >= sizeof(source_qn) || tn <= 0 || (size_t)tn >= sizeof(target_qn)) return false; cbm_node_t source = {0}, destination = {0}; bool correct = cbm_store_find_node_by_qn(store, project, source_qn, &source) == CBM_STORE_OK && cbm_store_find_node_by_qn(store, project, target_qn, &destination) == CBM_STORE_OK && source.id > 0 && destination.id > 0 && source.label && destination.label && strcmp(source.label, caller_label) == 0 && strcmp(destination.label, target_label) == 0; cbm_edge_t *edges = NULL; int edge_count = 0, hits = 0; if (correct) { correct = cbm_store_find_edges_by_source_type(store, source.id, "CALLS", &edges, &edge_count) == CBM_STORE_OK; for (int i = 0; correct && i < edge_count; i++) { if (edges[i].source_id != source.id || edges[i].target_id != destination.id) continue; hits++; const char *properties = edges[i].properties_json; yyjson_doc *doc = properties ? yyjson_read(properties, strlen(properties), 0) : NULL; const char *strategy = doc ? yyjson_get_str(yyjson_obj_get(yyjson_doc_get_root(doc), "strategy")) : NULL; correct = strategy && strncmp(strategy, "lsp_", 4) == 0; if (doc) yyjson_doc_free(doc); } } cbm_store_free_edges(edges, edge_count); cbm_node_free_fields(&source); cbm_node_free_fields(&destination); return correct && hits == 1; } static int python_module_callers_pipeline_case(bool parallel) { static const char helpers[] = "def helper():\n" " return 1\n" "class Service:\n" " def run(self):\n" " return 2\n"; static const char app[] = "from helpers import helper, Service\n" "def local_helper():\n" " return 3\n" "class Local:\n" " def run(self):\n" " return 4\n" "class Controller:\n" " def run(self):\n" " helper()\n" " method_service = Service()\n" " method_service.run()\n" "def control():\n" " helper()\n" " function_service = Service()\n" " function_service.run()\n" " local_helper()\n" " function_local = Local()\n" " function_local.run()\n" "helper()\n" "module_service = Service()\n" "module_service.run()\n" "local_helper()\n" "module_local = Local()\n" "module_local.run()\n"; static const struct { const char *caller; const char *caller_label; const char *target; const char *target_label; } expected[] = { {"app", "Module", "helpers.helper", "Function"}, {"app", "Module", "helpers.Service.run", "Method"}, {"app", "Module", "app.local_helper", "Function"}, {"app", "Module", "app.Local.run", "Method"}, {"app.control", "Function", "helpers.helper", "Function"}, {"app.control", "Function", "helpers.Service.run", "Method"}, {"app.control", "Function", "app.local_helper", "Function"}, {"app.control", "Function", "app.Local.run", "Method"}, {"app.Controller.run", "Method", "helpers.helper", "Function"}, {"app.Controller.run", "Method", "helpers.Service.run", "Method"}, {"pkg.__init__", "Module", "helpers.helper", "Function"}, }; char root[512]; int n = snprintf(root, sizeof(root), "%s/cbm-python-module-XXXXXX", cbm_tmpdir()); if (n <= 0 || (size_t)n >= sizeof(root) || !cbm_mkdtemp(root)) FAIL("failed to create Python module fixture"); char path[768]; n = snprintf(path, sizeof(path), "%s/pkg", root); bool written = n > 0 && (size_t)n < sizeof(path); if (written) { cbm_mkdir_p(path, 0755); written = python_module_write_fixture_file(root, "helpers.py", helpers) && python_module_write_fixture_file(root, "app.py", app) && python_module_write_fixture_file(root, "pkg/__init__.py", "from helpers import helper\n" "PACKAGE_VALUE = helper()\n"); } for (int i = 0; written && parallel && i < 52; i++) { char name[64]; snprintf(name, sizeof(name), "module_pad_%02d.py", i); written = python_module_write_fixture_file(root, name, "padding = 0\n"); } if (!written) { th_rmtree(root); FAIL("failed to write Python module fixture"); } const char *keys[] = {"CBM_WORKERS", "CBM_INDEX_SINGLE_THREAD"}; char *saved[2] = {0}; bool saved_ok = true; for (int i = 0; i < 2; i++) { const char *old = getenv(keys[i]); saved[i] = old ? strdup(old) : NULL; saved_ok = saved_ok && (!old || saved[i]); } if (!saved_ok) { free(saved[0]); free(saved[1]); th_rmtree(root); FAIL("failed to save Python module fixture environment"); } int set_rc = cbm_setenv(keys[0], parallel ? "4" : "1", 1); set_rc |= parallel ? cbm_unsetenv(keys[1]) : cbm_setenv(keys[1], "1", 1); n = snprintf(path, sizeof(path), "%s/module.db", root); cbm_pipeline_t *pipeline = set_rc == 0 && n > 0 && (size_t)n < sizeof(path) ? cbm_pipeline_new(root, path, CBM_MODE_FULL) : NULL; int run_rc = pipeline ? cbm_pipeline_run(pipeline) : -1; cbm_file_error_t *errors = NULL; int error_count = 0; cbm_pipeline_get_file_errors(pipeline, &errors, &error_count); cbm_store_t *store = run_rc == 0 ? cbm_store_open_path(path) : NULL; bool correct = store != NULL; int file_count = 0; if (store) { const char *project = cbm_pipeline_project_name(pipeline); cbm_node_t *files = NULL; correct = project && cbm_store_find_nodes_by_label(store, project, "File", &files, &file_count) == CBM_STORE_OK && file_count == (parallel ? 55 : 3); cbm_store_free_nodes(files, file_count); for (size_t i = 0; i < sizeof(expected) / sizeof(expected[0]); i++) { bool edge = project && python_module_exact_lsp_edge( store, project, expected[i].caller, expected[i].caller_label, expected[i].target, expected[i].target_label); correct = correct && edge; } /* Neither the ordinary nor package Module should acquire a fake * __module__ owner node. A failed lookup query is not absence proof. */ const char *phantoms[] = {"app.__module__", "pkg.__init__.__module__"}; for (int i = 0; project && i < 2; i++) { char qn[768]; n = snprintf(qn, sizeof(qn), "%s.%s", project, phantoms[i]); cbm_node_t node = {0}; bool absent = n > 0 && (size_t)n < sizeof(qn) && cbm_store_find_node_by_qn(store, project, qn, &node) == CBM_STORE_NOT_FOUND; correct = correct && absent; cbm_node_free_fields(&node); } cbm_store_close(store); } cbm_pipeline_free(pipeline); int restore_rc = 0; for (int i = 0; i < 2; i++) { restore_rc |= saved[i] ? cbm_setenv(keys[i], saved[i], 1) : cbm_unsetenv(keys[i]); free(saved[i]); } int cleanup_rc = th_rmtree(root); ASSERT_EQ(set_rc, 0); ASSERT_EQ(restore_rc, 0); ASSERT_EQ(cleanup_rc, 0); ASSERT_EQ(run_rc, 0); ASSERT_EQ(error_count, 0); ASSERT_TRUE(correct); PASS(); } /* Insert after python_module_exact_lsp_edge. Parent registers the four TESTs. */ static const char namespace_admission_control[] = "def target():\n" " return 1\n" "def keep():\n" " return target()\n"; static const char *const namespace_admission_env_keys[] = {"CBM_WORKERS", "CBM_INDEX_SINGLE_THREAD"}; typedef struct { char root[512]; char db[768]; char *saved[2]; bool created; bool saved_env; } namespace_admission_fixture; typedef struct { int rc; int errors; int format; cbm_incremental_route_t route; bool migrated; bool healthy; char project[512]; char *json; } namespace_admission_result; static bool namespace_admission_begin(namespace_admission_fixture *f) { int n = snprintf(f->root, sizeof(f->root), "%s/cbm-namespace-admission-XXXXXX", cbm_tmpdir()); if (n <= 0 || (size_t)n >= sizeof(f->root) || !cbm_mkdtemp(f->root)) return false; f->created = true; n = snprintf(f->db, sizeof(f->db), "%s/namespace.db", f->root); if (n <= 0 || (size_t)n >= sizeof(f->db)) return false; for (int i = 0; i < 2; i++) { const char *old = getenv(namespace_admission_env_keys[i]); f->saved[i] = old ? strdup(old) : NULL; if (old && !f->saved[i]) return false; } f->saved_env = true; int env_rc = cbm_setenv(namespace_admission_env_keys[0], "1", 1); env_rc |= cbm_setenv(namespace_admission_env_keys[1], "1", 1); return env_rc == 0 && python_module_write_fixture_file(f->root, "control.py", namespace_admission_control); } static bool namespace_admission_finish(namespace_admission_fixture *f) { int restore_rc = 0; for (int i = 0; i < 2; i++) { if (f->saved_env) restore_rc |= f->saved[i] ? cbm_setenv(namespace_admission_env_keys[i], f->saved[i], 1) : cbm_unsetenv(namespace_admission_env_keys[i]); free(f->saved[i]); } cbm_pipeline_incremental_test_reset_faults(); int cleanup_rc = f->created ? th_rmtree(f->root) : 0; return restore_rc == 0 && cleanup_rc == 0; } static bool namespace_admission_exact_hash(cbm_store_t *store, const char *project, const char *path, const char *source) { cbm_file_hash_t row = {0}; char expected[CBM_SHA256_HEX_LEN + 1]; cbm_sha256_hex(source, strlen(source), expected); bool correct = cbm_store_get_file_hash(store, project, path, &row) == CBM_STORE_OK && row.sha256 && strcmp(row.sha256, expected) == 0; cbm_store_clear_file_hash(&row); return correct; } static namespace_admission_result namespace_admission_run(namespace_admission_fixture *f, const char *watch, const char *source, int expected_files) { namespace_admission_result result = { .rc = -1, .errors = -1, .format = -1, .route = CBM_INCREMENTAL_ROUTE_NONE}; cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *pipeline = cbm_pipeline_new(f->root, f->db, CBM_MODE_FULL); if (!pipeline) return result; result.rc = cbm_pipeline_run(pipeline); result.route = cbm_pipeline_incremental_test_last_route(); result.migrated = cbm_pipeline_had_format_migration(pipeline); cbm_file_error_t *errors = NULL; cbm_pipeline_get_file_errors(pipeline, &errors, &result.errors); const char *project = cbm_pipeline_project_name(pipeline); int n = project ? snprintf(result.project, sizeof(result.project), "%s", project) : -1; cbm_store_t *store = result.rc == 0 && n > 0 && (size_t)n < sizeof(result.project) ? cbm_store_open_path(f->db) : NULL; if (store) { cbm_node_t *files = NULL; int file_count = 0; bool file_query = cbm_store_find_nodes_by_label(store, project, "File", &files, &file_count) == CBM_STORE_OK; int watched_files = 0; for (int i = 0; file_query && i < file_count; i++) if (files[i].file_path && strcmp(files[i].file_path, watch) == 0) watched_files++; cbm_store_free_nodes(files, file_count); bool positive = python_module_exact_lsp_edge(store, project, "control.keep", "Function", "control.target", "Function"); bool hashes = namespace_admission_exact_hash(store, project, "control.py", namespace_admission_control) && namespace_admission_exact_hash(store, project, watch, source); bool format_ok = cbm_store_get_format_version(store, &result.format) == CBM_STORE_OK; cbm_lsp_surface_row_t *rows = NULL; int row_count = 0; bool row_query = cbm_store_get_lsp_surfaces(store, project, &rows, &row_count) == CBM_STORE_OK; int matches = 0; for (int i = 0; row_query && i < row_count; i++) { if (!rows[i].rel_path || strcmp(rows[i].rel_path, watch) != 0) continue; matches++; if (rows[i].defs_json && !result.json) result.json = strdup(rows[i].defs_json); } cbm_store_free_lsp_surfaces(rows, row_count); bool python_row = matches == 1 && result.json && cbm_lsp_surface_python_change(result.json, result.json) == 0 && cbm_lsp_surface_python_change(result.json, NULL) == 1; result.healthy = result.errors == 0 && file_query && file_count == expected_files && watched_files == 1 && positive && hashes && format_ok && result.format == CBM_INDEX_FORMAT_VERSION && row_query && python_row; cbm_store_close(store); } cbm_pipeline_free(pipeline); return result; } TEST(pipeline_python_namespace_admission_upgrade_then_noop) { namespace_admission_fixture fixture = {0}; bool setup = namespace_admission_begin(&fixture); namespace_admission_result first = {0}, upgraded = {0}, unchanged = {0}; bool stamped = false; if (setup) { first = namespace_admission_run(&fixture, "control.py", namespace_admission_control, 1); cbm_store_t *store = first.healthy ? cbm_store_open_path(fixture.db) : NULL; if (store) { int old_format = -1; stamped = cbm_store_set_format_version(store, 1) == CBM_STORE_OK && cbm_store_get_format_version(store, &old_format) == CBM_STORE_OK && old_format == 1; cbm_store_close(store); } if (stamped) upgraded = namespace_admission_run(&fixture, "control.py", namespace_admission_control, 1); if (upgraded.healthy) unchanged = namespace_admission_run(&fixture, "control.py", namespace_admission_control, 1); } bool stable = first.json && upgraded.json && unchanged.json && cbm_lsp_surface_python_change(first.json, upgraded.json) == 0 && cbm_lsp_surface_python_change(upgraded.json, unchanged.json) == 0; free(first.json); free(upgraded.json); free(unchanged.json); bool cleaned = namespace_admission_finish(&fixture); ASSERT_TRUE(setup && stamped && cleaned); ASSERT_TRUE(first.healthy && upgraded.healthy && unchanged.healthy && stable); ASSERT_TRUE(upgraded.migrated); ASSERT_FALSE(unchanged.migrated); ASSERT_EQ(unchanged.route, CBM_INCREMENTAL_ROUTE_NOOP); PASS(); } TEST(pipeline_python_namespace_admission_body_alias_and_all) { static const char initial[] = "from math import floor as Alias\n" "__all__ = ['Alias']\n" "def body():\n return 1\n"; static const char body_edit[] = "from math import floor as Alias\n" "__all__ = ['Alias']\n" "def body():\n return 2\n"; static const char alias_edit[] = "from math import ceil as Alias\n" "__all__ = ['Alias']\n" "def body():\n return 2\n"; static const char all_edit[] = "from math import ceil as Alias\n" "__all__ = []\n" "def body():\n return 2\n"; namespace_admission_fixture fixture = {0}; bool setup = namespace_admission_begin(&fixture); namespace_admission_result phases[4] = {0}; const char *sources[] = {initial, body_edit, alias_edit, all_edit}; bool wrote_all = setup; for (int i = 0; i < 4 && wrote_all; i++) { wrote_all = python_module_write_fixture_file(fixture.root, "subject.py", sources[i]); if (wrote_all) phases[i] = namespace_admission_run(&fixture, "subject.py", sources[i], 2); if (!phases[i].healthy) break; } bool all_healthy = phases[0].healthy && phases[1].healthy && phases[2].healthy && phases[3].healthy; bool comparisons = all_healthy && cbm_lsp_surface_python_change(phases[0].json, phases[1].json) == 0 && cbm_lsp_surface_python_change(phases[1].json, phases[2].json) == 1 && cbm_lsp_surface_python_change(phases[2].json, phases[3].json) == 1; for (int i = 0; i < 4; i++) free(phases[i].json); bool cleaned = namespace_admission_finish(&fixture); ASSERT_TRUE(setup && wrote_all && cleaned && all_healthy && comparisons); ASSERT_EQ(phases[1].route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); ASSERT_EQ(phases[2].route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(phases[3].route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); PASS(); } TEST(pipeline_python_namespace_admission_deleted_and_empty) { const char *sources[] = {"def removed():\n return 3\n", "# empty namespace\n"}; bool setups[2] = {false, false}, removed[2] = {false, false}; bool healthy[2] = {false, false}, absent[2] = {false, false}, cleaned[2] = {false, false}; cbm_incremental_route_t routes[2] = {CBM_INCREMENTAL_ROUTE_NONE, CBM_INCREMENTAL_ROUTE_NONE}; for (int i = 0; i < 2; i++) { namespace_admission_fixture fixture = {0}; setups[i] = namespace_admission_begin(&fixture); if (setups[i]) setups[i] = python_module_write_fixture_file(fixture.root, "gone.py", sources[i]); namespace_admission_result before = {0}, after = {0}; if (setups[i]) before = namespace_admission_run(&fixture, "gone.py", sources[i], 2); if (before.healthy) { char path[768]; int n = snprintf(path, sizeof(path), "%s/gone.py", fixture.root); removed[i] = n > 0 && (size_t)n < sizeof(path) && cbm_unlink(path) == 0; } if (removed[i]) after = namespace_admission_run(&fixture, "control.py", namespace_admission_control, 1); healthy[i] = before.healthy && after.healthy; routes[i] = after.route; cbm_store_t *store = after.healthy ? cbm_store_open_path(fixture.db) : NULL; if (store) { cbm_node_t *nodes = NULL; int count = 0; absent[i] = cbm_store_find_nodes_by_file(store, after.project, "gone.py", &nodes, &count) == CBM_STORE_OK && count == 0; cbm_store_free_nodes(nodes, count); cbm_store_close(store); } free(before.json); free(after.json); cleaned[i] = namespace_admission_finish(&fixture); } for (int i = 0; i < 2; i++) { ASSERT_TRUE(setups[i] && removed[i] && healthy[i] && absent[i] && cleaned[i]); ASSERT_EQ(routes[i], CBM_INCREMENTAL_ROUTE_FORCED_FULL); } PASS(); } TEST(pipeline_python_namespace_admission_invalid_equal_hash) { static const char initial[] = "def body():\n return 1\n"; static const char edited[] = "def body():\n return 2\n"; namespace_admission_fixture fixture = {0}; bool setup = namespace_admission_begin(&fixture); if (setup) setup = python_module_write_fixture_file(fixture.root, "subject.py", initial); namespace_admission_result before = {0}, after = {0}; bool corrupted = false, wrote = false; char *old_sha = NULL, *new_sha = NULL; if (setup) before = namespace_admission_run(&fixture, "subject.py", initial, 2); cbm_store_t *store = before.healthy ? cbm_store_open_path(fixture.db) : NULL; if (store) { cbm_lsp_surface_row_t *rows = NULL; int count = 0; int rc = cbm_store_get_lsp_surfaces(store, before.project, &rows, &count); for (int i = 0; rc == CBM_STORE_OK && i < count; i++) { if (!rows[i].rel_path || strcmp(rows[i].rel_path, "subject.py") != 0) continue; old_sha = rows[i].surface_sha ? strdup(rows[i].surface_sha) : NULL; cbm_lsp_surface_row_t invalid = rows[i]; invalid.defs_json = "{}"; /* Leave the old hash intact: validate before equality. */ corrupted = old_sha && cbm_lsp_surface_python_change("{}", "{}") == -1 && cbm_store_upsert_lsp_surface_batch(store, &invalid, 1) == CBM_STORE_OK; break; } cbm_store_free_lsp_surfaces(rows, count); rows = NULL; count = 0; bool confirmed = false; rc = corrupted ? cbm_store_get_lsp_surfaces(store, before.project, &rows, &count) : CBM_STORE_ERR; for (int i = 0; rc == CBM_STORE_OK && i < count; i++) { if (!rows[i].rel_path || strcmp(rows[i].rel_path, "subject.py") != 0) continue; confirmed = rows[i].defs_json && strcmp(rows[i].defs_json, "{}") == 0 && rows[i].surface_sha && strcmp(rows[i].surface_sha, old_sha) == 0; break; } corrupted = corrupted && confirmed; cbm_store_free_lsp_surfaces(rows, count); cbm_store_close(store); } if (corrupted) wrote = python_module_write_fixture_file(fixture.root, "subject.py", edited); if (wrote) after = namespace_admission_run(&fixture, "subject.py", edited, 2); store = after.healthy ? cbm_store_open_path(fixture.db) : NULL; if (store) { cbm_lsp_surface_row_t *rows = NULL; int count = 0; int rc = cbm_store_get_lsp_surfaces(store, after.project, &rows, &count); for (int i = 0; rc == CBM_STORE_OK && i < count; i++) { if (rows[i].rel_path && strcmp(rows[i].rel_path, "subject.py") == 0 && rows[i].surface_sha) { new_sha = strdup(rows[i].surface_sha); break; } } cbm_store_free_lsp_surfaces(rows, count); cbm_store_close(store); } bool equal_hash = old_sha && new_sha && strcmp(old_sha, new_sha) == 0; bool equal_valid_surface = before.healthy && after.healthy && cbm_lsp_surface_python_change(before.json, after.json) == 0; free(old_sha); free(new_sha); free(before.json); free(after.json); bool cleaned = namespace_admission_finish(&fixture); ASSERT_TRUE(setup && corrupted && wrote && cleaned); ASSERT_TRUE(before.healthy && after.healthy && equal_hash && equal_valid_surface); ASSERT_EQ(after.route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); PASS(); } TEST(pipeline_python_module_callers_sequential) { return python_module_callers_pipeline_case(false); } TEST(pipeline_python_module_callers_parallel) { return python_module_callers_pipeline_case(true); } /* Distinct targets keep each newly walked class/decorator site observable * even though the stored graph aggregates calls by caller/target pair. */ static int python_class_body_pipeline_case(bool parallel) { static const char helpers[] = "def top_field():\n return 1\n" "def nested_field():\n return 2\n" "def class_decorator(value):\n return lambda target: target\n" "def class_argument():\n return 3\n" "def method_decorator(value):\n return lambda target: target\n" "def method_argument():\n return 4\n" "def method_control():\n return 5\n" "def local_field():\n return 6\n" "def method_local_field():\n return 7\n" "def base_factory():\n return object\n" "def package_field():\n return 8\n" "def package_decorator(value):\n return lambda target: target\n" "def package_argument():\n return 9\n"; static const char app[] = "from helpers import top_field, nested_field, class_decorator, class_argument\n" "from helpers import method_decorator, method_argument, method_control\n" "from helpers import local_field, method_local_field, base_factory\n" "class Base(base_factory()):\n" " value = top_field()\n" " @method_decorator(method_argument())\n" " def run(self):\n" " method_control()\n" " class MethodLocal:\n" " value = method_local_field()\n" "@class_decorator(class_argument())\n" "class Admin:\n" " class Meta:\n" " value = nested_field()\n" "def outer():\n" " class Local:\n" " value = local_field()\n" " return Local\n"; static const char package[] = "from helpers import package_field, package_decorator, package_argument\n" "@package_decorator(package_argument())\n" "class Package:\n" " value = package_field()\n"; static const struct { const char *caller; const char *caller_label; const char *target; } expected[] = { {"app", "Module", "helpers.base_factory"}, {"app", "Module", "helpers.top_field"}, {"app", "Module", "helpers.nested_field"}, {"app", "Module", "helpers.class_decorator"}, {"app", "Module", "helpers.class_argument"}, {"app", "Module", "helpers.method_decorator"}, {"app", "Module", "helpers.method_argument"}, {"app.Base.run", "Method", "helpers.method_control"}, {"app.Base.run", "Method", "helpers.method_local_field"}, {"app.outer", "Function", "helpers.local_field"}, {"pkg.__init__", "Module", "helpers.package_decorator"}, {"pkg.__init__", "Module", "helpers.package_argument"}, {"pkg.__init__", "Module", "helpers.package_field"}, }; char root[512], path[768]; int n = snprintf(root, sizeof(root), "%s/cbm-python-class-body-XXXXXX", cbm_tmpdir()); if (n <= 0 || (size_t)n >= sizeof(root) || !cbm_mkdtemp(root)) FAIL("failed to create Python class-body fixture"); n = snprintf(path, sizeof(path), "%s/pkg", root); bool written = n > 0 && (size_t)n < sizeof(path); if (written) { cbm_mkdir_p(path, 0755); written = python_module_write_fixture_file(root, "helpers.py", helpers) && python_module_write_fixture_file(root, "app.py", app) && python_module_write_fixture_file(root, "pkg/__init__.py", package); } for (int i = 0; written && parallel && i < 52; i++) { char name[64]; snprintf(name, sizeof(name), "class_body_pad_%02d.py", i); written = python_module_write_fixture_file(root, name, "# inert parallel routing file\n"); } if (!written) { th_rmtree(root); FAIL("failed to write Python class-body fixture"); } const char *keys[] = {"CBM_WORKERS", "CBM_INDEX_SINGLE_THREAD"}; char *saved[2] = {0}; bool saved_ok = true; for (int i = 0; i < 2; i++) { const char *old = getenv(keys[i]); saved[i] = old ? strdup(old) : NULL; saved_ok = saved_ok && (!old || saved[i]); } if (!saved_ok) { free(saved[0]); free(saved[1]); th_rmtree(root); FAIL("failed to save Python class-body fixture environment"); } int set_rc = cbm_setenv(keys[0], parallel ? "4" : "1", 1); set_rc |= parallel ? cbm_unsetenv(keys[1]) : cbm_setenv(keys[1], "1", 1); n = snprintf(path, sizeof(path), "%s/class-body.db", root); cbm_pipeline_t *pipeline = set_rc == 0 && n > 0 && (size_t)n < sizeof(path) ? cbm_pipeline_new(root, path, CBM_MODE_FULL) : NULL; int run_rc = pipeline ? cbm_pipeline_run(pipeline) : -1; cbm_file_error_t *errors = NULL; int error_count = 0; cbm_pipeline_get_file_errors(pipeline, &errors, &error_count); cbm_store_t *store = run_rc == 0 ? cbm_store_open_path(path) : NULL; bool correct = store != NULL; if (store) { const char *project = cbm_pipeline_project_name(pipeline); cbm_node_t *files = NULL; int file_count = 0; correct = project && cbm_store_find_nodes_by_label(store, project, "File", &files, &file_count) == CBM_STORE_OK && file_count == (parallel ? 55 : 3); cbm_store_free_nodes(files, file_count); for (size_t i = 0; i < sizeof(expected) / sizeof(expected[0]); i++) { bool edge = project && python_module_exact_lsp_edge(store, project, expected[i].caller, expected[i].caller_label, expected[i].target, "Function"); correct = correct && edge; } cbm_store_close(store); } cbm_pipeline_free(pipeline); int restore_rc = 0; for (int i = 0; i < 2; i++) { restore_rc |= saved[i] ? cbm_setenv(keys[i], saved[i], 1) : cbm_unsetenv(keys[i]); free(saved[i]); } int cleanup_rc = th_rmtree(root); ASSERT_EQ(set_rc, 0); ASSERT_EQ(restore_rc, 0); ASSERT_EQ(cleanup_rc, 0); ASSERT_EQ(run_rc, 0); ASSERT_EQ(error_count, 0); ASSERT_TRUE(correct); PASS(); } TEST(pipeline_python_class_body_calls_sequential) { return python_class_body_pipeline_case(false); } TEST(pipeline_python_class_body_calls_parallel) { return python_class_body_pipeline_case(true); } /* Python file identity keeps .__init__; its exported symbols do not. */ static bool python_init_local_inheritance(cbm_store_t *store, const char *project, const char *module) { char child_qn[768], parent_qn[768], wrong_qn[768]; int cn = snprintf(child_qn, sizeof(child_qn), "%s.%s.BooleanField", project, module); int pn = snprintf(parent_qn, sizeof(parent_qn), "%s.%s.Field", project, module); int wn = snprintf(wrong_qn, sizeof(wrong_qn), "%s.other.Field", project); if (cn <= 0 || (size_t)cn >= sizeof(child_qn) || pn <= 0 || (size_t)pn >= sizeof(parent_qn) || wn <= 0 || (size_t)wn >= sizeof(wrong_qn)) return false; cbm_node_t child = {0}, parent = {0}, wrong = {0}; bool ok = cbm_store_find_node_by_qn(store, project, child_qn, &child) == CBM_STORE_OK && cbm_store_find_node_by_qn(store, project, parent_qn, &parent) == CBM_STORE_OK && cbm_store_find_node_by_qn(store, project, wrong_qn, &wrong) == CBM_STORE_OK && child.id > 0 && parent.id > 0 && wrong.id > 0 && parent.id != wrong.id && child.label && parent.label && wrong.label && strcmp(child.label, "Class") == 0 && strcmp(parent.label, "Class") == 0 && strcmp(wrong.label, "Class") == 0; cbm_edge_t *edges = NULL; int count = 0, local = 0, foreign = 0; if (ok) { ok = cbm_store_find_edges_by_source_type(store, child.id, "INHERITS", &edges, &count) == CBM_STORE_OK; for (int i = 0; ok && i < count; i++) { local += edges[i].target_id == parent.id; foreign += edges[i].target_id == wrong.id; } } cbm_store_free_edges(edges, count); cbm_node_free_fields(&child); cbm_node_free_fields(&parent); cbm_node_free_fields(&wrong); return ok && local == 1 && foreign == 0; } static int python_init_scope_pipeline_case(bool parallel, const char *failure_stage) { static const char body[] = "from util import normalize\n" "from other import Field as OtherField\n" "def helper(value):\n return normalize(value)\n" "class Field:\n" " def clean(self, value):\n return helper(value)\n" " def validate(self, value):\n return self.clean(value)\n" "class BooleanField(Field):\n" " def to_python(self, value):\n" " self.validate(value)\n return helper(value)\n" "def build(value):\n" " field = BooleanField()\n" " field.to_python(value)\n return helper(value)\n"; static const char consumer[] = "from pkg import BooleanField, helper\n" "def consume(value):\n" " field = BooleanField()\n" " field.to_python(value)\n return helper(value)\n"; static const struct { const char *caller; const char *caller_label; const char *target; const char *target_label; } local_edges[] = { {"Field.clean", "Method", "helper", "Function"}, {"Field.validate", "Method", "Field.clean", "Method"}, {"BooleanField.to_python", "Method", "Field.validate", "Method"}, {"BooleanField.to_python", "Method", "helper", "Function"}, {"build", "Function", "BooleanField.to_python", "Method"}, {"build", "Function", "helper", "Function"}, }; char root[512], path[768]; int n = snprintf(root, sizeof(root), "%s/cbm-python-init-XXXXXX", cbm_tmpdir()); if (n <= 0 || (size_t)n >= sizeof(root) || !cbm_mkdtemp(root)) FAIL("failed to create Python package fixture"); n = snprintf(path, sizeof(path), "%s/pkg", root); bool written = n > 0 && (size_t)n < sizeof(path); /* The cross and register stages run inside the cross-file walk, which the * parallel driver skips for a file whose call sites the per-file LSP * already resolved: those stages need a call only the cross walk resolves. */ bool cross_walk_stage = failure_stage && (strcmp(failure_stage, "cross") == 0 || strcmp(failure_stage, "register") == 0); if (written) { cbm_mkdir_p(path, 0755); written = python_module_write_fixture_file( root, "pkg/__init__.py", cross_walk_stage ? "from memo_def import S\n" "def run():\n s = S()\n return s.m()\n" : failure_stage ? "class S:\n def m(self):\n return 1\n" "def run():\n s = S()\n return s.m()\n" : body); } if (written && cross_walk_stage) { written = python_module_write_fixture_file(root, "memo_def.py", "class S:\n def m(self):\n return 1\n"); } if (written && !failure_stage) { written = python_module_write_fixture_file(root, "plain.py", body) && python_module_write_fixture_file(root, "consumer.py", consumer) && python_module_write_fixture_file(root, "util.py", "def normalize(value):\n return value\n") && python_module_write_fixture_file(root, "other.py", "class Field:\n pass\n"); } for (int i = 0; written && parallel && i < 52; i++) { char name[64]; snprintf(name, sizeof(name), "init_pad_%02d.py", i); written = python_module_write_fixture_file(root, name, "# inert parallel routing file\n"); } if (!written) { th_rmtree(root); FAIL("failed to write Python package fixture"); } const char *keys[] = {"CBM_WORKERS", "CBM_INDEX_SINGLE_THREAD", "CBM_TEST_PY_LSP_SCOPE_FAIL_STAGE"}; char *saved[3] = {0}; bool saved_ok = true; for (int i = 0; i < 3; i++) { const char *old = getenv(keys[i]); saved[i] = old ? strdup(old) : NULL; saved_ok = saved_ok && (!old || saved[i]); } if (!saved_ok) { for (int i = 0; i < 3; i++) free(saved[i]); th_rmtree(root); FAIL("failed to save Python package fixture environment"); } int set_rc = cbm_setenv(keys[0], parallel ? "4" : "1", 1); set_rc |= parallel ? cbm_unsetenv(keys[1]) : cbm_setenv(keys[1], "1", 1); set_rc |= failure_stage ? cbm_setenv(keys[2], failure_stage, 1) : cbm_unsetenv(keys[2]); n = snprintf(path, sizeof(path), "%s/package.db", root); cbm_pipeline_t *pipeline = set_rc == 0 && n > 0 && (size_t)n < sizeof(path) ? cbm_pipeline_new(root, path, CBM_MODE_FULL) : NULL; int run_rc = pipeline ? cbm_pipeline_run(pipeline) : -1; cbm_file_error_t *errors = NULL; int error_count = 0, scope_errors = 0; bool precise = true; cbm_pipeline_get_file_errors(pipeline, &errors, &error_count); for (int i = 0; i < error_count; i++) { bool is_package = errors[i].path && strcmp(errors[i].path, "pkg/__init__.py") == 0; if (failure_stage && is_package) { scope_errors++; const char *phase = strcmp(failure_stage, "raw") == 0 ? "extract" : "lsp_skipped"; precise = precise && errors[i].phase && strcmp(errors[i].phase, phase) == 0 && errors[i].reason && strcmp(errors[i].reason, CBM_PY_LSP_SCOPE_ERROR) == 0; } else { precise = false; } } cbm_store_t *store = run_rc == 0 ? cbm_store_open_path(path) : NULL; bool correct = store != NULL; if (store) { const char *project = cbm_pipeline_project_name(pipeline); cbm_node_t *files = NULL; int file_count = 0; correct = project && cbm_store_find_nodes_by_label(store, project, "File", &files, &file_count) == CBM_STORE_OK && file_count == (cross_walk_stage ? 2 : failure_stage ? 1 : 5) + (parallel ? 52 : 0); cbm_store_free_nodes(files, file_count); if (project && !failure_stage) { const char *modules[] = {"plain", "pkg"}; for (int m = 0; m < 2; m++) { for (size_t i = 0; i < sizeof(local_edges) / sizeof(local_edges[0]); i++) { char caller[128], target[128]; snprintf(caller, sizeof(caller), "%s.%s", modules[m], local_edges[i].caller); snprintf(target, sizeof(target), "%s.%s", modules[m], local_edges[i].target); bool edge = python_module_exact_lsp_edge(store, project, caller, local_edges[i].caller_label, target, local_edges[i].target_label); correct = correct && edge; } char caller[128]; snprintf(caller, sizeof(caller), "%s.helper", modules[m]); bool imported = python_module_exact_lsp_edge(store, project, caller, "Function", "util.normalize", "Function"); bool inherits = python_init_local_inheritance(store, project, modules[m]); correct = correct && imported && inherits; } bool method = python_module_exact_lsp_edge(store, project, "consumer.consume", "Function", "pkg.BooleanField.to_python", "Method"); bool function = python_module_exact_lsp_edge(store, project, "consumer.consume", "Function", "pkg.helper", "Function"); correct = correct && method && function; } cbm_store_close(store); } cbm_pipeline_free(pipeline); int restore_rc = 0; for (int i = 0; i < 3; i++) { restore_rc |= saved[i] ? cbm_setenv(keys[i], saved[i], 1) : cbm_unsetenv(keys[i]); free(saved[i]); } int cleanup_rc = th_rmtree(root); ASSERT_EQ(set_rc, 0); ASSERT_EQ(restore_rc, 0); ASSERT_EQ(cleanup_rc, 0); ASSERT_EQ(run_rc, 0); ASSERT_EQ(error_count, failure_stage ? 1 : 0); ASSERT_EQ(scope_errors, failure_stage ? 1 : 0); ASSERT_TRUE(precise && correct); PASS(); } TEST(pipeline_python_init_scope_sequential) { return python_init_scope_pipeline_case(false, NULL); } TEST(pipeline_python_init_scope_parallel) { return python_init_scope_pipeline_case(true, NULL); } TEST(pipeline_python_init_raw_scope_failure_sequential) { return python_init_scope_pipeline_case(false, "raw"); } TEST(pipeline_python_init_raw_scope_failure_parallel) { return python_init_scope_pipeline_case(true, "raw"); } TEST(pipeline_python_init_cross_scope_failure_sequential) { return python_init_scope_pipeline_case(false, "cross"); } TEST(pipeline_python_init_cross_scope_failure_parallel) { return python_init_scope_pipeline_case(true, "cross"); } TEST(pipeline_python_init_register_scope_failure_sequential) { return python_init_scope_pipeline_case(false, "register"); } TEST(pipeline_python_init_register_scope_failure_parallel) { return python_init_scope_pipeline_case(true, "register"); } /* Compare identities, not just counts; a duplicate import must stop at the * same nearest eligible entry even when it contributes no new selections. */ static bool python_init_filter_is(CBMLSPDef *defs, int def_count, const char *const *imports, int import_count, const char *const *expected, int count) { CBMModuleDefIndex *index = cbm_pxc_build_module_def_index(defs, def_count); int got_count = -1; bool succeeded = false; CBMLSPDef *got = cbm_pxc_filter_defs_for_file(index, defs, CBM_LANG_PYTHON, NULL, "test.consumer", imports, import_count, &got_count, &succeeded); bool correct = index && succeeded && got_count == count && (count == 0 || got); for (int i = 0; correct && i < count; i++) { int hits = 0; for (int j = 0; j < got_count; j++) hits += got[j].qualified_name && strcmp(got[j].qualified_name, expected[i]) == 0; correct = hits == 1; } free(got); cbm_pxc_free_module_def_index(index); return correct; } TEST(pipeline_python_init_import_filter_precedence) { CBMLSPDef defs[] = { {.qualified_name = "test.consumer.use", .def_module_qn = "test.consumer", .lang = CBM_LANG_PYTHON}, {.qualified_name = "test.pkg.Store", .def_module_qn = "test.pkg.__init__", .lang = CBM_LANG_PYTHON}, {.qualified_name = "test.pkg.Foreign", .def_module_qn = "test.pkg.__init__", .lang = CBM_LANG_GO}, {.qualified_name = "test.Parent", .def_module_qn = "test.__init__", .lang = CBM_LANG_PYTHON}, {.qualified_name = "test.foreign.Only", .def_module_qn = "test.foreign.__init__", .lang = CBM_LANG_GO}, {.qualified_name = "test.unknown.Only", .def_module_qn = "test.unknown.__init__", .lang = (CBMLanguage)-1}, {.qualified_name = "test.plain.Exact", .def_module_qn = "test.plain", .lang = CBM_LANG_JAVA}, {.qualified_name = "test.plain.Package", .def_module_qn = "test.plain.__init__", .lang = CBM_LANG_PYTHON}, {.qualified_name = "test.pkg.nested.Item", .def_module_qn = "test.pkg.nested.__init__", .lang = CBM_LANG_PYTHON}, {.qualified_name = "test.pkg.sub.Item", .def_module_qn = "test.pkg.sub", .lang = CBM_LANG_PYTHON}, }; const char *pkg[] = {"test.pkg", "test.pkg.Store", "test.pkg.Store"}; const char *want_pkg[] = {"test.consumer.use", "test.pkg.Store"}; const char *foreign[] = {"test.foreign.Only", "test.unknown.Only"}; const char *want_parent[] = {"test.consumer.use", "test.Parent"}; const char *normal[] = {"test.plain.Exact", "test.plain.Exact"}; const char *want_normal[] = {"test.consumer.use", "test.plain.Exact"}; const char *nested[] = {"test.pkg.nested.Item", "test.pkg.nested.Item"}; const char *want_nested[] = {"test.consumer.use", "test.pkg.nested.Item"}; const char *submodule[] = {"test.pkg.sub.Item"}; const char *want_submodule[] = {"test.consumer.use", "test.pkg.sub.Item"}; int n = (int)(sizeof(defs) / sizeof(defs[0])); bool pkg_ok = python_init_filter_is(defs, n, pkg, 3, want_pkg, 2); bool foreign_ok = python_init_filter_is(defs, n, foreign, 2, want_parent, 2); bool normal_ok = python_init_filter_is(defs, n, normal, 2, want_normal, 2); bool nested_ok = python_init_filter_is(defs, n, nested, 2, want_nested, 2); bool submodule_ok = python_init_filter_is(defs, n, submodule, 1, want_submodule, 2); ASSERT_TRUE(pkg_ok && foreign_ok && normal_ok && nested_ok && submodule_ok); PASS(); } TEST(pipeline_python_init_import_probe_failure_falls_back) { const char *source = "from pkg import Store\n" "def use():\n store = Store()\n return store.get()\n"; CBMLSPDef defs[] = { {.qualified_name = "test.consumer.use", .short_name = "use", .label = "Function", .def_module_qn = "test.consumer", .lang = CBM_LANG_PYTHON}, {.qualified_name = "test.pkg.Store", .short_name = "Store", .label = "Class", .def_module_qn = "test.pkg.__init__", .lang = CBM_LANG_PYTHON}, {.qualified_name = "test.pkg.Store.get", .short_name = "get", .label = "Method", .def_module_qn = "test.pkg.__init__", .receiver_type = "test.pkg.Store", .return_types = "int", .lang = CBM_LANG_PYTHON}, }; const char *keys[] = {"Store"}; const char *values[] = {"test.pkg.Store"}; CBMModuleDefIndex *index = cbm_pxc_build_module_def_index(defs, 3); int count = -1; bool succeeded = true; cbm_pxc_test_import_probe_fail_after(0); CBMLSPDef *filtered = cbm_pxc_filter_defs_for_file( index, defs, CBM_LANG_PYTHON, NULL, "test.consumer", values, 1, &count, &succeeded); cbm_pxc_test_import_probe_fail_after(-1); bool failed_precisely = index && !filtered && count == 0 && !succeeded; free(filtered); CBMFileResult result = {0}; cbm_arena_init(&result.arena); cbm_pxc_test_import_probe_fail_after(0); cbm_pxc_dispatch_file(CBM_LANG_PYTHON, &result, source, (int)strlen(source), "consumer.py", "test.consumer", NULL, index, defs, 3, keys, values, 1, NULL, NULL); cbm_pxc_test_import_probe_fail_after(-1); const char *site = strstr(source, "store.get()"); int exact = 0; for (int i = 0; i < result.resolved_calls.count; i++) { const CBMResolvedCall *call = &result.resolved_calls.items[i]; if (call->caller_qn && strcmp(call->caller_qn, "test.consumer.use") == 0 && call->callee_qn && strcmp(call->callee_qn, "test.pkg.Store.get") == 0 && call->kind == CBM_RESOLVED_INVOCATION && call->strategy && strncmp(call->strategy, "lsp_", 4) == 0 && site && call->site_start_byte == (uint32_t)(site - source) && call->site_end_byte == (uint32_t)(site - source + strlen("store.get()"))) exact++; } bool resolved = !result.has_error && !result.lsp_skipped && exact == 1; cbm_arena_destroy(&result.arena); cbm_pxc_free_module_def_index(index); ASSERT_TRUE(failed_precisely && resolved); PASS(); } /* #1277: typed instance fields of a Python class imported from another file. * Counts CALLS edges caller -> callee (QN suffixes) whose properties name an * LSP strategy. */ static int count_calls_with(cbm_store_t *s, const char *project, const char *caller_suffix, const char *callee_suffix, const char *fragment); static int count_lsp_calls(cbm_store_t *s, const char *project, const char *caller_suffix, const char *callee_suffix) { return count_calls_with(s, project, caller_suffix, callee_suffix, "lsp"); } /* As count_lsp_calls, with any properties fragment (NULL = every CALLS edge). */ static int count_calls_with(cbm_store_t *s, const char *project, const char *caller_suffix, const char *callee_suffix, const char *fragment) { cbm_edge_t *edges = NULL; int edge_count = 0; cbm_store_find_edges_by_type(s, project, "CALLS", &edges, &edge_count); int hits = 0; for (int i = 0; i < edge_count; i++) { cbm_node_t src = {0}; cbm_node_t tgt = {0}; if (cbm_store_find_node_by_id(s, edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK) { size_t sl = strlen(src.qualified_name); size_t tl = strlen(tgt.qualified_name); size_t cs = strlen(caller_suffix); size_t ts = strlen(callee_suffix); if (sl >= cs && tl >= ts && strcmp(src.qualified_name + sl - cs, caller_suffix) == 0 && strcmp(tgt.qualified_name + tl - ts, callee_suffix) == 0 && (!fragment || (edges[i].properties_json && strstr(edges[i].properties_json, fragment)))) { hits++; } } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (edges) cbm_store_free_edges(edges, edge_count); return hits; } static int check_python_crossfile_typed_field_calls_issue1277(bool parallel) { const char *contracts = "class Contract:\n" " def process_batch(self) -> None:\n" " ...\n"; const char *trainer = "from contracts import Contract\n\n" "def make():\n" " return None\n\n" "class Trainer:\n" " engine: Contract\n\n" " def __init__(self, strategies: Contract) -> None:\n" " self.strategies = strategies\n" " self.typed: Contract = strategies\n" " self.plain = make()\n"; const char *loop = "from trainer import Trainer\n\n" "def run(trainer: Trainer) -> None:\n" " strategies = trainer.strategies\n" " strategies.process_batch()\n\n" "def run_typed(trainer: Trainer) -> None:\n" " trainer.typed.process_batch()\n\n" "def run_classlevel(trainer: Trainer) -> None:\n" " trainer.engine.process_batch()\n\n" "def run_untyped(trainer: Trainer) -> None:\n" " trainer.plain.process_batch()\n"; if (setup_usages_repo("contracts.py", contracts, "trainer.py", trainer) != 0) { FAIL("failed to create temp dir"); } char path[512]; snprintf(path, sizeof(path), "%s/loop.py", g_usages_tmpdir); if (th_write_file(path, loop) != 0) { teardown_usages_repo(); FAIL("failed to write loop.py"); } if (parallel) { /* Three semantic files plus 50 inert files exceed the production * threshold for the fused parallel pipeline. */ for (int i = 0; i < 50; i++) { snprintf(path, sizeof(path), "%s/field_pad_%02d.py", g_usages_tmpdir, i); if (th_write_file(path, "field_padding = 0\n") != 0) { teardown_usages_repo(); FAIL("failed to write parallel-selection fixture"); } } } const char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; if ((old_workers && !saved_workers) || (old_single && !saved_single)) { free(saved_workers); free(saved_single); teardown_usages_repo(); FAIL("failed to save pipeline environment"); } int pin_workers_rc = parallel ? cbm_setenv("CBM_WORKERS", "4", 1) : 0; int pin_single_rc = parallel ? cbm_unsetenv("CBM_INDEX_SINGLE_THREAD") : 0; char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_1277.db", g_usages_tmpdir); cbm_pipeline_t *p = pin_workers_rc == 0 && pin_single_rc == 0 ? cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL) : NULL; int run_rc = p ? cbm_pipeline_run(p) : -1; cbm_store_t *s = run_rc == 0 ? cbm_store_open_path(db_path) : NULL; bool store_opened = s != NULL; int via_alias = -1, via_typed = -1, via_class = -1, via_untyped = -1; if (s) { const char *project = cbm_pipeline_project_name(p); via_alias = count_lsp_calls(s, project, "loop.run", "contracts.Contract.process_batch"); via_typed = count_lsp_calls(s, project, "loop.run_typed", "contracts.Contract.process_batch"); via_class = count_lsp_calls(s, project, "loop.run_classlevel", "contracts.Contract.process_batch"); via_untyped = count_lsp_calls(s, project, "loop.run_untyped", "process_batch"); cbm_store_close(s); } cbm_pipeline_free(p); int restore_workers_rc = saved_workers ? cbm_setenv("CBM_WORKERS", saved_workers, 1) : cbm_unsetenv("CBM_WORKERS"); int restore_single_rc = saved_single ? cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1) : cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); free(saved_workers); free(saved_single); teardown_usages_repo(); ASSERT_EQ(pin_workers_rc, 0); ASSERT_EQ(pin_single_rc, 0); ASSERT_EQ(restore_workers_rc, 0); ASSERT_EQ(restore_single_rc, 0); ASSERT_EQ(run_rc, 0); ASSERT_TRUE(store_opened); ASSERT_EQ(via_alias, 1); /* self.x = */ ASSERT_EQ(via_typed, 1); /* self.x: T = ... */ ASSERT_EQ(via_class, 1); /* class-level x: T */ ASSERT_EQ(via_untyped, 0); /* control: no annotation, no guess */ PASS(); } TEST(python_crossfile_typed_field_calls_issue1277) { return check_python_crossfile_typed_field_calls_issue1277(false); } TEST(python_crossfile_typed_field_calls_issue1277_parallel) { return check_python_crossfile_typed_field_calls_issue1277(true); } /* #1277, second half: the reporter's chain with both annotation imports * under `if TYPE_CHECKING:` (the usual shape when the import would cycle). * trainer.strategies is typed through a TYPE_CHECKING import of Contract, and * run()'s parameter through a typing.TYPE_CHECKING import of Trainer; the call * resolves by the LSP exactly as with runtime imports. A runtime call through * a TYPE_CHECKING-only name cannot happen: it gets no CALLS edge at all. */ static int check_python_type_checking_typed_field_issue1277(bool parallel) { const char *contracts = "class Contract:\n" " def process_batch(self) -> None:\n" " ...\n"; const char *trainer = "from __future__ import annotations\n" "from typing import TYPE_CHECKING\n\n" "if TYPE_CHECKING:\n" " from contracts import Contract\n\n" "class Trainer:\n" " def __init__(self, strategies: Contract) -> None:\n" " self.strategies = strategies\n"; const char *loop = "from __future__ import annotations\n" "import typing\n\n" "if typing.TYPE_CHECKING:\n" " from trainer import Trainer\n\n" "def run(trainer: Trainer) -> None:\n" " trainer.strategies.process_batch()\n\n" "def bad():\n" " return Trainer()\n"; if (setup_usages_repo("contracts.py", contracts, "trainer.py", trainer) != 0) { FAIL("failed to create temp dir"); } char path[512]; snprintf(path, sizeof(path), "%s/loop.py", g_usages_tmpdir); if (th_write_file(path, loop) != 0) { teardown_usages_repo(); FAIL("failed to write loop.py"); } if (parallel) { for (int i = 0; i < 50; i++) { snprintf(path, sizeof(path), "%s/tc_pad_%02d.py", g_usages_tmpdir, i); if (th_write_file(path, "tc_padding = 0\n") != 0) { teardown_usages_repo(); FAIL("failed to write parallel-selection fixture"); } } } const char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; if ((old_workers && !saved_workers) || (old_single && !saved_single)) { free(saved_workers); free(saved_single); teardown_usages_repo(); FAIL("failed to save pipeline environment"); } int pin_workers_rc = parallel ? cbm_setenv("CBM_WORKERS", "4", 1) : 0; int pin_single_rc = parallel ? cbm_unsetenv("CBM_INDEX_SINGLE_THREAD") : 0; char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_1277_tc.db", g_usages_tmpdir); cbm_pipeline_t *p = pin_workers_rc == 0 && pin_single_rc == 0 ? cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL) : NULL; int run_rc = p ? cbm_pipeline_run(p) : -1; cbm_store_t *s = run_rc == 0 ? cbm_store_open_path(db_path) : NULL; bool store_opened = s != NULL; int via_field = -1, runtime_import = -1, runtime_lsp = -1; if (s) { const char *project = cbm_pipeline_project_name(p); via_field = count_lsp_calls(s, project, "loop.run", "contracts.Contract.process_batch"); runtime_import = count_calls_with(s, project, "loop.bad", "trainer.Trainer", "\"strategy\":\"import_map"); runtime_lsp = count_lsp_calls(s, project, "loop.bad", "trainer.Trainer"); cbm_store_close(s); } cbm_pipeline_free(p); int restore_workers_rc = saved_workers ? cbm_setenv("CBM_WORKERS", saved_workers, 1) : cbm_unsetenv("CBM_WORKERS"); int restore_single_rc = saved_single ? cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1) : cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); free(saved_workers); free(saved_single); teardown_usages_repo(); ASSERT_EQ(pin_workers_rc, 0); ASSERT_EQ(pin_single_rc, 0); ASSERT_EQ(restore_workers_rc, 0); ASSERT_EQ(restore_single_rc, 0); ASSERT_EQ(run_rc, 0); ASSERT_TRUE(store_opened); ASSERT_EQ(via_field, 1); /* annotation through TYPE_CHECKING imports */ ASSERT_EQ(runtime_import, 0); /* control: no CALLS import map binds the name */ ASSERT_EQ(runtime_lsp, 0); /* control: the LSP binds it for annotations only */ PASS(); } TEST(python_type_checking_typed_field_issue1277) { return check_python_type_checking_typed_field_issue1277(false); } TEST(python_type_checking_typed_field_issue1277_parallel) { return check_python_type_checking_typed_field_issue1277(true); } /* A base class spelled through a root-binding import (`import pkg.mod.nodes` * binds `pkg`; `class X(pkg.mod.nodes.Context)`) is the same class as the * from-imported spelling. The Python LSP's inheritance view never resolved the * dotted spelling, so a member inherited through it (self.m(), super().m(), * super().__init__()) fell to a weak name guess or to nothing. The plain * from-import control must keep resolving; the untyped receiver must not. */ static int check_python_dotted_base_inheritance(bool parallel) { const char *nodes = "class Context:\n" " def __init__(self, line=-1):\n" " self.line = line\n\n" " def ctx_only_helper(self):\n" " return 1\n\n" " def describe(self):\n" " return 0\n"; const char *types = "import pkg.mod.nodes\n" "from pkg.mod.nodes import Context\n\n" "class DottedType(pkg.mod.nodes.Context):\n" " def __init__(self, line=-1):\n" " super().__init__(line)\n\n" " def run(self):\n" " return self.ctx_only_helper()\n\n" " def describe(self):\n" " return super().describe()\n\n" "class PlainType(Context):\n" " def run(self):\n" " return self.ctx_only_helper()\n\n" "def untyped(thing):\n" " return thing.ctx_only_helper()\n"; if (setup_usages_repo("pkg/mod/nodes.py", nodes, "pkg/types.py", types) != 0) { FAIL("failed to create temp dir"); } char path[512]; const char *inits[] = {"pkg/__init__.py", "pkg/mod/__init__.py"}; for (size_t i = 0; i < sizeof(inits) / sizeof(inits[0]); i++) { snprintf(path, sizeof(path), "%s/%s", g_usages_tmpdir, inits[i]); if (th_write_file(path, "") != 0) { teardown_usages_repo(); FAIL("failed to write package marker"); } } if (parallel) { for (int i = 0; i < 50; i++) { snprintf(path, sizeof(path), "%s/dotted_pad_%02d.py", g_usages_tmpdir, i); if (th_write_file(path, "dotted_padding = 0\n") != 0) { teardown_usages_repo(); FAIL("failed to write parallel-selection fixture"); } } } const char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; if ((old_workers && !saved_workers) || (old_single && !saved_single)) { free(saved_workers); free(saved_single); teardown_usages_repo(); FAIL("failed to save pipeline environment"); } int pin_workers_rc = parallel ? cbm_setenv("CBM_WORKERS", "4", 1) : 0; int pin_single_rc = parallel ? cbm_unsetenv("CBM_INDEX_SINGLE_THREAD") : 0; char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_dotted_base.db", g_usages_tmpdir); cbm_pipeline_t *p = pin_workers_rc == 0 && pin_single_rc == 0 ? cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL) : NULL; int run_rc = p ? cbm_pipeline_run(p) : -1; cbm_store_t *s = run_rc == 0 ? cbm_store_open_path(db_path) : NULL; bool store_opened = s != NULL; int dotted_self = -1, dotted_super = -1, dotted_init = -1, plain_self = -1, untyped = -1; if (s) { const char *project = cbm_pipeline_project_name(p); dotted_self = count_lsp_calls(s, project, "pkg.types.DottedType.run", "pkg.mod.nodes.Context.ctx_only_helper"); dotted_super = count_lsp_calls(s, project, "pkg.types.DottedType.describe", "pkg.mod.nodes.Context.describe"); dotted_init = count_lsp_calls(s, project, "pkg.types.DottedType.__init__", "pkg.mod.nodes.Context.__init__"); plain_self = count_lsp_calls(s, project, "pkg.types.PlainType.run", "pkg.mod.nodes.Context.ctx_only_helper"); untyped = count_lsp_calls(s, project, "pkg.types.untyped", "ctx_only_helper"); cbm_store_close(s); } cbm_pipeline_free(p); int restore_workers_rc = saved_workers ? cbm_setenv("CBM_WORKERS", saved_workers, 1) : cbm_unsetenv("CBM_WORKERS"); int restore_single_rc = saved_single ? cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1) : cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); free(saved_workers); free(saved_single); teardown_usages_repo(); ASSERT_EQ(pin_workers_rc, 0); ASSERT_EQ(pin_single_rc, 0); ASSERT_EQ(restore_workers_rc, 0); ASSERT_EQ(restore_single_rc, 0); ASSERT_EQ(run_rc, 0); ASSERT_TRUE(store_opened); ASSERT_EQ(dotted_self, 1); /* self.m() inherited through a dotted base */ ASSERT_EQ(dotted_super, 1); /* super().m() through a dotted base */ ASSERT_EQ(dotted_init, 1); /* super().__init__() through a dotted base */ ASSERT_EQ(plain_self, 1); /* control: from-imported base */ ASSERT_EQ(untyped, 0); /* control: no receiver type, no LSP edge */ PASS(); } TEST(python_dotted_base_inheritance) { return check_python_dotted_base_inheritance(false); } TEST(python_dotted_base_inheritance_parallel) { return check_python_dotted_base_inheritance(true); } /* Properties of the `type` edge src_qn -> tgt_qn (strdup'd; caller frees), or NULL. */ static char *py_reexp_edge_props(cbm_store_t *s, const char *project, const char *src_qn, const char *tgt_qn, const char *type) { cbm_node_t src = {0}; cbm_node_t tgt = {0}; char *props = NULL; if (cbm_store_find_node_by_qn(s, project, src_qn, &src) == CBM_STORE_OK && cbm_store_find_node_by_qn(s, project, tgt_qn, &tgt) == CBM_STORE_OK) { cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(s, src.id, type, &edges, &ec); for (int i = 0; i < ec && !props; i++) { if (edges[i].target_id == tgt.id) props = strdup(edges[i].properties_json ? edges[i].properties_json : "{}"); } if (edges) cbm_store_free_edges(edges, ec); } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); return props; } /* Package re-exports, end to end: extraction records each module's ordered * namespace facts, the collector turns them into re-export records, the * shared Python registry folds them into its export table, and the LSP * consults it. `models.CharField` with `from .fields import *` in * models/__init__.py is the django shape. Each app function makes one call so * every edge's strategy is attributable. */ static const char *const py_reexport_calls[][2] = { {"attr_explicit_cls", "pkg.explicit_mod.Explicit"}, {"attr_explicit_fn", "pkg.explicit_mod.explicit_fn"}, {"attr_star_cls", "pkg.star_mod.Star"}, {"attr_star_fn", "pkg.star_mod.star_fn"}, {"attr_all_listed", "pkg.all_mod.Listed"}, {"attr_chain", "pkg.chain.inner.Deep"}, {"attr_renamed", "pkg.alias_mod.Orig"}, {"from_explicit", "pkg.explicit_mod.Explicit"}, {"from_star_fn", "pkg.star_mod.star_fn"}, {"from_all_listed", "pkg.all_mod.Listed"}, {"from_chain", "pkg.chain.inner.Deep"}, {"from_renamed", "pkg.alias_mod.Orig"}, {"cycle_a", "cyc.a.a_fn"}, {"cycle_b", "cyc.b.b_fn"}, {"sub_models", "db.models.fields.CharField"}, {"chain_var", "pkg.chain.inner.Deep.dive"}, {"ctl_from_cls", "direct.impl.Direct"}, {"ctl_from_fn", "direct.impl.direct_fn"}, {"lazy_tc", "lz.lazy_mod.Lazy"}, {"doc_reexport", "docp.impl.Doc"}, {"go_reexport", "gop.impl.Thing"}, }; enum { PY_REEXPORT_N_CALLS = sizeof(py_reexport_calls) / sizeof(py_reexport_calls[0]) }; /* Calls that must NOT reach their target by the LSP: an `__all__`-unlisted * name, a private name under a star, a re-export the module shadows later, * one made before a whole-namespace barrier (a top-level `try:`), a * re-export a submodule import of `__go` replaces, and one behind a `__go` * that writes its parameter. */ static const char *const py_reexport_excluded[][2] = { {"excl_unlisted", "pkg.all_mod.Unlisted"}, {"excl_private", "pkg.star_mod._private_fn"}, {"excl_shadowed", "shad.impl.Gone"}, {"excl_barrier", "barr.impl.Before"}, {"excl_compat", "cmp.types_only.Shim"}, {"excl_fstring", "fsp.impl.Kept"}, {"excl_go_submodule", "gop.impl.base"}, {"excl_go_param", "gox.impl.Kept"}, }; enum { PY_REEXPORT_N_EXCLUDED = sizeof(py_reexport_excluded) / sizeof(py_reexport_excluded[0]) }; /* Index the fixture; lsp[c] = call c's edge has an lsp_* strategy; excluded[e] * = excluded call e reached its target by the LSP anyway. */ static int py_reexport_run(bool parallel, bool incremental, bool *lsp, bool *excluded) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_reexport_XXXXXX"); if (!cbm_mkdtemp(tmp)) { return -1; } write_temp_file(tmp, "app/__init__.py", ""); write_temp_file(tmp, "app/pkg/__init__.py", "from .explicit_mod import Explicit, explicit_fn\n" "from .star_mod import *\n" "from .all_mod import *\n" "from .chain import *\n" "from .alias_mod import Orig as Renamed\n"); write_temp_file(tmp, "app/pkg/explicit_mod.py", "class Explicit:\n pass\n\n\ndef explicit_fn():\n return 1\n"); write_temp_file(tmp, "app/pkg/star_mod.py", "class Star:\n pass\n\n\n" "def star_fn():\n return 1\n\n\ndef _private_fn():\n return 0\n"); write_temp_file(tmp, "app/pkg/all_mod.py", "__all__ = [\"Listed\"]\n\n\nclass Listed:\n pass\n\n\n" "class Unlisted:\n pass\n"); write_temp_file(tmp, "app/pkg/chain/__init__.py", "from .inner import Deep\n"); write_temp_file(tmp, "app/pkg/chain/inner.py", "class Deep:\n def dive(self):\n return 1\n"); write_temp_file(tmp, "app/pkg/alias_mod.py", "class Orig:\n pass\n"); write_temp_file(tmp, "app/cyc/__init__.py", "from .a import *\nfrom .b import *\n"); write_temp_file(tmp, "app/cyc/a.py", "from .b import *\n\n\ndef a_fn():\n return 1\n"); write_temp_file(tmp, "app/cyc/b.py", "from .a import *\n\n\ndef b_fn():\n return 2\n"); write_temp_file(tmp, "app/db/__init__.py", ""); write_temp_file(tmp, "app/db/models/__init__.py", "from app.db.models.fields import *\n"); write_temp_file(tmp, "app/db/models/fields/__init__.py", "__all__ = [\"CharField\"]\n\n\nclass CharField:\n pass\n"); write_temp_file(tmp, "app/direct/__init__.py", ""); write_temp_file(tmp, "app/direct/impl.py", "class Direct:\n pass\n\n\ndef direct_fn():\n return 1\n"); write_temp_file(tmp, "app/shad/__init__.py", "from .impl import Gone\nGone = None\n"); write_temp_file(tmp, "app/shad/impl.py", "class Gone:\n pass\n"); write_temp_file( tmp, "app/barr/__init__.py", "from .impl import Before\n" "try:\n import optional_dep\nexcept ImportError:\n optional_dep = None\n"); write_temp_file(tmp, "app/barr/impl.py", "class Before:\n pass\n"); write_temp_file(tmp, "app/lz/__init__.py", "from typing import TYPE_CHECKING\n" "if TYPE_CHECKING:\n from .lazy_mod import Lazy\n" "else:\n import sys\n"); write_temp_file(tmp, "app/lz/lazy_mod.py", "class Lazy:\n pass\n"); write_temp_file(tmp, "app/cmp/__init__.py", "from typing import TYPE_CHECKING\n" "if TYPE_CHECKING:\n from .types_only import Shim\n" "else:\n try:\n from .runtime_impl import Shim\n" " except ImportError:\n Shim = None\n"); write_temp_file(tmp, "app/cmp/types_only.py", "class Shim:\n pass\n"); write_temp_file(tmp, "app/cmp/runtime_impl.py", "class Shim:\n pass\n"); /* The sql/expression.py shape of sqlalchemy: docstrings and a __future__ * import around the re-exports; an f-string with a field stays a barrier. */ write_temp_file(tmp, "app/docp/__init__.py", "\"\"\"Package docstring.\"\"\"\n" "from __future__ import annotations\n" "from .impl import Doc\n" "alias = Doc\n" "\"\"\"Attribute docstring.\"\"\"\n"); write_temp_file(tmp, "app/docp/impl.py", "class Doc:\n pass\n"); write_temp_file(tmp, "app/fsp/__init__.py", "from .impl import Kept\n" "f\"{Kept}\"\n"); write_temp_file(tmp, "app/fsp/impl.py", "class Kept:\n pass\n"); /* sqlalchemy's sql/__init__.py shape: a top-level `__go(locals())`. */ write_temp_file(tmp, "app/gop/__init__.py", "from typing import Any\n" "from .impl import Thing\n" "from .impl import base\n\n\n" "def __go(lcls: Any) -> None:\n" " from . import base\n" " from .annotation import prepare\n" " prepare(Thing)\n\n\n" "__go(locals())\n"); write_temp_file(tmp, "app/gop/impl.py", "class Thing:\n pass\n\n\ndef base():\n return 1\n"); write_temp_file(tmp, "app/gop/base.py", "def helper():\n return 1\n"); write_temp_file(tmp, "app/gop/annotation.py", "def prepare(cls):\n return cls\n"); write_temp_file(tmp, "app/gox/__init__.py", "from .impl import Kept\n\n\n" "def __go(lcls):\n lcls[\"Kept\"] = None\n\n\n" "__go(locals())\n"); write_temp_file(tmp, "app/gox/impl.py", "class Kept:\n pass\n"); write_temp_file(tmp, "app/main.py", "from app import pkg\n" "from app import cyc\n" "from app import shad\n" "from app import barr\n" "from app import lz\n" "from app import cmp\n" "from app import docp\n" "from app import fsp\n" "from app import gop\n" "from app import gox\n" "from app.db import models\n" "from app.pkg import Explicit, Listed, Deep, star_fn, Renamed\n" "from app.direct.impl import Direct, direct_fn\n\n\n" "def attr_explicit_cls():\n pkg.Explicit()\n\n\n" "def attr_explicit_fn():\n pkg.explicit_fn()\n\n\n" "def attr_star_cls():\n pkg.Star()\n\n\n" "def attr_star_fn():\n pkg.star_fn()\n\n\n" "def attr_all_listed():\n pkg.Listed()\n\n\n" "def attr_chain():\n pkg.Deep()\n\n\n" "def attr_renamed():\n pkg.Renamed()\n\n\n" "def from_explicit():\n Explicit()\n\n\n" "def from_star_fn():\n star_fn()\n\n\n" "def from_all_listed():\n Listed()\n\n\n" "def from_chain():\n Deep()\n\n\n" "def from_renamed():\n Renamed()\n\n\n" "def cycle_a():\n cyc.a_fn()\n\n\n" "def cycle_b():\n cyc.b_fn()\n\n\n" "def sub_models():\n models.CharField()\n\n\n" "def chain_var():\n x = pkg.Deep()\n x.dive()\n\n\n" "def excl_unlisted():\n pkg.Unlisted()\n\n\n" "def excl_private():\n pkg._private_fn()\n\n\n" "def excl_shadowed():\n shad.Gone()\n\n\n" "def excl_barrier():\n barr.Before()\n\n\n" "def excl_compat():\n cmp.Shim()\n\n\n" "def lazy_tc():\n lz.Lazy()\n\n\n" "def doc_reexport():\n docp.Doc()\n\n\n" "def excl_fstring():\n fsp.Kept()\n\n\n" "def go_reexport():\n gop.Thing()\n\n\n" "def excl_go_submodule():\n gop.base()\n\n\n" "def excl_go_param():\n gox.Kept()\n\n\n" "def ctl_from_cls():\n Direct()\n\n\n" "def ctl_from_fn():\n direct_fn()\n"); for (int i = 0; parallel && i < 52; i++) { char name[64]; char pad[128]; snprintf(name, sizeof(name), "pad/py_pad_%02d.py", i); snprintf(pad, sizeof(pad), "def py_pad_%02d():\n return %d\n", i, i); write_temp_file(tmp, name, pad); } char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", parallel ? "4" : "1", 1); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/py_reexport.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); int run_rc = p ? cbm_pipeline_run(p) : -1; if (incremental && run_rc == 0) { /* Touch only the caller: re-indexed alone, it must still find the * re-exports of the unchanged packages. */ cbm_pipeline_free(p); char main_path[512]; snprintf(main_path, sizeof(main_path), "%s/app/main.py", tmp); th_append_file(main_path, "\n# edited: the second run re-indexes the caller\n"); p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); run_rc = p ? cbm_pipeline_run(p) : -1; } const char *project = p ? cbm_pipeline_project_name(p) : NULL; cbm_store_t *s = project ? cbm_store_open_path(db_path) : NULL; if (!s && run_rc == 0) { run_rc = -1; } if (s) { char src[512]; char tgt[512]; for (int c = 0; c < PY_REEXPORT_N_CALLS; c++) { snprintf(src, sizeof(src), "%s.app.main.%s", project, py_reexport_calls[c][0]); snprintf(tgt, sizeof(tgt), "%s.app.%s", project, py_reexport_calls[c][1]); char *props = py_reexp_edge_props(s, project, src, tgt, "CALLS"); lsp[c] = props && strstr(props, "\"strategy\":\"lsp_") != NULL; if (!lsp[c]) { printf(" [%s] %s -> %s: %s\n", parallel ? "parallel" : "sequential", src, tgt, props ? props : "(no CALLS edge)"); } free(props); } for (int e = 0; e < PY_REEXPORT_N_EXCLUDED; e++) { snprintf(src, sizeof(src), "%s.app.main.%s", project, py_reexport_excluded[e][0]); snprintf(tgt, sizeof(tgt), "%s.app.%s", project, py_reexport_excluded[e][1]); char *props = py_reexp_edge_props(s, project, src, tgt, "CALLS"); excluded[e] = props && strstr(props, "\"strategy\":\"lsp_") != NULL; if (excluded[e]) { printf(" [%s] excluded %s -> %s: %s\n", parallel ? "parallel" : "sequential", src, tgt, props); } free(props); } cbm_store_close(s); } if (p) { cbm_pipeline_free(p); } if (saved_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); free(saved_workers); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); return run_rc; } /* Sequential driver (few files, one worker) and parallel driver (fillers + * four workers), each as a full index and as an incremental re-index that * touched only the caller. */ TEST(pipeline_python_package_reexports_resolve_by_lsp) { static const char *const mode_names[] = {"sequential", "parallel", "sequential incremental", "parallel incremental"}; for (int mode = 0; mode < 4; mode++) { bool lsp[PY_REEXPORT_N_CALLS]; bool excluded[PY_REEXPORT_N_EXCLUDED]; memset(lsp, 0, sizeof(lsp)); memset(excluded, 1, sizeof(excluded)); ASSERT_EQ(py_reexport_run((mode & 1) != 0, mode >= 2, lsp, excluded), 0); for (int c = 0; c < PY_REEXPORT_N_CALLS; c++) { if (!lsp[c]) { fprintf(stderr, " re-export call %s (%s) did not resolve by the LSP\n", py_reexport_calls[c][0], mode_names[mode]); } ASSERT_TRUE(lsp[c]); /* RED for every re-export shape; controls stay green */ } for (int e = 0; e < PY_REEXPORT_N_EXCLUDED; e++) { if (excluded[e]) { fprintf(stderr, " excluded call %s (%s) resolved by the LSP anyway\n", py_reexport_excluded[e][0], mode_names[mode]); } ASSERT_FALSE(excluded[e]); /* __all__, `_`, shadow, barrier */ } } PASS(); } /* Bind the external-import veto specifically to #1277's typed-field fold. * No base classes or CALLS edges participate: a name-only call fallback * cannot turn this metadata assertion into a false positive. The unguarded * registry deliberately has a same-module TestCase candidate in both cases. */ static int python_typed_field_scope_case(bool external) { char source[512]; snprintf(source, sizeof(source), "import unittest\n\n" "class TestCase:\n pass\n\n" "class Holder:\n field: %s\n", external ? "unittest.TestCase" : "TestCase"); CBMFileResult *result = cbm_extract_file(source, (int)strlen(source), CBM_LANG_PYTHON, "proj", "models.py", 0, NULL, NULL); cbm_registry_t *reg = cbm_registry_new(); cbm_gbuf_t *gbuf = cbm_gbuf_new("proj", "/unused"); CBMArena arena; cbm_arena_init(&arena); CBMLSPDef *defs = NULL; char *modules[1] = {NULL}; int ok = 0; if (result && reg && gbuf && result->field_types.count == 1) { for (int i = 0; i < result->defs.count; i++) { const CBMDefinition *def = &result->defs.items[i]; cbm_registry_add(reg, def->name, def->qualified_name, def->label); } const char **keys = NULL; const char **vals = NULL; int import_count = 0; cbm_pxc_build_import_map(gbuf, "proj", "models.py", CBM_LANG_PYTHON, result, &keys, &vals, &import_count); cbm_resolution_t raw = cbm_registry_resolve(reg, external ? "unittest.TestCase" : "TestCase", "proj.models", keys, vals, import_count); bool candidate = raw.qualified_name && raw.strategy && strcmp(raw.qualified_name, "proj.models.TestCase") == 0 && strcmp(raw.strategy, "same_module") == 0; cbm_pxc_free_import_map(keys, vals, import_count); cbm_pipeline_ctx_t ctx = {.project_name = "proj", .gbuf = gbuf, .registry = reg}; cbm_file_info_t files[1] = {{.rel_path = "models.py", .language = CBM_LANG_PYTHON}}; CBMFileResult *cache[1] = {result}; int count = 0; defs = cbm_pxc_collect_all_defs(&ctx, &arena, cache, files, 1, "proj", modules, &count, NULL); int holders = 0; bool field_ok = false; for (int i = 0; defs && i < count; i++) { if (defs[i].qualified_name && strcmp(defs[i].qualified_name, "proj.models.Holder") == 0) { holders++; field_ok = external ? !defs[i].field_defs : defs[i].field_defs && strcmp(defs[i].field_defs, "field:proj.models.TestCase") == 0; } } ok = candidate && holders == 1 && field_ok; if (!ok) { fprintf(stderr, " [py-field-scope external=%d] candidate=%d holders=%d field_ok=%d\n", external, candidate, holders, field_ok); } } free(defs); free(modules[0]); cbm_arena_destroy(&arena); cbm_gbuf_free(gbuf); cbm_registry_free(reg); cbm_free_result(result); return ok; } TEST(python_typed_field_external_import_stays_unresolved) { ASSERT_TRUE(python_typed_field_scope_case(true)); PASS(); } TEST(python_typed_field_project_class_resolves) { ASSERT_TRUE(python_typed_field_scope_case(false)); PASS(); } /* ── Explicit OVERRIDE through non-redeclaring ancestors (#1278) ── */ static int64_t ovr_class(cbm_gbuf_t *gb, const char *name) { char qn[128]; snprintf(qn, sizeof(qn), "m.%s", name); return cbm_gbuf_upsert_node(gb, "Class", name, qn, "m.py", 1, 50, "{}"); } static int64_t ovr_method(cbm_gbuf_t *gb, int64_t cls, const char *cls_name, const char *name) { char qn[128]; snprintf(qn, sizeof(qn), "m.%s.%s", cls_name, name); int64_t id = cbm_gbuf_upsert_node(gb, "Method", name, qn, "m.py", 2, 3, "{}"); cbm_gbuf_insert_edge(gb, cls, id, "DEFINES_METHOD", "{}"); return id; } /* OVERRIDE edges leaving `method`; *only_target receives the single target (or 0). */ static int ovr_out(cbm_gbuf_t *gb, int64_t method, int64_t *only_target) { const cbm_gbuf_edge_t **edges = NULL; int count = 0; cbm_gbuf_find_edges_by_source_type(gb, method, "OVERRIDE", &edges, &count); *only_target = count == 1 ? edges[0]->target_id : 0; return count; } /* Exactly two different targets, each once, independent of edge order. */ static bool ovr_two_targets(cbm_gbuf_t *gb, int64_t method, int64_t first, int64_t second) { const cbm_gbuf_edge_t **edges = NULL; int count = 0; cbm_gbuf_find_edges_by_source_type(gb, method, "OVERRIDE", &edges, &count); int first_count = 0; int second_count = 0; for (int i = 0; i < count; i++) { first_count += edges[i]->target_id == first; second_count += edges[i]->target_id == second; } return first != second && count == 2 && first_count == 1 && second_count == 1; } TEST(override_explicit_walks_to_nearest_declaring_ancestor) { cbm_gbuf_t *gb = cbm_gbuf_new("test-proj", "/tmp/test"); ASSERT_NOT_NULL(gb); /* Contract: Processor.process */ int64_t processor = ovr_class(gb, "Processor"); int64_t proc_process = ovr_method(gb, processor, "Processor", "process"); /* Direct control: DirectProcessor(Processor) */ int64_t direct = ovr_class(gb, "DirectProcessor"); int64_t direct_process = ovr_method(gb, direct, "DirectProcessor", "process"); cbm_gbuf_insert_edge(gb, direct, processor, "INHERITS", "{}"); /* The #1278 shape: LeafProcessor(IntermediateProcessor(Processor)), the * intermediate declares nothing. `extra` exists on no ancestor. */ int64_t mid = ovr_class(gb, "IntermediateProcessor"); int64_t leaf = ovr_class(gb, "LeafProcessor"); int64_t leaf_process = ovr_method(gb, leaf, "LeafProcessor", "process"); int64_t leaf_extra = ovr_method(gb, leaf, "LeafProcessor", "extra"); cbm_gbuf_insert_edge(gb, mid, processor, "INHERITS", "{}"); cbm_gbuf_insert_edge(gb, leaf, mid, "INHERITS", "{}"); /* Nearest wins: Deep(Redeclaring(Processor)), Redeclaring declares process. */ int64_t redecl = ovr_class(gb, "Redeclaring"); int64_t redecl_process = ovr_method(gb, redecl, "Redeclaring", "process"); int64_t deep = ovr_class(gb, "Deep"); int64_t deep_process = ovr_method(gb, deep, "Deep", "process"); cbm_gbuf_insert_edge(gb, redecl, processor, "INHERITS", "{}"); cbm_gbuf_insert_edge(gb, deep, redecl, "INHERITS", "{}"); /* Diamond: Both(Left, Right), Left and Right each inherit Processor * without redeclaring -> exactly one edge, no duplicate. */ int64_t left = ovr_class(gb, "Left"); int64_t right = ovr_class(gb, "Right"); int64_t both = ovr_class(gb, "Both"); int64_t both_process = ovr_method(gb, both, "Both", "process"); cbm_gbuf_insert_edge(gb, left, processor, "INHERITS", "{}"); cbm_gbuf_insert_edge(gb, right, processor, "INHERITS", "{}"); cbm_gbuf_insert_edge(gb, both, left, "INHERITS", "{}"); cbm_gbuf_insert_edge(gb, both, right, "INHERITS", "{}"); /* Equal nearest depth: two declaring ancestors behind empty direct * parents. Both declarations bind, while the deeper Processor does not. * Both explicit graph relationships share the nearest declaring depth. */ int64_t tie_left = ovr_class(gb, "TieLeft"); int64_t tie_right = ovr_class(gb, "TieRight"); int64_t tie_left_process = ovr_method(gb, tie_left, "TieLeft", "process"); int64_t tie_right_process = ovr_method(gb, tie_right, "TieRight", "process"); int64_t tie_left_mid = ovr_class(gb, "TieLeftMid"); int64_t tie_right_mid = ovr_class(gb, "TieRightMid"); int64_t tied = ovr_class(gb, "Tied"); int64_t tied_process = ovr_method(gb, tied, "Tied", "process"); cbm_gbuf_insert_edge(gb, tie_left, processor, "INHERITS", "{}"); cbm_gbuf_insert_edge(gb, tie_right, processor, "INHERITS", "{}"); cbm_gbuf_insert_edge(gb, tie_left_mid, tie_left, "INHERITS", "{}"); cbm_gbuf_insert_edge(gb, tie_right_mid, tie_right, "IMPLEMENTS", "{}"); cbm_gbuf_insert_edge(gb, tied, tie_right_mid, "IMPLEMENTS", "{}"); cbm_gbuf_insert_edge(gb, tied, tie_left_mid, "INHERITS", "{}"); /* The separate Go implicit-satisfaction pass owns .go classes. */ int64_t go_type = cbm_gbuf_upsert_node(gb, "Struct", "GoType", "m.GoType", "m.go", 1, 5, "{}"); int64_t go_process = ovr_method(gb, go_type, "GoType", "process"); cbm_gbuf_insert_edge(gb, go_type, processor, "IMPLEMENTS", "{}"); /* Malformed hierarchy: CycA <-> CycB plus a self-loop, CycLeaf(CycA). * The walk must terminate and bind nothing. */ int64_t cyc_a = ovr_class(gb, "CycA"); int64_t cyc_b = ovr_class(gb, "CycB"); int64_t cyc_leaf = ovr_class(gb, "CycLeaf"); int64_t cyc_process = ovr_method(gb, cyc_leaf, "CycLeaf", "process"); cbm_gbuf_insert_edge(gb, cyc_a, cyc_b, "INHERITS", "{}"); cbm_gbuf_insert_edge(gb, cyc_b, cyc_a, "INHERITS", "{}"); cbm_gbuf_insert_edge(gb, cyc_a, cyc_a, "INHERITS", "{}"); cbm_gbuf_insert_edge(gb, cyc_leaf, cyc_a, "INHERITS", "{}"); atomic_int cancelled = 0; cbm_pipeline_ctx_t ctx = { .project_name = "test-proj", .repo_path = "/tmp/test", .gbuf = gb, .cancelled = &cancelled, }; (void)cbm_pipeline_override_explicit(&ctx); int64_t target = 0; bool direct_ok = ovr_out(gb, direct_process, &target) == 1 && target == proc_process; bool indirect_ok = ovr_out(gb, leaf_process, &target) == 1 && target == proc_process; bool extra_absent = ovr_out(gb, leaf_extra, &target) == 0; bool nearest_ok = ovr_out(gb, deep_process, &target) == 1 && target == redecl_process; bool redecl_ok = ovr_out(gb, redecl_process, &target) == 1 && target == proc_process; bool diamond_ok = ovr_out(gb, both_process, &target) == 1 && target == proc_process; bool ties_ok = ovr_two_targets(gb, tied_process, tie_left_process, tie_right_process); bool go_absent = ovr_out(gb, go_process, &target) == 0; bool cycle_absent = ovr_out(gb, cyc_process, &target) == 0; int first_count = cbm_gbuf_edge_count_by_type(gb, "OVERRIDE"); /* Idempotent: a second run adds no edges. */ (void)cbm_pipeline_override_explicit(&ctx); int second_count = cbm_gbuf_edge_count_by_type(gb, "OVERRIDE"); bool second_ties_ok = ovr_two_targets(gb, tied_process, tie_left_process, tie_right_process); cbm_gbuf_free(gb); ASSERT_TRUE(direct_ok); ASSERT_TRUE(indirect_ok); ASSERT_TRUE(extra_absent); ASSERT_TRUE(nearest_ok); ASSERT_TRUE(redecl_ok); ASSERT_TRUE(diamond_ok); ASSERT_TRUE(ties_ok); ASSERT_TRUE(go_absent); ASSERT_TRUE(cycle_absent); ASSERT_EQ(first_count, 9); ASSERT_EQ(second_count, 9); ASSERT_TRUE(second_ties_ok); PASS(); } /* OVERRIDE edges from a method whose QN ends with src_suffix to one whose QN * ends with tgt_suffix, in the stored graph. */ static int count_override_edges(cbm_store_t *s, const char *project, const char *src_suffix, const char *tgt_suffix) { cbm_edge_t *edges = NULL; int edge_count = 0; cbm_store_find_edges_by_type(s, project, "OVERRIDE", &edges, &edge_count); int hits = 0; for (int i = 0; i < edge_count; i++) { cbm_node_t src = {0}; cbm_node_t tgt = {0}; if (cbm_store_find_node_by_id(s, edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK) { size_t sl = strlen(src.qualified_name); size_t tl = strlen(tgt.qualified_name); size_t ss = strlen(src_suffix); size_t ts = strlen(tgt_suffix); if (sl >= ss && tl >= ts && strcmp(src.qualified_name + sl - ss, src_suffix) == 0 && strcmp(tgt.qualified_name + tl - ts, tgt_suffix) == 0) { hits++; } } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (edges) cbm_store_free_edges(edges, edge_count); return hits; } TEST(override_python_through_intermediate_class) { /* The #1278 reproduction, end to end through the full pipeline. */ const char *py = "from abc import ABC, abstractmethod\n\n" "class Processor(ABC):\n" " @abstractmethod\n" " def process(self) -> str:\n" " raise NotImplementedError\n\n" "class IntermediateProcessor(Processor):\n" " pass\n\n" "class LeafProcessor(IntermediateProcessor):\n" " def process(self) -> str:\n" " return \"leaf\"\n\n" "class DirectProcessor(Processor):\n" " def process(self) -> str:\n" " return \"direct\"\n\n" "def run(processor: Processor) -> str:\n" " return processor.process()\n"; if (setup_usages_repo("procs.py", py, NULL, NULL) != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_override.db", g_usages_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); int direct = count_override_edges(s, project, "DirectProcessor.process", ".Processor.process"); int leaf = count_override_edges(s, project, "LeafProcessor.process", ".Processor.process"); int all = count_override_edges(s, project, "", ""); cbm_store_close(s); cbm_pipeline_free(p); teardown_usages_repo(); ASSERT_EQ(direct, 1); /* control */ ASSERT_EQ(leaf, 1); /* #1278 */ ASSERT_EQ(all, 2); /* nothing else, no duplicates */ PASS(); } TEST(usages_creates_edges) { /* Port of TestPassUsagesCreatesEdges. * Go source with callback reference → USAGE edge. */ const char *go_source = "package mypkg\n\n" "func Process(data string) string {\n" "\treturn data\n" "}\n\n" "func Register() {\n" "\thandler := Process\n" "\t_ = handler\n" "}\n"; if (setup_usages_repo("mypkg/main.go", go_source, "go.mod", "module testmod\ngo 1.21\n") != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_usages.db", g_usages_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Check for USAGE edges */ cbm_edge_t *edges = NULL; int edge_count = 0; rc = cbm_store_find_edges_by_type(s, project, "USAGE", &edges, &edge_count); bool found_usage = false; for (int i = 0; i < edge_count; i++) { cbm_node_t src = {0}, tgt = {0}; if (cbm_store_find_node_by_id(s, edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK) { if (strcmp(src.name, "Register") == 0 && strcmp(tgt.name, "Process") == 0) { found_usage = true; } } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (edges) cbm_store_free_edges(edges, edge_count); ASSERT_TRUE(found_usage); cbm_store_close(s); cbm_pipeline_free(p); teardown_usages_repo(); PASS(); } TEST(usages_no_duplicate_calls) { /* Port of TestPassUsagesDoesNotDuplicateCalls. * A direct function call should produce CALLS but not USAGE. */ const char *go_source = "package mypkg\n\n" "func Helper() string {\n" "\treturn \"ok\"\n" "}\n\n" "func Main() {\n" "\tHelper()\n" "}\n"; if (setup_usages_repo("mypkg/main.go", go_source, "go.mod", "module testmod\ngo 1.21\n") != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_no_dup.db", g_usages_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Should have CALLS edge from Main to Helper */ cbm_edge_t *call_edges = NULL; int call_count = 0; cbm_store_find_edges_by_type(s, project, "CALLS", &call_edges, &call_count); bool found_call = false; for (int i = 0; i < call_count; i++) { cbm_node_t src = {0}, tgt = {0}; if (cbm_store_find_node_by_id(s, call_edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, call_edges[i].target_id, &tgt) == CBM_STORE_OK) { if (strcmp(src.name, "Main") == 0 && strcmp(tgt.name, "Helper") == 0) { found_call = true; } } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (call_edges) cbm_store_free_edges(call_edges, call_count); ASSERT_TRUE(found_call); /* Should NOT have USAGE edge from Main to Helper */ cbm_edge_t *usage_edges = NULL; int usage_count = 0; cbm_store_find_edges_by_type(s, project, "USAGE", &usage_edges, &usage_count); for (int i = 0; i < usage_count; i++) { cbm_node_t src = {0}, tgt = {0}; if (cbm_store_find_node_by_id(s, usage_edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, usage_edges[i].target_id, &tgt) == CBM_STORE_OK) { ASSERT_FALSE(strcmp(src.name, "Main") == 0 && strcmp(tgt.name, "Helper") == 0); } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (usage_edges) cbm_store_free_edges(usage_edges, usage_count); cbm_store_close(s); cbm_pipeline_free(p); teardown_usages_repo(); PASS(); } TEST(calls_edge_carries_call_site_line) { /* Regression guard for #503: a CALLS edge must persist the source line of * the call site in its JSON props as "line":N. Helper() is invoked on * line 8 (1-based) of the source below. */ const char *go_source = "package mypkg\n" "\n" "func Helper() string {\n" "\treturn \"ok\"\n" "}\n" "\n" "func Main() {\n" "\tHelper()\n" "}\n"; if (setup_usages_repo("mypkg/main.go", go_source, "go.mod", "module testmod\ngo 1.21\n") != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_call_line.db", g_usages_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); cbm_edge_t *call_edges = NULL; int call_count = 0; cbm_store_find_edges_by_type(s, project, "CALLS", &call_edges, &call_count); bool found_line = false; for (int i = 0; i < call_count; i++) { cbm_node_t src = {0}, tgt = {0}; if (cbm_store_find_node_by_id(s, call_edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, call_edges[i].target_id, &tgt) == CBM_STORE_OK) { if (strcmp(src.name, "Main") == 0 && strcmp(tgt.name, "Helper") == 0) { ASSERT_NOT_NULL(call_edges[i].properties_json); ASSERT_TRUE(strstr(call_edges[i].properties_json, "\"line\":8}") != NULL); found_line = true; } } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (call_edges) cbm_store_free_edges(call_edges, call_count); ASSERT_TRUE(found_line); cbm_store_close(s); cbm_pipeline_free(p); teardown_usages_repo(); PASS(); } TEST(usages_kotlin_creates_edges) { /* Port of TestPassUsagesKotlinCreatesEdges. * Explicit Kotlin callable syntax produces CALL_REFERENCE, not USAGE. */ const char *kt_source = "fun process(data: String): String {\n" " return data\n" "}\n\n" "fun register() {\n" " val handler = ::process\n" "}\n"; if (setup_usages_repo("Main.kt", kt_source, NULL, NULL) != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_kt_usage.db", g_usages_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* The explicit reference is resolved and classified exclusively. */ cbm_edge_t *edges = NULL; int edge_count = 0; rc = cbm_store_find_edges_by_type(s, project, "CALL_REFERENCE", &edges, &edge_count); ASSERT_EQ(rc, CBM_STORE_OK); bool found_reference = false; for (int i = 0; i < edge_count; i++) { cbm_node_t src = {0}, tgt = {0}; if (cbm_store_find_node_by_id(s, edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK && src.name && tgt.name && strcmp(src.name, "register") == 0 && strcmp(tgt.name, "process") == 0) { found_reference = true; } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (edges) cbm_store_free_edges(edges, edge_count); ASSERT_TRUE(found_reference); edges = NULL; edge_count = 0; rc = cbm_store_find_edges_by_type(s, project, "USAGE", &edges, &edge_count); ASSERT_EQ(rc, CBM_STORE_OK); bool duplicated_as_usage = false; for (int i = 0; i < edge_count; i++) { cbm_node_t src = {0}, tgt = {0}; if (cbm_store_find_node_by_id(s, edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK && src.name && tgt.name && strcmp(src.name, "register") == 0 && strcmp(tgt.name, "process") == 0) { duplicated_as_usage = true; } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (edges) cbm_store_free_edges(edges, edge_count); ASSERT_FALSE(duplicated_as_usage); cbm_store_close(s); cbm_pipeline_free(p); teardown_usages_repo(); PASS(); } TEST(usages_kotlin_no_duplicate_calls) { /* Port of TestPassUsagesKotlinDoesNotDuplicateCalls. * Direct call in Kotlin → CALLS but not USAGE. */ const char *kt_source = "fun helper(): String {\n" " return \"ok\"\n" "}\n\n" "fun main() {\n" " helper()\n" "}\n"; if (setup_usages_repo("Main.kt", kt_source, NULL, NULL) != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_kt_nodup.db", g_usages_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_usages_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Should have CALLS edge from main to helper */ cbm_edge_t *call_edges = NULL; int call_count = 0; cbm_store_find_edges_by_type(s, project, "CALLS", &call_edges, &call_count); bool found_call = false; for (int i = 0; i < call_count; i++) { cbm_node_t src = {0}, tgt = {0}; if (cbm_store_find_node_by_id(s, call_edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, call_edges[i].target_id, &tgt) == CBM_STORE_OK) { if (strcmp(src.name, "main") == 0 && strcmp(tgt.name, "helper") == 0) { found_call = true; } } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (call_edges) cbm_store_free_edges(call_edges, call_count); ASSERT_TRUE(found_call); /* Should NOT have USAGE edge from main to helper */ cbm_edge_t *usage_edges = NULL; int usage_count = 0; cbm_store_find_edges_by_type(s, project, "USAGE", &usage_edges, &usage_count); for (int i = 0; i < usage_count; i++) { cbm_node_t src = {0}, tgt = {0}; if (cbm_store_find_node_by_id(s, usage_edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, usage_edges[i].target_id, &tgt) == CBM_STORE_OK) { ASSERT_FALSE(strcmp(src.name, "main") == 0 && strcmp(tgt.name, "helper") == 0); } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (usage_edges) cbm_store_free_edges(usage_edges, usage_count); cbm_store_close(s); cbm_pipeline_free(p); teardown_usages_repo(); PASS(); } /* ── Pipeline language integration tests ────────────────────────── */ /* General helper: create temp dir and write multiple files */ 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)) return -1; for (int i = 0; i < count; i++) { char path[512]; snprintf(path, sizeof(path), "%s/%s", g_lang_tmpdir, filenames[i]); /* Create parent directories */ char dir[512]; snprintf(dir, sizeof(dir), "%s", path); char *slash = strrchr(dir, '/'); if (slash) { *slash = '\0'; th_mkdir_p(dir); } FILE *f = fopen(path, "wb"); if (!f) return -1; fprintf(f, "%s", contents[i]); fclose(f); } return 0; } static void teardown_lang_repo(void) { if (g_lang_tmpdir[0]) rm_rf(g_lang_tmpdir); g_lang_tmpdir[0] = '\0'; } /* ── #1548: Python defs nested in a function body ─────────────────── */ /* The QN of `rel` in `project` ("web.create_app.health"). Route QNs * ("__route__GET__/health") carry no project and are taken as they are. */ static void pyn_qn(char *buf, size_t cap, const char *project, const char *rel) { if (strncmp(rel, "__route__", strlen("__route__")) == 0) { snprintf(buf, cap, "%s", rel); } else { snprintf(buf, cap, "%s.%s", project, rel); } } /* True iff an edge of `type` runs from node `src_rel` to node `tgt_rel`. */ static bool pyn_edge(cbm_store_t *s, const char *project, const char *src_rel, const char *tgt_rel, const char *type) { char sq[512]; char tq[512]; pyn_qn(sq, sizeof(sq), project, src_rel); pyn_qn(tq, sizeof(tq), project, tgt_rel); cbm_node_t src = {0}; cbm_node_t tgt = {0}; bool found = false; if (cbm_store_find_node_by_qn(s, project, sq, &src) == CBM_STORE_OK && cbm_store_find_node_by_qn(s, project, tq, &tgt) == CBM_STORE_OK) { cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(s, src.id, type, &edges, &ec); for (int i = 0; i < ec && !found; i++) { found = edges[i].target_id == tgt.id; } if (edges) { cbm_store_free_edges(edges, ec); } } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); return found; } /* The fixtures of the #1548 pipeline tests. */ static const char pyn_web_py[] = "from flask import Flask\n" "\n" "def load_user(uid):\n" " return uid\n" "\n" "def render(name):\n" " return name\n" "\n" "def create_app():\n" " app = Flask(__name__)\n" "\n" " @app.route(\"/users/\")\n" " def show_user(uid):\n" " return render(load_user(uid))\n" "\n" " @app.get(\"/health\")\n" " def health():\n" " return render(\"ok\")\n" "\n" " def ping():\n" " return render(\"pong\")\n" "\n" " app.add_url_rule(\"/ping\", \"ping\", ping)\n" " return app\n"; /* A nested def named like its method, and a local helper whose name exists * nowhere else -- b.py calls that name without binding it. */ static const char pyn_a_py[] = "class Tree:\n" " def visit(self, node):\n" " def visit(n):\n" " if n:\n" " visit(n.child)\n" "\n" " visit(node)\n" "\n" "\n" "def outer():\n" " def helper_only_here():\n" " return 1\n" "\n" " return helper_only_here()\n"; static const char pyn_b_py[] = "def elsewhere():\n" " return helper_only_here()\n"; /* Two defs, each with its own local class of the same name. */ static const char pyn_c_py[] = "def make_forms():\n" " class LocalForm:\n" " def clean(self):\n" " return 1\n" "\n" " form = LocalForm()\n" " return form.clean()\n" "\n" "\n" "def make_other_forms():\n" " class LocalForm:\n" " def clean(self):\n" " return 2\n" "\n" " form = LocalForm()\n" " return form.clean()\n"; /* A local def named like a stdlib function another module imports. */ static const char pyn_x_py[] = "def outer():\n" " def getpass(prompt):\n" " return prompt\n" "\n" " return getpass(\"?\")\n"; static const char pyn_y_py[] = "from getpass import getpass\n" "\n" "\n" "def ask():\n" " return getpass(\"password: \")\n"; /* The issue's headline: an app factory's route handlers are defs inside * create_app. They were never extracted, so the HTTP layer vanished -- no * HANDLES, and load_user()'s only caller was pinned on the file node. */ TEST(pipeline_python_app_factory_routes_issue1548) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_factory_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "web.py", pyn_web_py); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/factory.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_TRUE( pyn_edge(s, project, "web.create_app.show_user", "__route__ANY__/users/{}", "HANDLES")); ASSERT_TRUE(pyn_edge(s, project, "web.create_app.health", "__route__GET__/health", "HANDLES")); ASSERT_TRUE(pyn_edge(s, project, "web.create_app.show_user", "web.load_user", "CALLS")); ASSERT_TRUE(pyn_edge(s, project, "web.create_app.health", "web.render", "CALLS")); ASSERT_FALSE(pyn_edge(s, project, "web.py.__file__", "web.load_user", "CALLS")); /* A call-registered handler: add_url_rule names create_app's local ping. */ ASSERT_TRUE(pyn_edge(s, project, "web.create_app.ping", "__route__ANY__/ping", "HANDLES")); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* A def nested in a function is a local of it (Python LEGB). A bare call * resolves to the local bound in the nearest enclosing function: visit(node) * in Tree.visit reaches the nested visit, whose own recursion stays a * self-call instead of landing on the method. And no caller outside the * function can reach the local by name -- b.py's unbound helper_only_here() * must not be matched to it by a short-name strategy. */ TEST(pipeline_python_nested_def_is_function_local_issue1548) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_local_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "a.py", pyn_a_py); write_temp_file(tmp, "b.py", pyn_b_py); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/local.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_TRUE(pyn_edge(s, project, "a.Tree.visit", "a.Tree.visit.visit", "CALLS")); ASSERT_FALSE(pyn_edge(s, project, "a.Tree.visit.visit", "a.Tree.visit", "CALLS")); ASSERT_TRUE(pyn_edge(s, project, "a.outer", "a.outer.helper_only_here", "CALLS")); ASSERT_FALSE(pyn_edge(s, project, "b.elsewhere", "a.outer.helper_only_here", "CALLS")); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* Two defs each declare `class LocalForm`. Each constructor call reaches its * own def's class, and a method call on the instance never reaches the other * def's class: the Python LSP must not bind a local class module-wide by its * short name (every same-named local class collapsed onto one). */ TEST(pipeline_python_local_classes_stay_in_their_def_issue1548) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_localcls_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "c.py", pyn_c_py); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/localcls.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_TRUE(pyn_edge(s, project, "c.make_forms", "c.make_forms.LocalForm", "CALLS")); ASSERT_TRUE( pyn_edge(s, project, "c.make_other_forms", "c.make_other_forms.LocalForm", "CALLS")); ASSERT_FALSE(pyn_edge(s, project, "c.make_forms", "c.make_other_forms.LocalForm", "CALLS")); ASSERT_FALSE(pyn_edge(s, project, "c.make_other_forms", "c.make_forms.LocalForm", "CALLS")); ASSERT_FALSE( pyn_edge(s, project, "c.make_forms", "c.make_other_forms.LocalForm.clean", "CALLS")); ASSERT_FALSE( pyn_edge(s, project, "c.make_other_forms", "c.make_forms.LocalForm.clean", "CALLS")); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* No import can name a function's local. The symbol-name import fallback * bound `from getpass import getpass` (the stdlib module, not in the project) * to x.outer's local `def getpass`, and ask()'s call followed the import. */ TEST(pipeline_python_import_never_binds_a_local_def_issue1548) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_localimp_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "x.py", pyn_x_py); write_temp_file(tmp, "y.py", pyn_y_py); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/localimp.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_TRUE(pyn_edge(s, project, "x.outer", "x.outer.getpass", "CALLS")); ASSERT_FALSE(pyn_edge(s, project, "y.py.__file__", "x.outer.getpass", "IMPORTS")); ASSERT_FALSE(pyn_edge(s, project, "y.ask", "x.outer.getpass", "CALLS")); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* The same four fixtures through the parallel path: >= 50 files and * CBM_WORKERS=4 force pass_parallel.c and the cross-file Python LSP, which * register, resolve and bind through their own code. */ TEST(pipeline_python_nested_defs_parallel_issue1548) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_nested_par_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "web.py", pyn_web_py); write_temp_file(tmp, "a.py", pyn_a_py); write_temp_file(tmp, "b.py", pyn_b_py); write_temp_file(tmp, "c.py", pyn_c_py); write_temp_file(tmp, "x.py", pyn_x_py); write_temp_file(tmp, "y.py", pyn_y_py); for (int i = 0; i < 52; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "filler%d.py", i); snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", "4", 1); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/nested_par.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); const char *project = cbm_pipeline_project_name(p); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_TRUE( pyn_edge(s, project, "web.create_app.show_user", "__route__ANY__/users/{}", "HANDLES")); ASSERT_TRUE(pyn_edge(s, project, "web.create_app.health", "__route__GET__/health", "HANDLES")); ASSERT_TRUE(pyn_edge(s, project, "web.create_app.show_user", "web.load_user", "CALLS")); ASSERT_TRUE(pyn_edge(s, project, "web.create_app.ping", "__route__ANY__/ping", "HANDLES")); ASSERT_TRUE(pyn_edge(s, project, "a.Tree.visit", "a.Tree.visit.visit", "CALLS")); ASSERT_FALSE(pyn_edge(s, project, "a.Tree.visit.visit", "a.Tree.visit", "CALLS")); ASSERT_TRUE(pyn_edge(s, project, "a.outer", "a.outer.helper_only_here", "CALLS")); ASSERT_FALSE(pyn_edge(s, project, "b.elsewhere", "a.outer.helper_only_here", "CALLS")); ASSERT_TRUE(pyn_edge(s, project, "c.make_forms", "c.make_forms.LocalForm", "CALLS")); ASSERT_FALSE(pyn_edge(s, project, "c.make_forms", "c.make_other_forms.LocalForm", "CALLS")); ASSERT_FALSE( pyn_edge(s, project, "c.make_forms", "c.make_other_forms.LocalForm.clean", "CALLS")); ASSERT_FALSE( pyn_edge(s, project, "c.make_other_forms", "c.make_forms.LocalForm.clean", "CALLS")); ASSERT_TRUE(pyn_edge(s, project, "x.outer", "x.outer.getpass", "CALLS")); ASSERT_FALSE(pyn_edge(s, project, "y.py.__file__", "x.outer.getpass", "IMPORTS")); ASSERT_FALSE(pyn_edge(s, project, "y.ask", "x.outer.getpass", "CALLS")); cbm_store_close(s); cbm_pipeline_free(p); if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); PASS(); } /* An incremental re-index seeds its registry from the stored graph, not from * extraction. It must keep a function's locals out of the short-name index * exactly as the full index does: re-resolving the touched b.py must not bind * its unbound helper_only_here() to a.outer's local. */ TEST(pipeline_python_nested_def_local_after_incremental_issue1548) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_local_incr_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "a.py", pyn_a_py); write_temp_file(tmp, "b.py", pyn_b_py); /* An extensionless script is Python by its shebang: its locals are locals * in the incremental seed too, not only in the full index. */ write_temp_file(tmp, "tool", "#!/usr/bin/env python3\n" "def run():\n" " def helper_in_tool():\n" " return 1\n" "\n" " return helper_in_tool()\n"); write_temp_file(tmp, "d.py", "def elsewhere_too():\n" " return helper_in_tool()\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/local_incr.db", tmp); cbm_pipeline_t *p1 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p1); ASSERT_EQ(cbm_pipeline_run(p1), 0); cbm_pipeline_free(p1); char b_path[512]; snprintf(b_path, sizeof(b_path), "%s/b.py", tmp); ASSERT_EQ(th_append_file(b_path, "\n# touched: re-resolve b.py incrementally\n"), 0); snprintf(b_path, sizeof(b_path), "%s/d.py", tmp); ASSERT_EQ(th_append_file(b_path, "\n# touched: re-resolve d.py incrementally\n"), 0); cbm_pipeline_t *p2 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p2); ASSERT_EQ(cbm_pipeline_run(p2), 0); const char *project = cbm_pipeline_project_name(p2); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); ASSERT_TRUE(pyn_edge(s, project, "a.outer", "a.outer.helper_only_here", "CALLS")); ASSERT_FALSE(pyn_edge(s, project, "b.elsewhere", "a.outer.helper_only_here", "CALLS")); ASSERT_TRUE(pyn_edge(s, project, "tool.run", "tool.run.helper_in_tool", "CALLS")); ASSERT_FALSE(pyn_edge(s, project, "d.elsewhere_too", "tool.run.helper_in_tool", "CALLS")); cbm_store_close(s); cbm_pipeline_free(p2); th_rmtree(tmp); PASS(); } /* As pyn_edge, and the edge's properties also contain `fragment`. */ static bool pyn_edge_with(cbm_store_t *s, const char *project, const char *src_rel, const char *tgt_rel, const char *type, const char *fragment) { char sq[512]; char tq[512]; pyn_qn(sq, sizeof(sq), project, src_rel); pyn_qn(tq, sizeof(tq), project, tgt_rel); cbm_node_t src = {0}; cbm_node_t tgt = {0}; bool found = false; if (cbm_store_find_node_by_qn(s, project, sq, &src) == CBM_STORE_OK && cbm_store_find_node_by_qn(s, project, tq, &tgt) == CBM_STORE_OK) { cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(s, src.id, type, &edges, &ec); for (int i = 0; i < ec && !found; i++) { found = edges[i].target_id == tgt.id && edges[i].properties_json && strstr(edges[i].properties_json, fragment); } if (edges) { cbm_store_free_edges(edges, ec); } } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); return found; } /* A local def shadows a module def of the same name (Python LEGB), in both * orders of the two callers within one file: the registry's per-file resolve * cache is keyed by the callee name only, so a caller-dependent answer must * never come from it. The undecorated local is bound by the Python LSP itself * (an lsp_* edge); a decorated one is no exact callable value, so the LSP * shadows it and the registry's lexical_scope step binds it. */ static const char pyn_shadow_local_first_py[] = "def helper():\n" " return 0\n" "\n" "def uses_local():\n" " def helper():\n" " return 1\n" " return helper()\n" "\n" "def uses_module():\n" " return helper()\n" "\n" "def uses_decorated():\n" " @staticmethod\n" " def helper():\n" " return 2\n" " return helper()\n"; static const char pyn_shadow_module_first_py[] = "def helper():\n" " return 0\n" "\n" "def uses_module():\n" " return helper()\n" "\n" "def uses_local():\n" " if True:\n" " def helper():\n" " return 1\n" " return helper()\n"; /* A class declared in a def is walked by the LSP under the def's QN: super() * and self calls in its methods resolve through the class's own bases. */ static const char pyn_local_class_methods_py[] = "class Base:\n" " def greet(self):\n" " return 1\n" "\n" "def factory():\n" " class Local(Base):\n" " def greet(self):\n" " return super().greet()\n" "\n" " def other(self):\n" " return self.greet()\n" " return Local\n"; static int pyn_assert_shadowing(cbm_store_t *s, const char *project) { ASSERT_TRUE(pyn_edge_with(s, project, "sc.factory.Local.greet", "sc.Base.greet", "CALLS", "\"strategy\":\"lsp_super\"")); ASSERT_TRUE(pyn_edge_with(s, project, "sc.factory.Local.other", "sc.factory.Local.greet", "CALLS", "\"strategy\":\"lsp_")); /* sa's local def is a plain statement of the body: the LSP binds it as an * exact callable. sb's sits under `if True:`; at that compound statement's * join the LSP invalidates what the branch bound (the def may not exist on * every path), so it emits nothing and the registry's lexical step binds * the local -- still never the module def. H1 (the LSP walks compound * statements) may legitimately move sb to an lsp_* strategy; that PR * must update this expectation deliberately. */ const char *files[] = {"sa", "sb"}; const char *local_strategy[] = {"\"strategy\":\"lsp_", "\"strategy\":\"lexical_scope\""}; for (int f = 0; f < 2; f++) { char local_caller[64], local_def[64], module_caller[64], module_def[64]; snprintf(local_caller, sizeof(local_caller), "%s.uses_local", files[f]); snprintf(local_def, sizeof(local_def), "%s.uses_local.helper", files[f]); snprintf(module_caller, sizeof(module_caller), "%s.uses_module", files[f]); snprintf(module_def, sizeof(module_def), "%s.helper", files[f]); ASSERT_TRUE( pyn_edge_with(s, project, local_caller, local_def, "CALLS", local_strategy[f])); ASSERT_FALSE(pyn_edge(s, project, local_caller, module_def, "CALLS")); ASSERT_TRUE(pyn_edge(s, project, module_caller, module_def, "CALLS")); ASSERT_FALSE(pyn_edge(s, project, module_caller, local_def, "CALLS")); } ASSERT_TRUE(pyn_edge_with(s, project, "sa.uses_decorated", "sa.uses_decorated.helper", "CALLS", "\"strategy\":\"lexical_scope\"")); ASSERT_FALSE(pyn_edge(s, project, "sa.uses_decorated", "sa.helper", "CALLS")); return 0; } static int pyn_run_shadowing(int fillers) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_shadow_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "sa.py", pyn_shadow_local_first_py); write_temp_file(tmp, "sb.py", pyn_shadow_module_first_py); write_temp_file(tmp, "sc.py", pyn_local_class_methods_py); for (int i = 0; i < fillers; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "filler%d.py", i); snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; if (fillers > 0) { cbm_setenv("CBM_WORKERS", "4", 1); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/shadow.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); int rc = p ? cbm_pipeline_run(p) : -1; cbm_store_t *s = rc == 0 ? cbm_store_open_path(db_path) : NULL; int verdict = s ? pyn_assert_shadowing(s, cbm_pipeline_project_name(p)) : 1; if (s) { cbm_store_close(s); } if (p) { cbm_pipeline_free(p); } if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); ASSERT_EQ(rc, 0); return verdict; } TEST(pipeline_python_local_def_shadows_module_def_issue1548) { return pyn_run_shadowing(0); } TEST(pipeline_python_local_def_shadows_module_def_parallel_issue1548) { return pyn_run_shadowing(52); } /* ── super() follows the C3 method resolution order ──────────────── */ /* The home-assistant radarr shape: RadarrCalendarEntity(RadarrEntity, * CalendarEntity), where RadarrEntity reaches the common base Entity through * CoordinatorEntity and CalendarEntity overrides Entity's method. Python's C3 * order is [RadarrCalendarEntity, RadarrEntity, CoordinatorEntity, * CalendarEntity, Entity]: a common base comes after every class that * derives from it, so super() reaches CalendarEntity, not Entity through the * first base's ancestry. */ static const char pym_base_py[] = "class Entity:\n" " def __init__(self):\n" " self.ready = False\n" "\n" " def write_state(self):\n" " return 0\n" "\n" "class CoordinatorEntity(Entity):\n" " pass\n" "\n" "class CalendarEntity(Entity):\n" " def __init__(self):\n" " self.events = []\n" "\n" " def write_state(self):\n" " return 1\n"; static const char pym_radarr_py[] = "from base import CoordinatorEntity, CalendarEntity\n" "\n" "class RadarrEntity(CoordinatorEntity):\n" " pass\n" "\n" "class RadarrCalendarEntity(RadarrEntity, CalendarEntity):\n" " def __init__(self):\n" " super().__init__()\n" "\n" " def write_state(self):\n" " return super().write_state()\n" "\n" "class PlainCalendar(CalendarEntity):\n" " def write_state(self):\n" " return super().write_state()\n"; static int pym_run_super_c3(int fillers) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_superc3_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "base.py", pym_base_py); write_temp_file(tmp, "radarr.py", pym_radarr_py); for (int i = 0; i < fillers; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "filler%d.py", i); snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; if (fillers > 0) { cbm_setenv("CBM_WORKERS", "4", 1); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/superc3.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); int rc = p ? cbm_pipeline_run(p) : -1; cbm_store_t *s = rc == 0 ? cbm_store_open_path(db_path) : NULL; const char *pr = p ? cbm_pipeline_project_name(p) : NULL; const char *rce = "radarr.RadarrCalendarEntity"; char src[256]; snprintf(src, sizeof(src), "%s.write_state", rce); bool m_c3 = s && pyn_edge(s, pr, src, "base.CalendarEntity.write_state", "CALLS"); bool m_dfs = s && pyn_edge(s, pr, src, "base.Entity.write_state", "CALLS"); snprintf(src, sizeof(src), "%s.__init__", rce); bool i_c3 = s && pyn_edge(s, pr, src, "base.CalendarEntity.__init__", "CALLS"); bool i_dfs = s && pyn_edge(s, pr, src, "base.Entity.__init__", "CALLS"); bool plain = s && pyn_edge(s, pr, "radarr.PlainCalendar.write_state", "base.CalendarEntity.write_state", "CALLS"); if (s) { cbm_store_close(s); } if (p) { cbm_pipeline_free(p); } if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); ASSERT_EQ(rc, 0); ASSERT_TRUE(m_c3); /* super().write_state(): CalendarEntity comes before Entity */ ASSERT_FALSE(m_dfs); /* ... never Entity through RadarrEntity's ancestry */ ASSERT_TRUE(i_c3); /* super().__init__(): the same order */ ASSERT_FALSE(i_dfs); ASSERT_TRUE(plain); /* control: single inheritance */ PASS(); } TEST(pipeline_python_super_follows_c3_mro) { return pym_run_super_c3(0); } TEST(pipeline_python_super_follows_c3_mro_parallel) { return pym_run_super_c3(52); } /* ── attribute lookup follows the C3 method resolution order ─────── */ /* The same diamond as the super() test, seen from an ordinary attribute: * `self.write_state()` in RadarrCalendarEntity and `e.write_state()` on a * RadarrCalendarEntity both look the method up along the class's C3 order, * which reaches CalendarEntity before the shared base Entity. */ static const char pya_radarr_py[] = "from base import CoordinatorEntity, CalendarEntity\n" "\n" "class RadarrEntity(CoordinatorEntity):\n" " pass\n" "\n" "class RadarrCalendarEntity(RadarrEntity, CalendarEntity):\n" " def refresh(self):\n" " return self.write_state()\n" "\n" "class PlainCalendar(CalendarEntity):\n" " def refresh(self):\n" " return self.write_state()\n" "\n" "def publish(e: RadarrCalendarEntity):\n" " return e.write_state()\n"; static int pya_run_attr_c3(int fillers) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_attrc3_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "base.py", pym_base_py); write_temp_file(tmp, "radarr.py", pya_radarr_py); for (int i = 0; i < fillers; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "filler%d.py", i); snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; if (fillers > 0) { cbm_setenv("CBM_WORKERS", "4", 1); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/attrc3.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); int rc = p ? cbm_pipeline_run(p) : -1; cbm_store_t *s = rc == 0 ? cbm_store_open_path(db_path) : NULL; const char *pr = p ? cbm_pipeline_project_name(p) : NULL; const char *c3 = "base.CalendarEntity.write_state"; const char *dfs = "base.Entity.write_state"; const char *self_src = "radarr.RadarrCalendarEntity.refresh"; bool self_c3 = s && pyn_edge(s, pr, self_src, c3, "CALLS"); bool self_dfs = s && pyn_edge(s, pr, self_src, dfs, "CALLS"); bool typed_c3 = s && pyn_edge(s, pr, "radarr.publish", c3, "CALLS"); bool typed_dfs = s && pyn_edge(s, pr, "radarr.publish", dfs, "CALLS"); bool plain = s && pyn_edge(s, pr, "radarr.PlainCalendar.refresh", c3, "CALLS"); if (s) { cbm_store_close(s); } if (p) { cbm_pipeline_free(p); } if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); ASSERT_EQ(rc, 0); ASSERT_TRUE(self_c3); /* self.write_state(): CalendarEntity before Entity */ ASSERT_FALSE(self_dfs); /* ... never Entity through RadarrEntity's ancestry */ ASSERT_TRUE(typed_c3); /* e.write_state() on a RadarrCalendarEntity */ ASSERT_FALSE(typed_dfs); ASSERT_TRUE(plain); /* control: single inheritance */ PASS(); } TEST(pipeline_python_attribute_follows_c3_mro) { return pya_run_attr_c3(0); } TEST(pipeline_python_attribute_follows_c3_mro_parallel) { return pya_run_attr_c3(52); } /* L7 review of #1548: a package __init__.py function can share a submodule's QN * (poetry: layouts/__init__.py `def layout` is the QN prefix of every symbol in * layouts/layout.py). The function-local detection is bounded at the symbol's * own file, so class Layout stays a module-level, importable class. src/ layout * without a manifest: the module resolver misses `rowpkg.layout`, so the import * relies on the symbol-name fallback, which skips function locals -- unbounded, * Layout read as a local of layout() and its import and calls vanished. Both * resolvers; the second run re-indexes incrementally after an edit of * layout.py, so the incremental registry seed is held to the same bound. */ static const char pyn_layout_init_py[] = "def layout(name):\n" " return name\n"; static const char pyn_layout_mod_py[] = "class Layout:\n" " def __init__(self, name):\n" " self.name = name\n" "\n" " def create(self):\n" " return self.name\n"; static const char pyn_layout_app_py[] = "from rowpkg.layout import Layout\n" "\n" "def test_build():\n" " return Layout(\"demo\").create()\n"; static int pyn_run_init_collision(int fillers) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_initqn_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "src/rowpkg/__init__.py", pyn_layout_init_py); write_temp_file(tmp, "src/rowpkg/layout.py", pyn_layout_mod_py); write_temp_file(tmp, "tests/test_app.py", pyn_layout_app_py); for (int i = 0; i < fillers; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "filler%d.py", i); snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; if (fillers > 0) { cbm_setenv("CBM_WORKERS", "4", 1); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/initqn.db", tmp); int rc[2] = {-1, -1}; bool imports[2] = {false, false}; bool calls[2] = {false, false}; bool method[2] = {false, false}; for (int run = 0; run < 2; run++) { if (run == 1) { char path[512]; snprintf(path, sizeof(path), "%s/src/rowpkg/layout.py", tmp); th_append_file(path, "\n# edited: the second run re-indexes incrementally\n"); } cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); rc[run] = p ? cbm_pipeline_run(p) : -1; cbm_store_t *s = rc[run] == 0 ? cbm_store_open_path(db_path) : NULL; if (s) { const char *project = cbm_pipeline_project_name(p); imports[run] = pyn_edge(s, project, "tests.test_app.py.__file__", "src.rowpkg.layout.Layout", "IMPORTS"); calls[run] = pyn_edge(s, project, "tests.test_app.test_build", "src.rowpkg.layout.Layout", "CALLS"); method[run] = pyn_edge(s, project, "tests.test_app.test_build", "src.rowpkg.layout.Layout.create", "CALLS"); cbm_store_close(s); } if (p) { cbm_pipeline_free(p); } } if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); for (int run = 0; run < 2; run++) { ASSERT_EQ(rc[run], 0); ASSERT_TRUE(imports[run]); /* the class stays importable */ ASSERT_TRUE(calls[run]); /* Layout("demo") */ ASSERT_TRUE(method[run]); /* .create() through the constructed type */ } PASS(); } TEST(pipeline_python_init_function_shares_submodule_qn_issue1548) { return pyn_run_init_collision(0); } TEST(pipeline_python_init_function_shares_submodule_qn_parallel_issue1548) { return pyn_run_init_collision(52); } /* ── F15: a nested class's bare base resolves in its class body first ── */ /* The sqlalchemy asyncpg shape: an adapter class re-declares the DBAPI * exception hierarchy as nested classes, each based on an EARLIER sibling * spelled bare (`class IntegrityError(DatabaseError)`). Python evaluates a * class statement's bases in the enclosing class body, so the sibling wins * over the same-named module classes of exc.py, which the adapter imports * as a module (so a name guess prefers them, as on sqlalchemy once its * relative `from ... import exc` resolves). A sibling defined LATER in the * body is not bound yet: `Early(Later)` names the module-level Later. */ static const char pyb_exc_py[] = "class DBAPIError(Exception):\n" " pass\n" "\n" "class DatabaseError(DBAPIError):\n" " pass\n" "\n" "class IntegrityError(DatabaseError):\n" " pass\n"; static const char pyb_adapter_py[] = "import exc\n" "from exc import DBAPIError\n" "\n" "class Later(Exception):\n" " pass\n" "\n" "class AsyncAdapt_dbapi:\n" " class Error(DBAPIError):\n" " pass\n" "\n" " class DatabaseError(Error):\n" " pass\n" "\n" " class IntegrityError(DatabaseError):\n" " pass\n" "\n" " class NotNullViolationError(IntegrityError):\n" " pass\n" "\n" " class Early(Later):\n" " pass\n" "\n" " class Later(Exception):\n" " pass\n"; /* A second adapter with the same nested names, as sqlalchemy has one per * driver: a name-only guess has several same-named candidates to pick. */ static const char pyb_adapter2_py[] = "class AsyncAdapt_other_dbapi:\n" " class Error(Exception):\n" " pass\n" "\n" " class DatabaseError(Error):\n" " pass\n" "\n" " class IntegrityError(DatabaseError):\n" " pass\n"; static int pyb_run_class_body_base(int fillers) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_cbbase_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "exc.py", pyb_exc_py); write_temp_file(tmp, "adapter.py", pyb_adapter_py); write_temp_file(tmp, "adapter2.py", pyb_adapter2_py); for (int i = 0; i < fillers; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "filler%d.py", i); snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; if (fillers > 0) { cbm_setenv("CBM_WORKERS", "4", 1); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/cbbase.db", tmp); int rc[2] = {-1, -1}; bool db_sib[2] = {false, false}; bool db_mod[2] = {true, true}; bool ie_sib[2] = {false, false}; bool ie_mod[2] = {true, true}; bool nn_sib[2] = {false, false}; bool err_imp[2] = {false, false}; bool early_mod[2] = {false, false}; bool early_later_sib[2] = {true, true}; bool other_sib[2] = {false, false}; for (int run = 0; run < 2; run++) { if (run == 1) { char path[512]; snprintf(path, sizeof(path), "%s/adapter.py", tmp); th_append_file(path, "\n# edited: the second run re-indexes incrementally\n"); } cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); rc[run] = p ? cbm_pipeline_run(p) : -1; cbm_store_t *s = rc[run] == 0 ? cbm_store_open_path(db_path) : NULL; if (s) { const char *pr = cbm_pipeline_project_name(p); const char *cls = "adapter.AsyncAdapt_dbapi"; char a[256]; char b[256]; snprintf(a, sizeof(a), "%s.DatabaseError", cls); snprintf(b, sizeof(b), "%s.Error", cls); db_sib[run] = pyn_edge(s, pr, a, b, "INHERITS"); db_mod[run] = pyn_edge(s, pr, a, "adapter2.AsyncAdapt_other_dbapi.Error", "INHERITS"); snprintf(a, sizeof(a), "%s.IntegrityError", cls); snprintf(b, sizeof(b), "%s.DatabaseError", cls); ie_sib[run] = pyn_edge(s, pr, a, b, "INHERITS"); ie_mod[run] = pyn_edge(s, pr, a, "exc.DatabaseError", "INHERITS"); snprintf(a, sizeof(a), "%s.NotNullViolationError", cls); snprintf(b, sizeof(b), "%s.IntegrityError", cls); nn_sib[run] = pyn_edge(s, pr, a, b, "INHERITS"); snprintf(a, sizeof(a), "%s.Error", cls); err_imp[run] = pyn_edge(s, pr, a, "exc.DBAPIError", "INHERITS"); snprintf(a, sizeof(a), "%s.Early", cls); snprintf(b, sizeof(b), "%s.Later", cls); early_mod[run] = pyn_edge(s, pr, a, "adapter.Later", "INHERITS"); early_later_sib[run] = pyn_edge(s, pr, a, b, "INHERITS"); other_sib[run] = pyn_edge(s, pr, "adapter2.AsyncAdapt_other_dbapi.IntegrityError", "adapter2.AsyncAdapt_other_dbapi.DatabaseError", "INHERITS"); cbm_store_close(s); } if (p) { cbm_pipeline_free(p); } } if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); for (int run = 0; run < 2; run++) { ASSERT_EQ(rc[run], 0); ASSERT_TRUE(db_sib[run]); /* DatabaseError(Error): the earlier sibling */ ASSERT_FALSE(db_mod[run]); /* ... not the other adapter's Error */ ASSERT_TRUE(ie_sib[run]); /* IntegrityError(DatabaseError): the sibling */ ASSERT_FALSE(ie_mod[run]); /* ... not exc.DatabaseError */ ASSERT_TRUE(nn_sib[run]); /* a chain of siblings */ ASSERT_TRUE(err_imp[run]); /* control: a non-sibling base keeps the import */ ASSERT_TRUE(early_mod[run]); /* a LATER sibling is not bound yet: module Later */ ASSERT_FALSE(early_later_sib[run]); /* ... never the later sibling */ ASSERT_TRUE(other_sib[run]); /* the second adapter binds its own sibling */ } PASS(); } TEST(pipeline_python_class_body_sibling_base) { return pyb_run_class_body_base(0); } TEST(pipeline_python_class_body_sibling_base_parallel) { return pyb_run_class_body_base(52); } /* #1548 keeps TESTS for calls made through a test's own nested helper: the * call now starts at the extracted helper def (not at the test FILE node, as * before #1548), so TESTS is attributed to the enclosing test function. A * helper nested in a NON-test function stays without TESTS, like a module-level * helper. Both resolvers. */ static const char pyn_tests_prod_py[] = "def target(value):\n" " return value\n" "\n" "def other_target(value):\n" " return value\n"; static const char pyn_tests_test_py[] = "from pkg.prod import target, other_target\n" "\n" "def test_through_nested_helper():\n" " def check(v):\n" " return target(v)\n" " assert check(1) == 1\n" "\n" "class TestGroup:\n" " def test_method_helper(self):\n" " def probe():\n" " return target(2)\n" " assert probe() == 2\n" "\n" "def make_fixture():\n" " def build():\n" " return other_target(3)\n" " return build\n"; static int pyn_run_nested_helper_tests(int fillers) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_ntests_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "pkg/__init__.py", ""); write_temp_file(tmp, "pkg/prod.py", pyn_tests_prod_py); write_temp_file(tmp, "tests/test_rows.py", pyn_tests_test_py); for (int i = 0; i < fillers; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "filler%d.py", i); snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; if (fillers > 0) { cbm_setenv("CBM_WORKERS", "4", 1); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/ntests.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); int rc = p ? cbm_pipeline_run(p) : -1; cbm_store_t *s = rc == 0 ? cbm_store_open_path(db_path) : NULL; bool fn_tests = false, method_tests = false, helper_calls = false, fixture_tests = true; if (s) { const char *project = cbm_pipeline_project_name(p); helper_calls = pyn_edge(s, project, "tests.test_rows.test_through_nested_helper.check", "pkg.prod.target", "CALLS"); fn_tests = pyn_edge(s, project, "tests.test_rows.test_through_nested_helper", "pkg.prod.target", "TESTS"); method_tests = pyn_edge(s, project, "tests.test_rows.TestGroup.test_method_helper", "pkg.prod.target", "TESTS"); fixture_tests = pyn_edge(s, project, "tests.test_rows.make_fixture", "pkg.prod.other_target", "TESTS") || pyn_edge(s, project, "tests.test_rows.make_fixture.build", "pkg.prod.other_target", "TESTS"); cbm_store_close(s); } if (p) { cbm_pipeline_free(p); } if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); ASSERT_EQ(rc, 0); ASSERT_TRUE(helper_calls); /* the call starts at the nested helper (#1548) */ ASSERT_TRUE(fn_tests); /* ... and the enclosing test function still TESTS the target */ ASSERT_TRUE(method_tests); /* the same inside a test method */ ASSERT_FALSE(fixture_tests); /* a helper nested in a non-test function: no TESTS */ PASS(); } TEST(pipeline_python_nested_helper_keeps_tests_issue1548) { return pyn_run_nested_helper_tests(0); } TEST(pipeline_python_nested_helper_keeps_tests_parallel_issue1548) { return pyn_run_nested_helper_tests(52); } /* A decorated class nested in a class body (`@dataclass class Meta:` inside a * class) is a nested class like an undecorated one: it gets its Class node and * its methods their Method nodes. push_nested_class_nodes looked through * field/template/declaration and conditional wrappers but not through Python's * decorated_definition, so the decorated class and everything in it was never * extracted. Both resolvers. */ static const char pyn_decorated_nested_py[] = "def deco(cls):\n" " return cls\n" "\n" "class Outer:\n" " @deco\n" " class Decorated:\n" " def run(self):\n" " return 1\n" "\n" " class Plain:\n" " def go(self):\n" " return 2\n"; static bool pyn_node_exists(cbm_store_t *s, const char *project, const char *rel_qn, const char *label) { char qn[512]; pyn_qn(qn, sizeof(qn), project, rel_qn); cbm_node_t n = {0}; bool found = cbm_store_find_node_by_qn(s, project, qn, &n) == CBM_STORE_OK && n.label && strcmp(n.label, label) == 0; cbm_node_free_fields(&n); return found; } static int pyn_run_decorated_nested(int fillers) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_decnest_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "sd.py", pyn_decorated_nested_py); for (int i = 0; i < fillers; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "filler%d.py", i); snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; if (fillers > 0) { cbm_setenv("CBM_WORKERS", "4", 1); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/decnest.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); int rc = p ? cbm_pipeline_run(p) : -1; cbm_store_t *s = rc == 0 ? cbm_store_open_path(db_path) : NULL; const char *project = p ? cbm_pipeline_project_name(p) : NULL; bool dec_class = s && pyn_node_exists(s, project, "sd.Outer.Decorated", "Class"); bool dec_method = s && pyn_node_exists(s, project, "sd.Outer.Decorated.run", "Method"); bool plain_class = s && pyn_node_exists(s, project, "sd.Outer.Plain", "Class"); bool plain_method = s && pyn_node_exists(s, project, "sd.Outer.Plain.go", "Method"); if (s) { cbm_store_close(s); } if (p) { cbm_pipeline_free(p); } if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); ASSERT_EQ(rc, 0); ASSERT_TRUE(dec_class); /* the decorated nested class is extracted */ ASSERT_TRUE(dec_method); /* ... and its method */ ASSERT_TRUE(plain_class); /* control: undecorated nested class */ ASSERT_TRUE(plain_method); /* control */ PASS(); } TEST(pipeline_python_decorated_nested_class_extracted) { return pyn_run_decorated_nested(0); } TEST(pipeline_python_decorated_nested_class_extracted_parallel) { return pyn_run_decorated_nested(52); } /* A class nested in a class is walked by the Python LSP under its outer * class (m.Outer.Inner): a typed-parameter call, a self call and a * constructor call in its methods resolve, and so does a decorated nested * class. Walked under the module (m.Inner) none of the LSP rows joined the * method nodes, so the calls fell to name guesses or got no edge. */ static const char pyn_nested_class_py[] = "class Helper:\n" " def go(self):\n" " return 1\n" "\n" "def deco(cls):\n" " return cls\n" "\n" "class Outer:\n" " class Inner:\n" " def run(self, h: Helper):\n" " return h.go()\n" "\n" " def again(self):\n" " return self.run(Helper())\n" "\n" " @deco\n" " class Decorated:\n" " def run(self, h: Helper):\n" " return h.go()\n"; static int pyn_run_nested_class(int fillers) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_nestcls_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "sn.py", pyn_nested_class_py); for (int i = 0; i < fillers; i++) { char name[64]; char body[128]; snprintf(name, sizeof(name), "filler%d.py", i); snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i); write_temp_file(tmp, name, body); } char *old_workers = getenv("CBM_WORKERS"); char *saved = old_workers ? strdup(old_workers) : NULL; if (fillers > 0) { cbm_setenv("CBM_WORKERS", "4", 1); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/nestcls.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); int rc = p ? cbm_pipeline_run(p) : -1; cbm_store_t *s = rc == 0 ? cbm_store_open_path(db_path) : NULL; const char *project = p ? cbm_pipeline_project_name(p) : NULL; bool typed = s && pyn_edge_with(s, project, "sn.Outer.Inner.run", "sn.Helper.go", "CALLS", "\"strategy\":\"lsp_"); bool self_call = s && pyn_edge_with(s, project, "sn.Outer.Inner.again", "sn.Outer.Inner.run", "CALLS", "\"strategy\":\"lsp_"); bool ctor = s && pyn_edge_with(s, project, "sn.Outer.Inner.again", "sn.Helper", "CALLS", "\"strategy\":\"lsp_"); bool decorated = s && pyn_edge_with(s, project, "sn.Outer.Decorated.run", "sn.Helper.go", "CALLS", "\"strategy\":\"lsp_"); if (s) { cbm_store_close(s); } if (p) { cbm_pipeline_free(p); } if (saved) { cbm_setenv("CBM_WORKERS", saved, 1); free(saved); } else { cbm_unsetenv("CBM_WORKERS"); } th_rmtree(tmp); ASSERT_EQ(rc, 0); ASSERT_TRUE(typed); ASSERT_TRUE(self_call); ASSERT_TRUE(ctor); ASSERT_TRUE(decorated); PASS(); } TEST(pipeline_python_nested_class_methods_resolve) { return pyn_run_nested_class(0); } TEST(pipeline_python_nested_class_methods_resolve_parallel) { return pyn_run_nested_class(52); } TEST(pipeline_python_project) { /* Port of TestPipelinePythonProject */ const char *files[] = {"main.py", "utils.py"}; const char *contents[] = { "def greet(name):\n return f\"Hello, {name}\"\n\n" "def process():\n result = greet(\"world\")\n return result\n", "API_URL = \"https://example.com/api\"\nMAX_RETRIES = 3\n\n" "def fetch_data(url):\n pass\n\n" "class DataProcessor:\n def transform(self, data):\n return data\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); cbm_node_t *funcs = NULL; int fc = 0; cbm_store_find_nodes_by_label(s, proj, "Function", &funcs, &fc); ASSERT_GTE(fc, 3); /* greet, process, fetch_data */ cbm_store_free_nodes(funcs, fc); cbm_node_t *cls = NULL; int cc = 0; cbm_store_find_nodes_by_label(s, proj, "Class", &cls, &cc); ASSERT_GTE(cc, 1); /* DataProcessor */ cbm_store_free_nodes(cls, cc); cbm_node_t *methods = NULL; int mc = 0; cbm_store_find_nodes_by_label(s, proj, "Method", &methods, &mc); ASSERT_GTE(mc, 1); /* transform */ cbm_store_free_nodes(methods, mc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } /* Edges of `edge_type` from the node `.` to `.` * (QNs, not names: a C macro and the definition it shadows share their name). */ static int c1_edge_count_qn(cbm_store_t *s, const char *project, const char *edge_type, const char *source_tail, const char *target_tail) { char source_qn[512]; char target_qn[512]; snprintf(source_qn, sizeof(source_qn), "%s.%s", project, source_tail); snprintf(target_qn, sizeof(target_qn), "%s.%s", project, target_tail); cbm_node_t source = {0}; cbm_node_t target = {0}; int matches = -1; if (cbm_store_find_node_by_qn(s, project, source_qn, &source) == CBM_STORE_OK && cbm_store_find_node_by_qn(s, project, target_qn, &target) == CBM_STORE_OK) { cbm_edge_t *edges = NULL; int edge_count = 0; matches = 0; if (cbm_store_find_edges_by_source_type(s, source.id, edge_type, &edges, &edge_count) == CBM_STORE_OK) { for (int i = 0; i < edge_count; i++) { matches += edges[i].target_id == target.id; } cbm_store_free_edges(edges, edge_count); } } cbm_node_free_fields(&source); cbm_node_free_fields(&target); return matches; } /* Label of the node `.`, copied into `out`; "" when there is none. */ static const char *c1_label_of(cbm_store_t *s, const char *project, const char *qn_tail, char *out, size_t out_size) { char qn[512]; snprintf(qn, sizeof(qn), "%s.%s", project, qn_tail); cbm_node_t node = {0}; out[0] = '\0'; if (cbm_store_find_node_by_qn(s, project, qn, &node) == CBM_STORE_OK && node.label) { snprintf(out, out_size, "%s", node.label); } cbm_node_free_fields(&node); return out; } /* Do the properties of the node `.` contain `needle`? */ static bool c1_props_contain(cbm_store_t *s, const char *project, const char *qn_tail, const char *needle) { char qn[512]; snprintf(qn, sizeof(qn), "%s.%s", project, qn_tail); cbm_node_t node = {0}; bool found = cbm_store_find_node_by_qn(s, project, qn, &node) == CBM_STORE_OK && node.properties_json && strstr(node.properties_json, needle) != NULL; cbm_node_free_fields(&node); return found; } /* PR C1, end to end: C keeps one graph node per QN, so the right entity must win. * A header and its .c share the module QN (proj.s): `struct S s_global;` in s.c * minted a Class for the REFERENCE and "smallest path wins" handed it the node (redis: * struct redisServer pointed at server.c:85). A function and its #else macro * stand-in shared a QN, and the later macro line won (curl url_match_proxy_use, 41 * functions): the macro now has a QN of its own (`...#macro`), so both are nodes. * The typedef alias had no node at all. */ TEST(pipeline_c_definitions_own_their_qn_c1) { const char *files[] = {"s.h", "s.c"}; const char *contents[] = {"struct S {\n" " int v;\n" "};\n" "typedef struct S S_t;\n", "#include \"s.h\"\n" "struct S s_global;\n" "#ifndef DISABLE_PROXY\n" "static int match_proxy(int a)\n" "{\n" " return a + 1;\n" "}\n" "#else\n" "#define match_proxy(a) 1\n" "#endif\n" "int use(void) { return match_proxy(s_global.v); }\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); cbm_node_t *nodes = NULL; int n = 0; cbm_store_find_nodes_by_name(s, proj, "S", &nodes, &n); ASSERT_EQ(n, 1); ASSERT_STR_EQ(nodes[0].label, "Class"); ASSERT_STR_EQ(nodes[0].file_path, "s.h"); ASSERT_EQ(nodes[0].start_line, 1); cbm_store_free_nodes(nodes, n); /* the function and its #else macro stand-in: two nodes, the plain QN is the * function's */ cbm_store_find_nodes_by_name(s, proj, "match_proxy", &nodes, &n); ASSERT_EQ(n, 2); cbm_store_free_nodes(nodes, n); char qn[512]; cbm_node_t fn = {0}; snprintf(qn, sizeof(qn), "%s.s.match_proxy", proj); ASSERT_EQ(cbm_store_find_node_by_qn(s, proj, qn, &fn), CBM_STORE_OK); ASSERT_STR_EQ(fn.label, "Function"); ASSERT_EQ(fn.start_line, 4); cbm_node_free_fields(&fn); cbm_node_t mac = {0}; snprintf(qn, sizeof(qn), "%s.s.match_proxy#macro", proj); ASSERT_EQ(cbm_store_find_node_by_qn(s, proj, qn, &mac), CBM_STORE_OK); ASSERT_STR_EQ(mac.label, "Macro"); ASSERT_STR_EQ(mac.name, "match_proxy"); ASSERT_EQ(mac.start_line, 9); cbm_node_free_fields(&mac); cbm_store_find_nodes_by_name(s, proj, "S_t", &nodes, &n); ASSERT_EQ(n, 1); ASSERT_STR_EQ(nodes[0].label, "Type"); cbm_store_free_nodes(nodes, n); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } /* The tie rule of the macro namespace: a name visible both as a definition and as a * macro resolves to the DEFINITION; the macro is the target only when no definition * of that name is visible. `pick_fn` is a function in one #if branch and a macro in * the other: before, one node held the QN and it was the macro (the later line), so * the call landed on a Macro. `renamed_fn` is declared by a prototype whose name a * rename macro replaces (jemalloc smallocx): no definition exists in the repo, the * macro is the only node, and the call keeps pointing at it. */ TEST(pipeline_c_call_targets_definition_before_macro_c1) { const char *files[] = {"tie.c"}; const char *contents[] = {"#define renamed_fn NS_renamed_fn\n" "extern int renamed_fn(int v);\n" "#ifdef USE_REAL\n" "static int pick_fn(int v) { return v; }\n" "#else\n" "#define pick_fn(v) (v)\n" "#endif\n" "int caller(int b) {\n" " return pick_fn(b) + renamed_fn(b);\n" "}\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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); char proj[256]; snprintf(proj, sizeof(proj), "%s", cbm_pipeline_project_name(p)); char label[64]; ASSERT_STR_EQ(c1_label_of(s, proj, "tie.pick_fn", label, sizeof(label)), "Function"); ASSERT_STR_EQ(c1_label_of(s, proj, "tie.pick_fn#macro", label, sizeof(label)), "Macro"); ASSERT_STR_EQ(c1_label_of(s, proj, "tie.renamed_fn#macro", label, sizeof(label)), "Macro"); ASSERT_STR_EQ(c1_label_of(s, proj, "tie.renamed_fn", label, sizeof(label)), ""); int to_definition = c1_edge_count_qn(s, proj, "CALLS", "tie.caller", "tie.pick_fn"); int to_shadowed_macro = c1_edge_count_qn(s, proj, "CALLS", "tie.caller", "tie.pick_fn#macro"); int to_only_macro = c1_edge_count_qn(s, proj, "CALLS", "tie.caller", "tie.renamed_fn#macro"); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); ASSERT_EQ(to_definition, 1); ASSERT_EQ(to_shadowed_macro, 0); ASSERT_EQ(to_only_macro, 1); PASS(); } /* A flattened enumerator is still what `E::A` names: the reference resolves by its * leaf, so `Color::RED`, a namespace-qualified and a class-qualified spelling and the * bare `GREEN` all reach the node `.`; a scoped enum's `Mode::FAST` * reaches `.Mode.FAST`. */ TEST(pipeline_cpp_enum_reference_reaches_flat_enumerator_c1) { const char *files[] = {"shapes.hpp", "use.cpp"}; const char *contents[] = {"enum Color { RED, GREEN };\n" "namespace gfx {\n" "enum Blend { ADD, MULTIPLY };\n" "class Brush {\n" "public:\n" " enum Shape { ROUND, SQUARE };\n" "};\n" "}\n" "enum class Mode { FAST, SLOW };\n", "#include \"shapes.hpp\"\n" "int pick(int v) {\n" " if (v == Color::RED) return 1;\n" " if (v == gfx::Blend::ADD) return 2;\n" " if (v == gfx::Brush::ROUND) return 3;\n" " if (v == static_cast(Mode::FAST)) return 4;\n" " if (v == GREEN) return 5;\n" " return 0;\n" "}\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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); char proj[256]; snprintf(proj, sizeof(proj), "%s", cbm_pipeline_project_name(p)); static const char *const targets[] = {"shapes.RED", "shapes.gfx.ADD", "shapes.gfx.Brush.ROUND", "shapes.Mode.FAST", "shapes.GREEN"}; int usage[5]; for (int i = 0; i < 5; i++) { usage[i] = c1_edge_count_qn(s, proj, "USAGE", "use.pick", targets[i]); } char nested[64]; c1_label_of(s, proj, "shapes.Color.RED", nested, sizeof(nested)); /* "the constants of enum Color" still enumerates: parent_class is the membership */ char parent_marker[512]; snprintf(parent_marker, sizeof(parent_marker), "\"parent_class\":\"%s.shapes.Color\"", proj); cbm_node_t *vars = NULL; int var_count = 0; int color_members = 0; cbm_store_find_nodes_by_label(s, proj, "Variable", &vars, &var_count); for (int i = 0; i < var_count; i++) { color_members += vars[i].properties_json && strstr(vars[i].properties_json, parent_marker) != NULL; } cbm_store_free_nodes(vars, var_count); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); for (int i = 0; i < 5; i++) { if (usage[i] != 1) { fprintf(stderr, " [c1] USAGE use.pick -> %s: %d\n", targets[i], usage[i]); } ASSERT_EQ(usage[i], 1); } ASSERT_STR_EQ(nested, ""); ASSERT_EQ(color_members, 2); PASS(); } /* C, an enum declared inside a struct (curl lib/cf-h1-proxy.c `enum keeponval {...} * keepon;`): its enumerators are file-scope names, so their nodes are `.` * and the unqualified references in the code reach them. The struct is no QN segment, * for the named enum and the anonymous one alike. */ TEST(pipeline_c_enum_inside_struct_reference_resolves_c1) { const char *files[] = {"link.c"}; const char *contents[] = {"struct conn {\n" " enum state { ST_IDLE, ST_BUSY } st;\n" " enum { KIND_A, KIND_B } kind;\n" " int fd;\n" "};\n" "int busy(struct conn *c) {\n" " return c->st == ST_BUSY || c->kind == KIND_B;\n" "}\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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); char proj[256]; snprintf(proj, sizeof(proj), "%s", cbm_pipeline_project_name(p)); int to_named = c1_edge_count_qn(s, proj, "USAGE", "link.busy", "link.ST_BUSY"); int to_anonymous = c1_edge_count_qn(s, proj, "USAGE", "link.busy", "link.KIND_B"); char in_struct[64]; c1_label_of(s, proj, "link.conn.ST_BUSY", in_struct, sizeof(in_struct)); char parent_marker[512]; snprintf(parent_marker, sizeof(parent_marker), "\"parent_class\":\"%s.link.conn.state\"", proj); bool named_keeps_parent = c1_props_contain(s, proj, "link.ST_BUSY", parent_marker); bool anonymous_has_parent = c1_props_contain(s, proj, "link.KIND_B", "\"parent_class\""); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); ASSERT_EQ(to_named, 1); ASSERT_EQ(to_anonymous, 1); ASSERT_STR_EQ(in_struct, ""); ASSERT_TRUE(named_keeps_parent); ASSERT_FALSE(anonymous_has_parent); PASS(); } typedef struct { int rc; bool opened; bool pick_lists_both; /* .v.pick carries the two spans, in order */ bool limit_has_list; /* .v.LIMIT#macro carries a list */ bool once_has_list; /* a name defined once must not */ int other_lang_nodes; /* nodes of cmd.json */ int other_lang_lists; /* ... that carry a list (must be 0) */ } c1_variants_obs_t; static c1_variants_obs_t c1_observe_variants(const char *repo, const char *db_name) { c1_variants_obs_t obs = {0}; char db[512]; snprintf(db, sizeof(db), "%s/%s", repo, db_name); cbm_pipeline_t *p = cbm_pipeline_new(repo, db, CBM_MODE_FULL); if (!p) { obs.rc = -1; return obs; } obs.rc = cbm_pipeline_run(p); char proj[256]; snprintf(proj, sizeof(proj), "%s", cbm_pipeline_project_name(p)); cbm_pipeline_free(p); cbm_store_t *s = cbm_store_open_path(db); if (!s) { return obs; } obs.opened = true; 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; if (cbm_store_find_nodes_by_file_overlap(s, proj, "cmd.json", 1, 1000, &nodes, &count) == CBM_STORE_OK) { obs.other_lang_nodes = count; for (int i = 0; i < count; i++) { obs.other_lang_lists += nodes[i].properties_json && strstr(nodes[i].properties_json, "\"variants\"") != NULL; } cbm_store_free_nodes(nodes, count); } cbm_store_close(s); return obs; } /* `variants` reaches the node on BOTH execution paths. The property is computed per * file at extraction; the sequential pass (pass_definitions.c) and the parallel one * (pass_parallel.c) each serialize it, and the properties buffer is sized for the * list, so a path that forgot either would publish a node without it. One fixture, * two runs: CBM_INDEX_SINGLE_THREAD forces the sequential path, the 55 fillers plus * CBM_WORKERS select the parallel one. A JSON file with a repeated key rides along: * no language outside the C preprocessor ones gets the property. */ TEST(pipeline_c_variants_on_sequential_and_parallel_paths_c1) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_c1_variants_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } char path[512]; snprintf(path, sizeof(path), "%s/v.c", tmp); int write_rc = th_write_file(path, "#if A\n" "int pick(int a) { return a; }\n" "#else\n" "int pick(int a) {\n" " return a + 1;\n" "}\n" "#endif\n" "#ifdef B\n" "#define LIMIT 1\n" "#else\n" "#define LIMIT 2\n" "#endif\n" "int once(void) { return 0; }\n"); snprintf(path, sizeof(path), "%s/cmd.json", tmp); /* the repeated key on two lines: one span would never be listed, gate or no gate */ write_rc |= th_write_file(path, "{\n \"a\": {\"name\": 1},\n \"b\": {\"name\": 2}\n}\n"); for (int i = 0; i < 55; i++) { char source[96]; snprintf(path, sizeof(path), "%s/pad_%02d.c", tmp, i); snprintf(source, sizeof(source), "int c1_pad_%02d(void) { return %d; }\n", i, i); write_rc |= th_write_file(path, source); } if (write_rc != 0) { th_rmtree(tmp); FAIL("failed to write the variants fixture"); } char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); char *saved_single = old_single ? strdup(old_single) : NULL; cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1); c1_variants_obs_t sequential = c1_observe_variants(tmp, "variants-sequential.db"); cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); cbm_setenv("CBM_WORKERS", "4", 1); c1_variants_obs_t parallel = c1_observe_variants(tmp, "variants-parallel.db"); if (saved_workers) { cbm_setenv("CBM_WORKERS", saved_workers, 1); free(saved_workers); } else { cbm_unsetenv("CBM_WORKERS"); } if (saved_single) { cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); free(saved_single); } else { cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); } th_rmtree(tmp); const c1_variants_obs_t *runs[] = {&sequential, ¶llel}; for (int i = 0; i < 2; i++) { if (!runs[i]->pick_lists_both || !runs[i]->limit_has_list || runs[i]->once_has_list || runs[i]->other_lang_lists != 0) { fprintf( stderr, " [c1-variants] path=%s pick=%d limit=%d once=%d json_nodes=%d json_lists=%d\n", i == 0 ? "sequential" : "parallel", runs[i]->pick_lists_both, runs[i]->limit_has_list, runs[i]->once_has_list, runs[i]->other_lang_nodes, runs[i]->other_lang_lists); } } /* the property on each path first, the language limit second: one concern must * not hide the other */ for (int i = 0; i < 2; i++) { ASSERT_EQ(runs[i]->rc, 0); ASSERT_TRUE(runs[i]->opened); ASSERT_TRUE(runs[i]->pick_lists_both); ASSERT_TRUE(runs[i]->limit_has_list); ASSERT_FALSE(runs[i]->once_has_list); } for (int i = 0; i < 2; i++) { ASSERT_GT(runs[i]->other_lang_nodes, 0); ASSERT_EQ(runs[i]->other_lang_lists, 0); } PASS(); } /* C++ overloads share a QN (the signature is no part of it), so two of them in one * file are the same shape as two #if branches: one node -- the last by start line -- * and `variants` lists both spans. */ TEST(pipeline_cpp_overloads_are_listed_as_variants_c1) { const char *files[] = {"ov.cpp"}; const char *contents[] = {"int scale(int v) { return v * 2; }\n" "double scale(double v) {\n" " return v * 2.0;\n" "}\n" "int once(int v) { return scale(v); }\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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); char proj[256]; snprintf(proj, sizeof(proj), "%s", cbm_pipeline_project_name(p)); cbm_node_t *nodes = NULL; int n = 0; cbm_store_find_nodes_by_name(s, proj, "scale", &nodes, &n); 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\":[{\"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); cbm_pipeline_free(p); teardown_lang_repo(); ASSERT_EQ(scale_nodes, 1); ASSERT_EQ(kept_start, 2); ASSERT_TRUE(lists_both); ASSERT_FALSE(once_has_list); PASS(); } /* CBM_SEMANTIC_INDEX_VERSION 4: the C-family node identities changed (macro QNs end * in "#macro", unscoped enumerators are flat, typedef names are nodes). An index * written at version 3 holds the old QNs for every file that did not change, and an * unchanged repository is otherwise a no-op, so the version is what makes the new * binary rebuild it. The stored index is put back to the version-3 state by hand * (metadata and the old macro QN); the run after that must replace it in full. */ TEST(pipeline_semantic_version_3_index_is_rebuilt_in_full_c1) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_c1_version_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_temp_file(tmp, "cfg.c", "#define LIMIT 10\nint limit(void) { return LIMIT; }\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/index.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *first = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(first); ASSERT_EQ(cbm_pipeline_run(first), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(first)); cbm_pipeline_free(first); /* Put the published index back to what version 3 wrote. */ cbm_store_t *store = cbm_store_open_path(db_path); ASSERT_NOT_NULL(store); char label[64]; bool fenced_before = strcmp(c1_label_of(store, project, "cfg.LIMIT#macro", label, sizeof(label)), "Macro") == 0; cbm_coverage_row_t *coverage_rows = NULL; int coverage_count = 0; ASSERT_EQ(cbm_store_coverage_get(store, project, &coverage_rows, &coverage_count), CBM_STORE_OK); cbm_coverage_meta_t meta = {0}; ASSERT_EQ(cbm_store_coverage_meta_get(store, project, &meta), CBM_STORE_OK); cbm_coverage_meta_t old_meta = meta; old_meta.coverage_version = 3; ASSERT_EQ( cbm_store_coverage_replace_ex(store, project, coverage_rows, coverage_count, &old_meta), CBM_STORE_OK); cbm_store_free_coverage(coverage_rows, coverage_count); cbm_store_coverage_meta_clear(&meta); ASSERT_EQ(cbm_store_exec(store, "UPDATE nodes SET qualified_name = " "substr(qualified_name, 1, length(qualified_name) - 6) " "WHERE label = 'Macro' AND qualified_name LIKE '%#macro';"), CBM_STORE_OK); bool plain_after_downgrade = strcmp(c1_label_of(store, project, "cfg.LIMIT", label, sizeof(label)), "Macro") == 0; cbm_store_close(store); /* Nothing in the repository changed: only the version says the index is stale. */ cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *upgrade = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(upgrade); int upgrade_rc = cbm_pipeline_run(upgrade); cbm_incremental_route_t upgrade_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(upgrade); store = cbm_store_open_path(db_path); ASSERT_NOT_NULL(store); bool fenced_after = strcmp(c1_label_of(store, project, "cfg.LIMIT#macro", label, sizeof(label)), "Macro") == 0; bool plain_after = strcmp(c1_label_of(store, project, "cfg.LIMIT", label, sizeof(label)), "Macro") == 0; cbm_coverage_meta_t new_meta = {0}; ASSERT_EQ(cbm_store_coverage_meta_get(store, project, &new_meta), CBM_STORE_OK); int stored_version = new_meta.coverage_version; cbm_store_coverage_meta_clear(&new_meta); cbm_store_close(store); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_TRUE(fenced_before); ASSERT_TRUE(plain_after_downgrade); ASSERT_EQ(upgrade_rc, 0); ASSERT_EQ(upgrade_route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(stored_version, CBM_SEMANTIC_INDEX_VERSION); ASSERT_TRUE(fenced_after); ASSERT_FALSE(plain_after); ASSERT_GTE(CBM_SEMANTIC_INDEX_VERSION, 4); PASS(); } /* `#include ` from arch/x/bugs.c: two headers end with the * include path (include/linux/device.h, tools/virtio/linux/device.h) and each * declares `struct device`. The exact-file lookup returned the first matching * node in by-name bucket order — the workers' merge order, different run to * run: 5,821 kernel IMPORTS edges moved between two indexes of one tree and * the CALLS resolved through their import maps moved with them. The target is * the least nested File node, whichever order the nodes were registered in. */ static void add_device_header(cbm_gbuf_t *gb, const char *rel, const char *qn_stem) { char fqn[256]; snprintf(fqn, sizeof(fqn), "%s.h.__file__", qn_stem); cbm_gbuf_upsert_node(gb, "File", "device.h", fqn, rel, 0, 0, "{}"); char cqn[256]; snprintf(cqn, sizeof(cqn), "%s.device", qn_stem); cbm_gbuf_upsert_node(gb, "Class", "device", cqn, rel, 10, 40, "{}"); } static const cbm_gbuf_node_t *resolve_device_h_from(cbm_gbuf_t *gb) { atomic_int cancelled = 0; cbm_pipeline_ctx_t ctx = { .project_name = "p", .repo_path = "/tmp/p", .gbuf = gb, .registry = NULL, .cancelled = &cancelled, }; CBMImport imp = {0}; imp.module_path = "linux/device.h"; imp.local_name = "h"; return cbm_pipeline_resolve_import_node(&ctx, "arch/x/bugs.c", "p.arch.x.bugs.c.__file__", &imp, NULL); } TEST(pipeline_header_include_target_is_independent_of_registration_order) { cbm_gbuf_t *a = cbm_gbuf_new("p", "/tmp/cbm_include_order_a"); cbm_gbuf_t *b = cbm_gbuf_new("p", "/tmp/cbm_include_order_b"); ASSERT_NOT_NULL(a); ASSERT_NOT_NULL(b); add_device_header(a, "include/linux/device.h", "p.include.linux.device"); add_device_header(a, "tools/virtio/linux/device.h", "p.tools.virtio.linux.device"); add_device_header(b, "tools/virtio/linux/device.h", "p.tools.virtio.linux.device"); add_device_header(b, "include/linux/device.h", "p.include.linux.device"); const cbm_gbuf_node_t *ta = resolve_device_h_from(a); const cbm_gbuf_node_t *tb = resolve_device_h_from(b); ASSERT_NOT_NULL(ta); ASSERT_NOT_NULL(tb); ASSERT_STR_EQ(ta->qualified_name, "p.include.linux.device.h.__file__"); ASSERT_STR_EQ(tb->qualified_name, "p.include.linux.device.h.__file__"); cbm_gbuf_free(a); cbm_gbuf_free(b); PASS(); } /* #768 end-to-end: `import { A, B } from './lib'` must survive the REAL * pipeline (extract -> gbuf dedup -> raw SQLite dump) as TWO IMPORTS edges * with distinct local_name — and the dumped DB must satisfy its own schema. * With only the graph-buffer half of the fix, both edges reach the dump but * violate an unwidened UNIQUE(source_id,target_id,type), which PRAGMA * integrity_check flags as a non-unique autoindex entry. */ TEST(pipeline_imports_multi_symbol_edges) { const char *files[] = {"consumer.ts", "lib.ts"}; const char *contents[] = { "import { A, B } from './lib';\n\nexport function useBoth() {\n return A() + B();\n}\n", "export function A() {\n return 1;\n}\nexport function B() {\n return 2;\n}\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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); /* The dumped DB must pass SQLite's own full integrity check — this is * what catches a buffer-only fix shipping DBs that violate their own * UNIQUE constraint. */ sqlite3 *raw = NULL; ASSERT_EQ(sqlite3_open(db, &raw), SQLITE_OK); sqlite3_stmt *stmt = NULL; sqlite3_prepare_v2(raw, "PRAGMA integrity_check", -1, &stmt, NULL); ASSERT_EQ(sqlite3_step(stmt), SQLITE_ROW); const char *integrity = (const char *)sqlite3_column_text(stmt, 0); ASSERT_STR_EQ(integrity, "ok"); sqlite3_finalize(stmt); sqlite3_close(raw); /* Both named imports must be queryable as separate IMPORTS edges. */ cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); const char *proj = cbm_pipeline_project_name(p); cbm_edge_t *edges = NULL; int count = 0; ASSERT_EQ(cbm_store_find_edges_by_type(s, proj, "IMPORTS", &edges, &count), CBM_STORE_OK); ASSERT_EQ(count, 2); ASSERT_TRUE(strstr(edges[0].properties_json, "\"local_name\":\"A\"") != NULL || strstr(edges[1].properties_json, "\"local_name\":\"A\"") != NULL); ASSERT_TRUE(strstr(edges[0].properties_json, "\"local_name\":\"B\"") != NULL || strstr(edges[1].properties_json, "\"local_name\":\"B\"") != NULL); cbm_store_free_edges(edges, count); 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"}; const char *contents[] = { "package main\n\nimport \"example.com/myapp/svc\"\n\n" "func run() {\n\tsvc.ProcessOrder(\"123\")\n}\n", "package svc\n\nfunc ProcessOrder(id string) error {\n\treturn nil\n}\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); /* Verify ProcessOrder exists */ cbm_node_t *targets = NULL; int tc = 0; cbm_store_find_nodes_by_name(s, proj, "ProcessOrder", &targets, &tc); ASSERT_GT(tc, 0); /* Verify run() exists */ cbm_node_t *callers = NULL; int clc = 0; cbm_store_find_nodes_by_name(s, proj, "run", &callers, &clc); ASSERT_GT(clc, 0); /* Check CALLS edge from run to ProcessOrder */ cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(s, callers[0].id, "CALLS", &edges, &ec); bool found = false; for (int i = 0; i < ec; i++) { if (edges[i].target_id == targets[0].id) found = true; } ASSERT_TRUE(found); if (edges) cbm_store_free_edges(edges, ec); cbm_store_free_nodes(targets, tc); cbm_store_free_nodes(callers, clc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } /* End-to-end (issue #551 item 1): two SwiftPM packages, Core and App, * indexed under one root. App declares a local path dependency on Core and * a target dependency on Core's product; App.swift does a bare * `import Core`. This only resolves because Core's OWN Package.swift * self-registers "Core" -> Core/Sources/Core in the shared pkgmap, landing * App.swift's IMPORTS edge on that directory's Folder node -- proving real * cross-package resolution. Asserts the EXACT provider node (the * Core/Sources/Core Folder, found by its real QN, not a substring guess) * and the exact edge (from App.swift's own file node to that provider) -- * stronger than repro_issue408.c/repro_issue56.c's own count/substring * checks, which this test intentionally does not settle for. */ TEST(pipeline_swift_cross_package_import) { const char *files[] = { "Core/Package.swift", "Core/Sources/Core/Core.swift", "App/Package.swift", "App/Sources/App/App.swift", }; const char *contents[] = { ("let package = Package(\n" " name: \"Core\",\n" " products: [.library(name: \"Core\", targets: [\"Core\"])],\n" " targets: [.target(name: \"Core\", dependencies: [])]\n" ")\n"), "public func coreValue() -> Int {\n return 1\n}\n", ("let package = Package(\n" " name: \"App\",\n" " dependencies: [.package(path: \"../Core\")],\n" " targets: [.target(name: \"App\", dependencies: [\n" " .product(name: \"Core\", package: \"Core\")\n" " ])]\n" ")\n"), ("import Core\n\n" "func run() -> Int {\n return coreValue()\n}\n"), }; if (setup_lang_repo(files, contents, 4) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); /* The exact provider node: the Folder for Core/Sources/Core, which is * exactly what Core/Package.swift's OWN self-registration resolves to * (see cbm_pkgmap_build: pkg name "Core" -> fqn_module(entry_rel)). * cbm_pipeline_fqn_module and cbm_pipeline_fqn_folder agree on this * path (no extension on any segment to strip), so this is the same QN * the structural pass gave the real Folder node -- not a guess. */ char *provider_qn = cbm_pipeline_fqn_folder(proj, "Core/Sources/Core"); ASSERT_NOT_NULL(provider_qn); cbm_node_t provider = {0}; ASSERT_EQ(cbm_store_find_node_by_qn(s, proj, provider_qn, &provider), CBM_STORE_OK); ASSERT_STR_EQ(provider.label, "Folder"); free(provider_qn); /* The exact importing file node: App/Sources/App/App.swift. */ char *importer_qn = cbm_pipeline_fqn_compute(proj, "App/Sources/App/App.swift", "__file__"); ASSERT_NOT_NULL(importer_qn); cbm_node_t importer = {0}; ASSERT_EQ(cbm_store_find_node_by_qn(s, proj, importer_qn, &importer), CBM_STORE_OK); free(importer_qn); /* The IMPORTS edge must run from THAT importer to THAT provider -- * not merely "some edge lands on a QN containing Core" (a node named * e.g. "AppCore" would have false-passed the old substring check). */ cbm_edge_t *edges = NULL; int ec = 0; ASSERT_EQ(cbm_store_find_edges_by_source_type(s, importer.id, "IMPORTS", &edges, &ec), CBM_STORE_OK); bool found_exact_edge = false; for (int i = 0; i < ec; i++) { if (edges[i].target_id == provider.id) { found_exact_edge = true; } } ASSERT_TRUE(found_exact_edge); if (edges) cbm_store_free_edges(edges, ec); cbm_node_free_fields(&provider); cbm_node_free_fields(&importer); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_python_cross_module_call) { /* Port of TestPythonCrossModuleCallViaImport */ const char *files[] = {"utils.py", "main.py"}; const char *contents[] = { "def fetch_data(url):\n return {\"status\": \"ok\"}\n", "from utils import fetch_data\n\n" "def process():\n result = fetch_data(\"https://example.com\")\n return result\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); cbm_node_t *targets = NULL; int tc = 0; cbm_store_find_nodes_by_name(s, proj, "fetch_data", &targets, &tc); ASSERT_GT(tc, 0); cbm_node_t *callers = NULL; int clc = 0; cbm_store_find_nodes_by_name(s, proj, "process", &callers, &clc); ASSERT_GT(clc, 0); cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(s, callers[0].id, "CALLS", &edges, &ec); bool found = false; for (int i = 0; i < ec; i++) { if (edges[i].target_id == targets[0].id) found = true; } ASSERT_TRUE(found); if (edges) cbm_store_free_edges(edges, ec); cbm_store_free_nodes(targets, tc); cbm_store_free_nodes(callers, clc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } /* #725: two same-named symbols across languages must not share CALLS edges. * Python Store.commit is the real callee of save(); the JS Editor.commit * function is a distinct binding and must have no inbound CALLS from Python. * unique_name (candidates==1) is #1572 and is not this claim. */ TEST(pipeline_cross_language_same_name_does_not_share_calls_issue725) { const char *files[] = {"store.py", "app.py", "web/src/pages/Editor.js"}; const char *contents[] = {"class Store:\n" " def commit(self):\n" " return True\n", "from store import Store\n" "\n" "def save():\n" " return Store().commit()\n", "export function commit() {\n" " return 1;\n" "}\n"}; if (setup_lang_repo(files, contents, 3) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); cbm_node_t *commits = NULL; int ncommit = 0; cbm_store_find_nodes_by_name(s, proj, "commit", &commits, &ncommit); ASSERT_GTE(ncommit, 2); int64_t js_id = 0; int64_t py_id = 0; for (int i = 0; i < ncommit; i++) { if (commits[i].file_path && strstr(commits[i].file_path, "Editor.js")) js_id = commits[i].id; if (commits[i].file_path && strstr(commits[i].file_path, "store.py")) py_id = commits[i].id; } ASSERT_TRUE(js_id != 0); ASSERT_TRUE(py_id != 0); cbm_node_t *saves = NULL; int nsave = 0; cbm_store_find_nodes_by_name(s, proj, "save", &saves, &nsave); ASSERT_GT(nsave, 0); cbm_edge_t *from_save = NULL; int nfrom = 0; cbm_store_find_edges_by_source_type(s, saves[0].id, "CALLS", &from_save, &nfrom); bool save_calls_py = false; bool save_calls_js = false; for (int i = 0; i < nfrom; i++) { if (from_save[i].target_id == py_id) save_calls_py = true; if (from_save[i].target_id == js_id) save_calls_js = true; } ASSERT_TRUE(save_calls_py); ASSERT_FALSE(save_calls_js); cbm_edge_t *into_js = NULL; int njs = 0; cbm_store_find_edges_by_target_type(s, js_id, "CALLS", &into_js, &njs); ASSERT_EQ(njs, 0); if (from_save) cbm_store_free_edges(from_save, nfrom); if (into_js) cbm_store_free_edges(into_js, njs); cbm_store_free_nodes(commits, ncommit); cbm_store_free_nodes(saves, nsave); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_go_type_classification) { /* Port of TestGoTypeClassification */ const char *files[] = {"types.go"}; const char *contents[] = {"package types\n\n" "type Reader interface {\n\tRead(p []byte) (n int, err error)\n}\n\n" "type Writer interface {\n\tWrite(p []byte) (n int, err error)\n}\n\n" "type Config struct {\n\tHost string\n\tPort int\n}\n\n" "type ID = string\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); /* Should have 2 Interface nodes (Reader, Writer) */ cbm_node_t *ifaces = NULL; int ic = 0; cbm_store_find_nodes_by_label(s, proj, "Interface", &ifaces, &ic); ASSERT_EQ(ic, 2); cbm_store_free_nodes(ifaces, ic); /* Should have 1 Struct node (Config struct) */ cbm_node_t *cls = NULL; int cc = 0; cbm_store_find_nodes_by_label(s, proj, "Struct", &cls, &cc); ASSERT_EQ(cc, 1); ASSERT_STR_EQ(cls[0].name, "Config"); cbm_store_free_nodes(cls, cc); /* Should have 1 Type node (ID alias) */ cbm_node_t *types = NULL; int tc = 0; cbm_store_find_nodes_by_label(s, proj, "Type", &types, &tc); ASSERT_EQ(tc, 1); ASSERT_STR_EQ(types[0].name, "ID"); cbm_store_free_nodes(types, tc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_go_grouped_types) { /* Port of TestGoGroupedTypeDeclaration */ const char *files[] = {"models.go"}; const char *contents[] = {"package models\n\n" "type (\n" "\tRequest struct {\n\t\tURL string\n\t}\n\n" "\tResponse struct {\n\t\tStatus int\n\t}\n\n" "\tHandler interface {\n\t\tHandle(req Request) Response\n\t}\n" ")\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); cbm_node_t *cls = NULL; int cc = 0; cbm_store_find_nodes_by_label(s, proj, "Struct", &cls, &cc); ASSERT_EQ(cc, 2); /* Request, Response */ cbm_store_free_nodes(cls, cc); cbm_node_t *ifaces = NULL; int ic = 0; cbm_store_find_nodes_by_label(s, proj, "Interface", &ifaces, &ic); ASSERT_EQ(ic, 1); /* Handler */ cbm_store_free_nodes(ifaces, ic); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_kotlin_project) { /* Port of TestPipelineKotlinProject */ const char *files[] = {"Main.kt", "Service.kt"}; const char *contents[] = { "fun greet(name: String): String {\n return \"Hello, $name\"\n}\n\n" "fun main() {\n val result = greet(\"world\")\n println(result)\n}\n", "class OrderService {\n" " fun processOrder(id: String): Boolean {\n return true\n }\n\n" " fun submitOrder(order: String): Boolean {\n" " return processOrder(order)\n }\n}\n\n" "object Config {\n val API_URL = \"https://example.com/api\"\n}\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); cbm_node_t *funcs = NULL; int fc = 0; cbm_store_find_nodes_by_label(s, proj, "Function", &funcs, &fc); ASSERT_GTE(fc, 2); /* greet, main */ cbm_store_free_nodes(funcs, fc); cbm_node_t *cls = NULL; int cc = 0; cbm_store_find_nodes_by_label(s, proj, "Class", &cls, &cc); ASSERT_GTE(cc, 1); /* OrderService */ cbm_store_free_nodes(cls, cc); cbm_node_t *methods = NULL; int mc = 0; cbm_store_find_nodes_by_label(s, proj, "Method", &methods, &mc); ASSERT_GTE(mc, 2); /* processOrder, submitOrder */ cbm_store_free_nodes(methods, mc); int edge_count = cbm_store_count_edges(s, proj); ASSERT_GT(edge_count, 0); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_lua_anonymous_functions) { /* Port of TestLuaAnonymousFunctionExtraction */ const char *files[] = {"app.lua"}; const char *contents[] = {"local run_before_filter\n" "run_before_filter = function(filter, r)\n" " return filter(r)\n" "end\n\n" "local validate = function(data)\n" " return data ~= nil\n" "end\n\n" "function named_func(x)\n" " return x + 1\n" "end\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); cbm_node_t *funcs = NULL; int fc = 0; cbm_store_find_nodes_by_label(s, proj, "Function", &funcs, &fc); ASSERT_GTE(fc, 3); /* run_before_filter, validate, named_func */ /* Verify specific functions exist */ const char *expected[] = {"run_before_filter", "validate", "named_func"}; for (int e = 0; e < 3; e++) { cbm_node_t *found = NULL; int fnc = 0; cbm_store_find_nodes_by_name(s, proj, expected[e], &found, &fnc); ASSERT_GT(fnc, 0); cbm_store_free_nodes(found, fnc); } cbm_store_free_nodes(funcs, fc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_csharp_modern) { /* Port of TestCSharpModernFeatures */ const char *files[] = {"Controller.cs", "Model.cs"}; const char *contents[] = {"namespace Conduit.Features;\n\n" "public class UsersController {\n" "\tpublic void Get() {}\n" "\tpublic void Create(string name) {}\n" "}\n", "namespace Conduit.Models {\n" "\tclass User {\n" "\t\tpublic string Name { get; set; }\n" "\t\tpublic int GetAge() { return 0; }\n" "\t}\n" "}\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); cbm_node_t *modules = NULL; int modc = 0; cbm_store_find_nodes_by_label(s, proj, "Module", &modules, &modc); ASSERT_GTE(modc, 2); cbm_store_free_nodes(modules, modc); cbm_node_t *cls = NULL; int cc = 0; cbm_store_find_nodes_by_label(s, proj, "Class", &cls, &cc); ASSERT_GTE(cc, 2); /* UsersController, User */ cbm_store_free_nodes(cls, cc); cbm_node_t *methods = NULL; int mc = 0; cbm_store_find_nodes_by_label(s, proj, "Method", &methods, &mc); ASSERT_GTE(mc, 3); /* Get, Create, GetAge */ cbm_store_free_nodes(methods, mc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_bom_stripping) { /* Port of TestBOMStripping — UTF-8 BOM prefix should be handled */ snprintf(g_lang_tmpdir, sizeof(g_lang_tmpdir), "/tmp/cbm_bom_XXXXXX"); if (!cbm_mkdtemp(g_lang_tmpdir)) FAIL("tmpdir"); char path[512]; snprintf(path, sizeof(path), "%s/bom.go", g_lang_tmpdir); FILE *f = fopen(path, "wb"); ASSERT_NOT_NULL(f); /* Write UTF-8 BOM (0xEF 0xBB 0xBF) followed by Go source */ unsigned char bom[] = {0xEF, 0xBB, 0xBF}; fwrite(bom, 1, 3, f); fprintf(f, "package main\n\nfunc BOMFunc() {}\n"); fclose(f); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); cbm_node_t *found = NULL; int fc = 0; cbm_store_find_nodes_by_name(s, proj, "BOMFunc", &found, &fc); ASSERT_GT(fc, 0); /* BOMFunc should be found despite BOM */ cbm_store_free_nodes(found, fc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_form_call_resolution) { /* Port of TestFORMProcedureCallResolution */ const char *files[] = {"calc.frm"}; const char *contents[] = {"#procedure callee(x)\n" " id x = 0;\n" "#endprocedure\n" "#procedure caller()\n" " #call callee(1)\n" "#endprocedure\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); /* Verify CALLS edge exists to callee */ cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_type(s, proj, "CALLS", &edges, &ec); bool found = false; for (int i = 0; i < ec; i++) { cbm_node_t tgt = {0}; if (cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK && strcmp(tgt.name, "callee") == 0) { found = true; } cbm_node_free_fields(&tgt); } ASSERT_TRUE(found); if (edges) cbm_store_free_edges(edges, ec); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_python_type_inference) { /* Port of TestPythonMethodDispatchViaTypeInference * p = DataProcessor() then p.transform() → CALLS DataProcessor.transform */ const char *files[] = {"processor.py", "main.py"}; const char *contents[] = {"class DataProcessor:\n" " def transform(self, data):\n" " return data.upper()\n" "\n" " def validate(self, data):\n" " return len(data) > 0\n", "from processor import DataProcessor\n" "\n" "def run():\n" " p = DataProcessor()\n" " result = p.transform(\"hello\")\n" " return result\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); /* Verify DataProcessor.transform exists as a Method */ cbm_node_t *methods = NULL; int mc = 0; cbm_store_find_nodes_by_name(s, proj, "transform", &methods, &mc); ASSERT_GT(mc, 0); /* Verify run() exists */ cbm_node_t *callers = NULL; int clc = 0; cbm_store_find_nodes_by_name(s, proj, "run", &callers, &clc); ASSERT_GT(clc, 0); /* Check CALLS edge from run() to DataProcessor.transform */ cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(s, callers[0].id, "CALLS", &edges, &ec); bool found = false; for (int i = 0; i < ec; i++) { if (edges[i].target_id == methods[0].id) found = true; } /* Type inference may or may not resolve — log but don't fail hard */ if (!found) { /* At minimum, verify the method exists and run() has some calls */ } if (edges) cbm_store_free_edges(edges, ec); cbm_store_free_nodes(methods, mc); cbm_store_free_nodes(callers, clc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } /* ═══════════════════════════════════════════════════════════════════ * Docstring integration (port of TestDocstringIntegration) * ═══════════════════════════════════════════════════════════════════ */ TEST(pipeline_docstring_go_function) { /* Go function with // comment docstring */ const char *files[] = {"main.go"}; const char *contents[] = {"package main\n\n" "// Compute does something.\n" "func Compute() {}\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); cbm_node_t *nodes = NULL; int nc = 0; cbm_store_find_nodes_by_name(s, proj, "Compute", &nodes, &nc); ASSERT_GT(nc, 0); /* Check properties_json contains docstring */ bool found_docstring = false; for (int i = 0; i < nc; i++) { if (strcmp(nodes[i].label, "Function") == 0 && nodes[i].properties_json && strstr(nodes[i].properties_json, "docstring") && strstr(nodes[i].properties_json, "Compute does something")) { found_docstring = true; } } ASSERT_TRUE(found_docstring); cbm_store_free_nodes(nodes, nc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_docstring_python_function) { /* Python function with triple-quoted docstring */ const char *files[] = {"main.py"}; const char *contents[] = {"def compute():\n" "\t\"\"\"Does something.\"\"\"\n" "\tpass\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); cbm_node_t *nodes = NULL; int nc = 0; cbm_store_find_nodes_by_name(s, proj, "compute", &nodes, &nc); ASSERT_GT(nc, 0); bool found_docstring = false; for (int i = 0; i < nc; i++) { if (strcmp(nodes[i].label, "Function") == 0 && nodes[i].properties_json && strstr(nodes[i].properties_json, "docstring") && strstr(nodes[i].properties_json, "Does something")) { found_docstring = true; } } ASSERT_TRUE(found_docstring); cbm_store_free_nodes(nodes, nc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_docstring_java_method) { /* Java method with Javadoc comment */ const char *files[] = {"A.java"}; const char *contents[] = {"class A {\n" "\t/** Computes result. */\n" "\tvoid compute() {}\n" "}\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); cbm_node_t *nodes = NULL; int nc = 0; cbm_store_find_nodes_by_name(s, proj, "compute", &nodes, &nc); ASSERT_GT(nc, 0); bool found_docstring = false; for (int i = 0; i < nc; i++) { if (strcmp(nodes[i].label, "Method") == 0 && nodes[i].properties_json && strstr(nodes[i].properties_json, "docstring") && strstr(nodes[i].properties_json, "Computes result")) { found_docstring = true; } } ASSERT_TRUE(found_docstring); cbm_store_free_nodes(nodes, nc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_docstring_kotlin_function) { /* Kotlin function with KDoc comment */ const char *files[] = {"main.kt"}; const char *contents[] = {"/** Computes result. */\n" "fun compute() {}\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); cbm_node_t *nodes = NULL; int nc = 0; cbm_store_find_nodes_by_name(s, proj, "compute", &nodes, &nc); ASSERT_GT(nc, 0); bool found_docstring = false; for (int i = 0; i < nc; i++) { if (strcmp(nodes[i].label, "Function") == 0 && nodes[i].properties_json && strstr(nodes[i].properties_json, "docstring") && strstr(nodes[i].properties_json, "Computes result")) { found_docstring = true; } } ASSERT_TRUE(found_docstring); cbm_store_free_nodes(nodes, nc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(pipeline_docstring_go_class) { /* Go struct with // comment docstring */ const char *files[] = {"main.go"}; const char *contents[] = {"package main\n\n" "// MyStruct is documented.\n" "type MyStruct struct{}\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); cbm_node_t *nodes = NULL; int nc = 0; cbm_store_find_nodes_by_name(s, proj, "MyStruct", &nodes, &nc); ASSERT_GT(nc, 0); bool found_docstring = false; for (int i = 0; i < nc; i++) { if (strcmp(nodes[i].label, "Struct") == 0 && nodes[i].properties_json && strstr(nodes[i].properties_json, "docstring") && strstr(nodes[i].properties_json, "MyStruct is documented")) { found_docstring = true; } } ASSERT_TRUE(found_docstring); cbm_store_free_nodes(nodes, nc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } /* Index one file; true when the node with this label and name stores `want` in * its properties JSON. */ static bool doc_props_contain(const char *file, const char *content, const char *label, const char *name, const char *want) { const char *files[] = {file}; const char *contents[] = {content}; if (setup_lang_repo(files, contents, 1) != 0) { return false; } char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL); bool found = false; if (p && cbm_pipeline_run(p) == 0) { cbm_store_t *s = cbm_store_open_path(db); cbm_node_t *nodes = NULL; int nc = 0; if (s && cbm_store_find_nodes_by_label(s, cbm_pipeline_project_name(p), label, &nodes, &nc) == 0) { for (int i = 0; i < nc; i++) { if (nodes[i].name && strcmp(nodes[i].name, name) == 0 && nodes[i].properties_json && strstr(nodes[i].properties_json, want)) { found = true; } } cbm_store_free_nodes(nodes, nc); } if (s) { cbm_store_close(s); } } if (p) { cbm_pipeline_free(p); } teardown_lang_repo(); return found; } TEST(pipeline_doc_go_package_comment_on_file_node) { /* go/doc: the comment group touching `package` documents the package; it is * stored on the File node. */ ASSERT_TRUE(doc_props_contain("knob.go", "// Package knob turns knobs.\n" "package knob\n\n" "func Turn() {}\n", "File", "knob.go", "\"docstring\":\"// Package knob turns knobs.\"")); PASS(); } TEST(pipeline_doc_rust_inner_doc_on_file_node) { ASSERT_TRUE(doc_props_contain("lib.rs", "//! Knob crate.\n" "//! Turns knobs.\n" "\n" "pub fn turn() {}\n", "File", "lib.rs", "\"docstring\":\"//! Knob crate.\\n//! Turns knobs.\"")); PASS(); } TEST(pipeline_doc_long_docstring_stored_whole) { /* A 30-line doc (longer than the old fixed 2 KB properties buffer on its * own) is stored whole, and the fields after it are not dropped. */ char src[4096]; int s = snprintf(src, sizeof(src), "package knob\n\n"); for (int i = 0; i < 30; i++) { s += snprintf(src + s, sizeof(src) - (size_t)s, "// Line %02d of a long doc comment that keeps on going.\n", i); } snprintf(src + s, sizeof(src) - (size_t)s, "func Long(steps int) int { return steps }\n"); ASSERT_TRUE(doc_props_contain("knob.go", src, "Function", "Long", "that keeps on going.\\n// Line 29 of a long doc")); ASSERT_TRUE(doc_props_contain("knob.go", src, "Function", "Long", "\"signature\":")); PASS(); } TEST(project_name_from_path) { /* Port of TestProjectNameFromPath — more cases than integ_pipeline_project_name */ struct { const char *path; const char *want; } cases[] = { {"/tmp/bench/erlang/lib/stdlib/src", "tmp-bench-erlang-lib-stdlib-src"}, {"/Users/martin/projects/myapp", "Users-martin-projects-myapp"}, {"/home/user/repo", "home-user-repo"}, {"/single", "single"}, }; for (int i = 0; i < 4; i++) { char *got = cbm_project_name_from_path(cases[i].path); ASSERT_NOT_NULL(got); ASSERT_STR_EQ(got, cases[i].want); free(got); } PASS(); } /* Regression for #394/#227/#367: a Windows drive letter is case-insensitive, so * "c:/repo" and "C:/repo" must canonicalize to the SAME project key. Otherwise * agent CWDs (which report lowercase "c:\...") produce a distinct key + colliding * cache file that clobbers the good index, and the lowercase index self-deletes. * Pure string logic, so it reproduces on any platform. */ TEST(project_name_drive_letter_case_insensitive_issue394) { char *lower = cbm_project_name_from_path("c:/WEBDEV/Cardio-Cloud"); char *upper = cbm_project_name_from_path("C:/WEBDEV/Cardio-Cloud"); ASSERT_NOT_NULL(lower); ASSERT_NOT_NULL(upper); /* Both must fold to the upper-case-drive key, e.g. "C-WEBDEV-Cardio-Cloud". */ ASSERT_STR_EQ(lower, upper); ASSERT_EQ(lower[0], 'C'); free(lower); free(upper); /* And the normalizer itself upper-cases the drive root in place. */ char buf1[32]; snprintf(buf1, sizeof(buf1), "%s", "c:/x"); cbm_normalize_path_sep(buf1); ASSERT_STR_EQ(buf1, "C:/x"); char buf2[32]; snprintf(buf2, sizeof(buf2), "%s", "d:\\proj\\sub"); cbm_normalize_path_sep(buf2); ASSERT_STR_EQ(buf2, "D:/proj/sub"); PASS(); } static const char *test_null_dev(void) { #ifdef _WIN32 return "NUL"; #else return "/dev/null"; #endif } static bool git_available(void) { char cmd[128]; snprintf(cmd, sizeof(cmd), "git --version >%s 2>&1", test_null_dev()); int rc = system(cmd); return rc == 0; } static int run_cmd(const char *cmd) { return system(cmd); } TEST(git_context_non_git_path) { char *tmp = th_mktempdir("cbm_gitctx_nongit"); ASSERT_NOT_NULL(tmp); cbm_git_context_t ctx = {0}; ASSERT_EQ(cbm_git_context_resolve(tmp, &ctx), 0); ASSERT_FALSE(ctx.is_git); ASSERT_TRUE(ctx.root_exists); char *qn = cbm_git_context_branch_qn("proj", &ctx); ASSERT_NOT_NULL(qn); ASSERT_STR_EQ(qn, "proj.__branch__.working-tree"); free(qn); char json[1024]; ASSERT_GT(cbm_git_context_props_json(&ctx, json, sizeof(json)), 0); ASSERT_NOT_NULL(strstr(json, "\"is_git\":false")); ASSERT_NOT_NULL(strstr(json, "\"root_exists\":true")); char long_value[1200]; memset(long_value, 'a', sizeof(long_value) - 1); long_value[sizeof(long_value) - 1] = '\0'; cbm_git_context_t long_ctx = { .root_exists = true, .canonical_root = long_value, }; char small_json[64]; ASSERT_EQ(cbm_git_context_props_json(&long_ctx, small_json, sizeof(small_json)), 0); cbm_git_context_free(&ctx); th_rmtree(tmp); PASS(); } TEST(git_context_linked_worktree) { if (!git_available()) { FAIL("git unavailable"); } char *tmp = th_mktempdir("cbm_gitctx_repo"); ASSERT_NOT_NULL(tmp); char repo[512], wt[512], cmd[2048]; int n = snprintf(repo, sizeof(repo), "%s/repo with space", tmp); ASSERT_TRUE(n > 0 && n < (int)sizeof(repo)); n = snprintf(wt, sizeof(wt), "%s/wt with space", tmp); ASSERT_TRUE(n > 0 && n < (int)sizeof(wt)); ASSERT_EQ(th_mkdir_p(repo), 0); const char *null_dev = test_null_dev(); snprintf(cmd, sizeof(cmd), "git -C \"%s\" init >%s 2>&1", repo, null_dev); ASSERT_EQ(run_cmd(cmd), 0); snprintf(cmd, sizeof(cmd), "git -C \"%s\" checkout -b main >%s 2>&1", repo, null_dev); ASSERT_EQ(run_cmd(cmd), 0); ASSERT_EQ(th_write_file(TH_PATH(repo, "file.txt"), "hello\n"), 0); snprintf(cmd, sizeof(cmd), "git -C \"%s\" add file.txt >%s 2>&1", repo, null_dev); ASSERT_EQ(run_cmd(cmd), 0); snprintf(cmd, sizeof(cmd), "git -C \"%s\" -c user.name=\"CBM Test\" -c user.email=\"cbm@example.invalid\" " "commit -m \"initial\" >%s 2>&1", repo, null_dev); ASSERT_EQ(run_cmd(cmd), 0); snprintf(cmd, sizeof(cmd), "git -C \"%s\" worktree add -b feature/git-context \"%s\" >%s 2>&1", repo, wt, null_dev); ASSERT_EQ(run_cmd(cmd), 0); cbm_git_context_t main_ctx = {0}; cbm_git_context_t wt_ctx = {0}; ASSERT_EQ(cbm_git_context_resolve(repo, &main_ctx), 0); ASSERT_EQ(cbm_git_context_resolve(wt, &wt_ctx), 0); ASSERT_TRUE(main_ctx.is_git); ASSERT_FALSE(main_ctx.is_worktree); ASSERT_TRUE(wt_ctx.is_git); ASSERT_TRUE(wt_ctx.is_worktree); ASSERT_STR_EQ(main_ctx.canonical_root, wt_ctx.canonical_root); ASSERT_NOT_NULL(wt_ctx.branch); ASSERT_STR_EQ(wt_ctx.branch, "feature/git-context"); ASSERT_STR_EQ(wt_ctx.branch_slug, "feature-git-context"); ASSERT_NOT_NULL(wt_ctx.head_sha); char *qn = cbm_git_context_branch_qn("proj", &wt_ctx); ASSERT_NOT_NULL(qn); ASSERT_STR_EQ(qn, "proj.__branch__.feature-git-context"); free(qn); char json[2048]; ASSERT_GT(cbm_git_context_props_json(&wt_ctx, json, sizeof(json)), 0); ASSERT_NOT_NULL(strstr(json, "\"is_git\":true")); ASSERT_NOT_NULL(strstr(json, "\"is_worktree\":true")); ASSERT_NOT_NULL(strstr(json, "\"branch\":\"feature/git-context\"")); cbm_git_context_free(&main_ctx); cbm_git_context_free(&wt_ctx); snprintf(cmd, sizeof(cmd), "git -C \"%s\" checkout --detach HEAD >%s 2>&1", repo, null_dev); ASSERT_EQ(run_cmd(cmd), 0); cbm_git_context_t detached_ctx = {0}; ASSERT_EQ(cbm_git_context_resolve(repo, &detached_ctx), 0); ASSERT_TRUE(detached_ctx.is_detached); ASSERT_STR_EQ(detached_ctx.branch_slug, "detached"); cbm_git_context_free(&detached_ctx); th_rmtree(tmp); PASS(); } #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API static int branch_head_match_count(cbm_store_t *store, const char *project, const char *head_sha) { cbm_node_t *branches = NULL; int branch_count = 0; if (!store || !project || !head_sha || cbm_store_find_nodes_by_label(store, project, "Branch", &branches, &branch_count) != CBM_STORE_OK) { return -1; } int matches = 0; for (int i = 0; i < branch_count; i++) { if (branches[i].properties_json && strstr(branches[i].properties_json, head_sha)) { matches++; } } cbm_store_free_nodes(branches, branch_count); return matches; } /* Regression for #1213: index A, re-index B after a tracked source change, * then re-index C after an empty commit. Each successful run publishes its * run-start HEAD. Construct the final pipeline before moving HEAD to also * cover refreshing the snapshot captured by cbm_pipeline_new(). */ TEST(pipeline_git_context_change_forces_full_and_refreshes_branch) { if (!git_available()) { FAIL("git unavailable"); } char tmp[256]; char *created = th_mktempdir("cbm_manifest_git_context"); ASSERT_NOT_NULL(created); snprintf(tmp, sizeof(tmp), "%s", created); char repo[512], db_path[512], cmd[2048]; snprintf(repo, sizeof(repo), "%s/repo", tmp); snprintf(db_path, sizeof(db_path), "%s/context.db", tmp); ASSERT_EQ(th_mkdir_p(repo), 0); ASSERT_EQ(th_write_file(TH_PATH(repo, "stable.py"), "def StableGitContext():\n return 1\n"), 0); const char *null_dev = test_null_dev(); snprintf(cmd, sizeof(cmd), "git -C \"%s\" init >%s 2>&1", repo, null_dev); ASSERT_EQ(run_cmd(cmd), 0); snprintf(cmd, sizeof(cmd), "git -C \"%s\" add stable.py >%s 2>&1", repo, null_dev); ASSERT_EQ(run_cmd(cmd), 0); snprintf(cmd, sizeof(cmd), "git -C \"%s\" -c user.name=\"CBM Test\" " "-c user.email=\"cbm@example.invalid\" commit -m \"initial\" >%s 2>&1", repo, null_dev); ASSERT_EQ(run_cmd(cmd), 0); cbm_git_context_t initial_ctx = {0}; ASSERT_EQ(cbm_git_context_resolve(repo, &initial_ctx), 0); ASSERT_NOT_NULL(initial_ctx.head_sha); char initial_head[128]; snprintf(initial_head, sizeof(initial_head), "%s", initial_ctx.head_sha); cbm_git_context_free(&initial_ctx); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *baseline = cbm_pipeline_new(repo, db_path, CBM_MODE_FAST); ASSERT_NOT_NULL(baseline); ASSERT_EQ(cbm_pipeline_run(baseline), 0); char project[256]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); cbm_store_t *initial_store = cbm_store_open_path(db_path); ASSERT_NOT_NULL(initial_store); int initial_matches = branch_head_match_count(initial_store, project, initial_head); cbm_store_close(initial_store); ASSERT_EQ(initial_matches, 1); /* B: a committed source change must advance the existing project's HEAD. */ ASSERT_EQ(th_write_file(TH_PATH(repo, "stable.py"), "def StableGitContext():\n return 2\n"), 0); snprintf(cmd, sizeof(cmd), "git -C \"%s\" -c user.name=\"CBM Test\" " "-c user.email=\"cbm@example.invalid\" commit -am \"source change\" >%s 2>&1", repo, null_dev); ASSERT_EQ(run_cmd(cmd), 0); cbm_git_context_t source_ctx = {0}; ASSERT_EQ(cbm_git_context_resolve(repo, &source_ctx), 0); ASSERT_NOT_NULL(source_ctx.head_sha); char source_head[128]; snprintf(source_head, sizeof(source_head), "%s", source_ctx.head_sha); cbm_git_context_free(&source_ctx); ASSERT_TRUE(strcmp(initial_head, source_head) != 0); cbm_pipeline_t *after_source_change = cbm_pipeline_new(repo, db_path, CBM_MODE_FAST); ASSERT_NOT_NULL(after_source_change); int source_rc = cbm_pipeline_run(after_source_change); cbm_pipeline_free(after_source_change); ASSERT_EQ(source_rc, 0); cbm_store_t *source_store = cbm_store_open_path(db_path); ASSERT_NOT_NULL(source_store); int source_matches = branch_head_match_count(source_store, project, source_head); int initial_stale_matches = branch_head_match_count(source_store, project, initial_head); cbm_store_close(source_store); ASSERT_EQ(source_matches, 1); ASSERT_EQ(initial_stale_matches, 0); /* C: no file hashes change, but the empty commit must still advance HEAD. */ cbm_pipeline_t *after_head_move = cbm_pipeline_new(repo, db_path, CBM_MODE_FAST); ASSERT_NOT_NULL(after_head_move); snprintf(cmd, sizeof(cmd), "git -C \"%s\" -c user.name=\"CBM Test\" " "-c user.email=\"cbm@example.invalid\" commit --allow-empty " "-m \"head-only change\" >%s 2>&1", repo, null_dev); ASSERT_EQ(run_cmd(cmd), 0); cbm_git_context_t changed_ctx = {0}; ASSERT_EQ(cbm_git_context_resolve(repo, &changed_ctx), 0); ASSERT_NOT_NULL(changed_ctx.head_sha); char changed_head[128]; snprintf(changed_head, sizeof(changed_head), "%s", changed_ctx.head_sha); cbm_git_context_free(&changed_ctx); ASSERT_TRUE(strcmp(source_head, changed_head) != 0); cbm_pipeline_incremental_test_reset_faults(); int changed_rc = cbm_pipeline_run(after_head_move); cbm_incremental_route_t changed_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(after_head_move); cbm_store_t *store = cbm_store_open_path(db_path); int changed_branch_matches = store ? branch_head_match_count(store, project, changed_head) : -1; int stale_branch_matches = store ? branch_head_match_count(store, project, source_head) : -1; cbm_file_hash_t git_input = {0}; int git_input_rc = store ? cbm_store_get_file_hash( store, project, ".codebase-memory/.semantic-input/git-context-v1", &git_input) : CBM_STORE_NOT_FOUND; cbm_store_clear_file_hash(&git_input); if (store) { cbm_store_close(store); } cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *unchanged = cbm_pipeline_new(repo, db_path, CBM_MODE_FAST); ASSERT_NOT_NULL(unchanged); int unchanged_rc = cbm_pipeline_run(unchanged); cbm_incremental_route_t unchanged_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(unchanged); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(changed_rc, 0); ASSERT_EQ(changed_route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(changed_branch_matches, 1); ASSERT_EQ(stale_branch_matches, 0); ASSERT_EQ(git_input_rc, CBM_STORE_OK); ASSERT_EQ(unchanged_rc, 0); ASSERT_EQ(unchanged_route, CBM_INCREMENTAL_ROUTE_NOOP); PASS(); } /* The global extension config lives outside the repository but changes both * discovery and parsing. Its exact consumed bytes therefore belong to the * same semantic generation as the graph they selected. */ TEST(pipeline_global_extension_config_change_forces_full) { char tmp[256]; char *created = th_mktempdir("cbm_manifest_global_config"); ASSERT_NOT_NULL(created); snprintf(tmp, sizeof(tmp), "%s", created); char repo[512], config_root[512], app_dir[768], config_path[1024], db_path[512]; snprintf(repo, sizeof(repo), "%s/repo", tmp); snprintf(config_root, sizeof(config_root), "%s/config-root", tmp); snprintf(app_dir, sizeof(app_dir), "%s/codebase-memory-mcp", config_root); snprintf(config_path, sizeof(config_path), "%s/config.json", app_dir); snprintf(db_path, sizeof(db_path), "%s/config.db", tmp); ASSERT_EQ(th_mkdir_p(repo), 0); ASSERT_EQ(th_mkdir_p(app_dir), 0); ASSERT_EQ(th_write_file(TH_PATH(repo, "fixture.cbmfixture"), "def GlobalExtensionTarget():\n return 1\n"), 0); ASSERT_EQ(th_write_file(config_path, "{\"extra_extensions\":{\".cbmfixture\":\"python\"}}\n"), 0); #ifdef _WIN32 const char *env_name = "APPDATA"; #else const char *env_name = "XDG_CONFIG_HOME"; #endif char old_env[CBM_SZ_1K] = {0}; bool had_old_env = cbm_safe_getenv(env_name, old_env, sizeof(old_env), NULL) != NULL; int setenv_rc = cbm_setenv(env_name, config_root, 1); int baseline_rc = -1; int initial_nodes = -1; int changed_rc = -1; cbm_incremental_route_t changed_route = CBM_INCREMENTAL_ROUTE_NONE; int changed_nodes = -1; int config_input_rc = CBM_STORE_NOT_FOUND; int unchanged_rc = -1; cbm_incremental_route_t unchanged_route = CBM_INCREMENTAL_ROUTE_NONE; char project[256] = {0}; if (setenv_rc == 0) { cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *baseline = cbm_pipeline_new(repo, db_path, CBM_MODE_FAST); if (baseline) { baseline_rc = cbm_pipeline_run(baseline); snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(baseline)); cbm_pipeline_free(baseline); } cbm_store_t *baseline_store = cbm_store_open_path(db_path); if (baseline_store) { initial_nodes = named_node_count(baseline_store, project, "GlobalExtensionTarget"); cbm_store_close(baseline_store); } if (th_write_file(config_path, "{\"extra_extensions\":{\".cbmfixture\":\"json\"}}\n") == 0) { cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *changed = cbm_pipeline_new(repo, db_path, CBM_MODE_FAST); if (changed) { changed_rc = cbm_pipeline_run(changed); changed_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(changed); } cbm_store_t *changed_store = cbm_store_open_path(db_path); if (changed_store) { changed_nodes = named_node_count(changed_store, project, "GlobalExtensionTarget"); cbm_file_hash_t config_input = {0}; config_input_rc = cbm_store_get_file_hash( changed_store, project, ".codebase-memory/.semantic-input/global-extension-config-v1", &config_input); cbm_store_clear_file_hash(&config_input); cbm_store_close(changed_store); } cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *unchanged = cbm_pipeline_new(repo, db_path, CBM_MODE_FAST); if (unchanged) { unchanged_rc = cbm_pipeline_run(unchanged); unchanged_route = cbm_pipeline_incremental_test_last_route(); cbm_pipeline_free(unchanged); } } } if (had_old_env) { cbm_setenv(env_name, old_env, 1); } else { cbm_unsetenv(env_name); } cbm_set_user_lang_config(NULL); cbm_pipeline_incremental_test_reset_faults(); th_rmtree(tmp); ASSERT_EQ(setenv_rc, 0); ASSERT_EQ(baseline_rc, 0); ASSERT_EQ(initial_nodes, 1); ASSERT_EQ(changed_rc, 0); ASSERT_EQ(changed_route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_EQ(changed_nodes, 0); ASSERT_EQ(config_input_rc, CBM_STORE_OK); ASSERT_EQ(unchanged_rc, 0); ASSERT_EQ(unchanged_route, CBM_INCREMENTAL_ROUTE_NOOP); PASS(); } #endif TEST(project_name_uniqueness) { /* Port of TestProjectNameUniqueness */ char *a = cbm_project_name_from_path("/tmp/bench/zig/lib/std"); char *b = cbm_project_name_from_path("/tmp/bench/erlang/lib/stdlib/src"); ASSERT_NOT_NULL(a); ASSERT_NOT_NULL(b); ASSERT_TRUE(strcmp(a, b) != 0); free(a); free(b); PASS(); } /* ── Git diff helpers (pass_gitdiff.c) ─────────────────────────── */ TEST(gitdiff_parse_range_with_count) { int start, count; cbm_parse_range("10,5", &start, &count); ASSERT_EQ(start, 10); ASSERT_EQ(count, 5); PASS(); } TEST(gitdiff_parse_range_no_count) { int start, count; cbm_parse_range("10", &start, &count); ASSERT_EQ(start, 10); ASSERT_EQ(count, 1); PASS(); } TEST(gitdiff_parse_range_zero_count) { int start, count; cbm_parse_range("1,0", &start, &count); ASSERT_EQ(start, 1); ASSERT_EQ(count, 0); PASS(); } TEST(gitdiff_parse_range_large) { int start, count; cbm_parse_range("52,2", &start, &count); ASSERT_EQ(start, 52); ASSERT_EQ(count, 2); PASS(); } TEST(gitdiff_parse_name_status) { const char *input = "M\tinternal/store/nodes.go\n" "A\tnew_file.go\n" "D\told_file.go\n" "R100\tsrc/old.go\tsrc/new.go\n"; cbm_changed_file_t files[16]; int n = cbm_parse_name_status(input, files, 16); ASSERT_EQ(n, 4); ASSERT_STR_EQ(files[0].status, "M"); ASSERT_STR_EQ(files[0].path, "internal/store/nodes.go"); ASSERT_STR_EQ(files[1].status, "A"); ASSERT_STR_EQ(files[1].path, "new_file.go"); ASSERT_STR_EQ(files[2].status, "D"); ASSERT_STR_EQ(files[2].path, "old_file.go"); ASSERT_STR_EQ(files[3].status, "R"); ASSERT_STR_EQ(files[3].path, "src/new.go"); ASSERT_STR_EQ(files[3].old_path, "src/old.go"); PASS(); } TEST(gitdiff_parse_name_status_filters_untrackable) { const char *input = "M\tpackage-lock.json\n" "M\tsrc/main.go\n" "M\tvendor/lib.go\n"; cbm_changed_file_t files[16]; int n = cbm_parse_name_status(input, files, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(files[0].path, "src/main.go"); PASS(); } TEST(gitdiff_parse_hunks) { const char *input = "diff --git a/main.go b/main.go\n" "index abc1234..def5678 100644\n" "--- a/main.go\n" "+++ b/main.go\n" "@@ -10,3 +10,5 @@ func main() {\n" "+\tnewLine1()\n" "+\tnewLine2()\n" "@@ -50,0 +52,2 @@ func helper() {\n" "+\tanother()\n" "+\tline()\n" "diff --git a/binary.png b/binary.png\n" "Binary files a/binary.png and b/binary.png differ\n" "diff --git a/utils.go b/utils.go\n" "--- a/utils.go\n" "+++ b/utils.go\n" "@@ -1 +1 @@ package utils\n" "-old\n" "+new\n"; cbm_changed_hunk_t hunks[16]; int n = cbm_parse_hunks(input, hunks, 16); ASSERT_EQ(n, 3); ASSERT_STR_EQ(hunks[0].path, "main.go"); ASSERT_EQ(hunks[0].start_line, 10); ASSERT_EQ(hunks[0].end_line, 14); ASSERT_STR_EQ(hunks[1].path, "main.go"); ASSERT_EQ(hunks[1].start_line, 52); ASSERT_EQ(hunks[1].end_line, 53); ASSERT_STR_EQ(hunks[2].path, "utils.go"); ASSERT_EQ(hunks[2].start_line, 1); ASSERT_EQ(hunks[2].end_line, 1); PASS(); } TEST(gitdiff_parse_hunks_no_newline_marker) { const char *input = "diff --git a/file.go b/file.go\n" "--- a/file.go\n" "+++ b/file.go\n" "@@ -5,2 +5,3 @@ func foo() {\n" "+\tbar()\n" "\\ No newline at end of file\n"; cbm_changed_hunk_t hunks[4]; int n = cbm_parse_hunks(input, hunks, 4); ASSERT_EQ(n, 1); ASSERT_EQ(hunks[0].start_line, 5); ASSERT_EQ(hunks[0].end_line, 7); PASS(); } TEST(gitdiff_parse_hunks_mode_change) { const char *input = "diff --git a/script.sh b/script.sh\n" "old mode 100644\n" "new mode 100755\n"; cbm_changed_hunk_t hunks[4]; int n = cbm_parse_hunks(input, hunks, 4); ASSERT_EQ(n, 0); PASS(); } TEST(gitdiff_parse_hunks_deletion) { const char *input = "diff --git a/file.go b/file.go\n" "--- a/file.go\n" "+++ b/file.go\n" "@@ -10,3 +10,0 @@ func foo() {\n"; cbm_changed_hunk_t hunks[4]; int n = cbm_parse_hunks(input, hunks, 4); ASSERT_EQ(n, 1); ASSERT_EQ(hunks[0].start_line, 10); PASS(); } /* ── Config helpers (pass_configures.c) ───────────────────────── */ TEST(configures_is_env_var_name) { ASSERT(cbm_is_env_var_name("DATABASE_URL")); ASSERT(cbm_is_env_var_name("API_KEY")); ASSERT(cbm_is_env_var_name("PORT")); ASSERT(!cbm_is_env_var_name("A")); /* too short */ ASSERT(!cbm_is_env_var_name("port")); /* lowercase */ ASSERT(!cbm_is_env_var_name("apiKey")); /* camelCase */ ASSERT(cbm_is_env_var_name("DB_2")); /* with digit */ ASSERT(!cbm_is_env_var_name("__")); /* no uppercase */ ASSERT(!cbm_is_env_var_name("")); /* empty */ PASS(); } TEST(configures_normalize_config_key) { char norm[256]; int tokens; tokens = cbm_normalize_config_key("max_connections", norm, sizeof(norm)); ASSERT_STR_EQ(norm, "max_connections"); ASSERT_EQ(tokens, 2); tokens = cbm_normalize_config_key("maxConnections", norm, sizeof(norm)); ASSERT_STR_EQ(norm, "max_connections"); ASSERT_EQ(tokens, 2); tokens = cbm_normalize_config_key("DATABASE_HOST", norm, sizeof(norm)); ASSERT_STR_EQ(norm, "database_host"); ASSERT_EQ(tokens, 2); tokens = cbm_normalize_config_key("database.host", norm, sizeof(norm)); ASSERT_STR_EQ(norm, "database_host"); ASSERT_EQ(tokens, 2); tokens = cbm_normalize_config_key("port", norm, sizeof(norm)); ASSERT_STR_EQ(norm, "port"); ASSERT_EQ(tokens, 1); tokens = cbm_normalize_config_key("maxRetryCount", norm, sizeof(norm)); ASSERT_STR_EQ(norm, "max_retry_count"); ASSERT_EQ(tokens, 3); PASS(); } TEST(configures_has_config_extension) { ASSERT(cbm_has_config_extension("config.toml")); ASSERT(cbm_has_config_extension("settings.yaml")); ASSERT(cbm_has_config_extension("config.yml")); ASSERT(cbm_has_config_extension(".env")); ASSERT(cbm_has_config_extension("config.ini")); ASSERT(cbm_has_config_extension("data.json")); ASSERT(cbm_has_config_extension("pom.xml")); ASSERT(!cbm_has_config_extension("main.go")); ASSERT(!cbm_has_config_extension("app.py")); ASSERT(!cbm_has_config_extension("data.csv")); PASS(); } /* ── Config integration tests (configures_test.go ports) ──────── */ TEST(configures_env_var_in_config) { /* Port of TestBuildEnvIndex_ConfigVariableAdded: * config.toml has DATABASE_URL, main.go does os.Getenv("DATABASE_URL") * → CONFIGURES edges should link them. */ const char *files[] = {"config.toml", "main.go"}; const char *contents[] = {"DATABASE_URL = \"postgresql://localhost/db\"\n", "package main\n\n" "import \"os\"\n\n" "func main() {\n" "\turl := os.Getenv(\"DATABASE_URL\")\n" "\t_ = url\n" "}\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); /* Verify CONFIGURES edges were created */ cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_type(s, proj, "CONFIGURES", &edges, &ec); /* At minimum the pipeline should not crash. Edge count depends on * extraction matching env var accesses to config variables. */ if (edges) cbm_store_free_edges(edges, ec); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(configures_lowercase_key_skipped) { /* Port of TestBuildEnvIndex_LowercaseKeySkipped: * config.toml has lowercase key — should NOT produce env var CONFIGURES edges. */ const char *files[] = {"config.toml", "main.go"}; const char *contents[] = {"database_host = \"localhost\"\n", "package main\n\nfunc main() {}\n"}; if (setup_lang_repo(files, contents, 2) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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); /* Pipeline ran successfully — no crash from lowercase config keys */ cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(configures_non_config_file_skipped) { /* Port of TestBuildEnvIndex_NonConfigFileSkipped: * Only Go file, no config file — no config-derived CONFIGURES edges. */ const char *files[] = {"main.go"}; const char *contents[] = {"package main\n\n" "var API_URL = \"https://api.example.com\"\n\n" "func main() {}\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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); /* No config file → buildEnvIndex should not create config-derived entries */ cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(configures_full_pipeline_integration) { /* Port of TestConfigIntegration_FullPipeline: * TOML + INI + JSON config files + Go code → Class & Variable nodes from * config files, plus CONFIGURES edges. */ const char *files[] = {"config.toml", "settings.ini", "config.json", "main.go"}; const char *contents[] = {"[database]\n" "host = \"localhost\"\n" "port = 5432\n" "max_connections = 100\n\n" "[server]\n" "bind_address = \"0.0.0.0\"\n", "[database]\n" "host = localhost\n" "port = 5432\n", "{\"appName\": \"test\", \"maxRetries\": 3}", "package main\n\n" "import \"os\"\n\n" "func getMaxConnections() int { return 100 }\n\n" "func loadConfig() {\n" "\tcfg := readFile(\"config.toml\")\n" "\t_ = cfg\n" "\tdbURL := os.Getenv(\"DATABASE_URL\")\n" "\t_ = dbURL\n" "}\n\n" "func readFile(path string) string { return \"\" }\n"}; if (setup_lang_repo(files, contents, 4) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); /* Should have Class nodes (database, server sections from TOML) */ cbm_node_t *classes = NULL; int cc = 0; cbm_store_find_nodes_by_label(s, proj, "Class", &classes, &cc); ASSERT_GT(cc, 0); if (classes) cbm_store_free_nodes(classes, cc); /* Should have Variable nodes from config files */ cbm_node_t *vars = NULL; int vc = 0; cbm_store_find_nodes_by_label(s, proj, "Variable", &vars, &vc); ASSERT_GT(vc, 0); if (vars) cbm_store_free_nodes(vars, vc); /* Should have Function nodes from Go code */ cbm_node_t *funcs = NULL; int fc = 0; cbm_store_find_nodes_by_label(s, proj, "Function", &funcs, &fc); ASSERT_GT(fc, 0); if (funcs) cbm_store_free_nodes(funcs, fc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(enrichment_split_camel_case) { char *parts[8]; int n; n = cbm_split_camel_case("GetMapping", parts, 8); ASSERT_EQ(n, 2); ASSERT_STR_EQ(parts[0], "Get"); ASSERT_STR_EQ(parts[1], "Mapping"); for (int i = 0; i < n; i++) free(parts[i]); n = cbm_split_camel_case("getMessage", parts, 8); ASSERT_EQ(n, 2); ASSERT_STR_EQ(parts[0], "get"); ASSERT_STR_EQ(parts[1], "Message"); for (int i = 0; i < n; i++) free(parts[i]); n = cbm_split_camel_case("cache", parts, 8); ASSERT_EQ(n, 1); ASSERT_STR_EQ(parts[0], "cache"); for (int i = 0; i < n; i++) free(parts[i]); n = cbm_split_camel_case("HTMLParser", parts, 8); ASSERT_EQ(n, 1); ASSERT_STR_EQ(parts[0], "HTMLParser"); for (int i = 0; i < n; i++) free(parts[i]); n = cbm_split_camel_case("", parts, 8); ASSERT_EQ(n, 0); PASS(); } TEST(enrichment_tokenize_decorator) { char *tokens[16]; int n; n = cbm_tokenize_decorator("@Override", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "override"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@Deprecated", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "deprecated"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@Test", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "test"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@login_required", tokens, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(tokens[0], "login"); ASSERT_STR_EQ(tokens[1], "required"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@cache", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "cache"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@pytest.fixture", tokens, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(tokens[0], "pytest"); ASSERT_STR_EQ(tokens[1], "fixture"); for (int i = 0; i < n; i++) free(tokens[i]); /* "get" is stopword → only "mapping" */ n = cbm_tokenize_decorator("@GetMapping(\"/api\")", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "mapping"); for (int i = 0; i < n; i++) free(tokens[i]); /* "post" passes, "mapping" passes */ n = cbm_tokenize_decorator("@PostMapping(\"/api\")", tokens, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(tokens[0], "post"); ASSERT_STR_EQ(tokens[1], "mapping"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@Transactional", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "transactional"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@MessageMapping(\"/chat\")", tokens, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(tokens[0], "message"); ASSERT_STR_EQ(tokens[1], "mapping"); for (int i = 0; i < n; i++) free(tokens[i]); /* Rust-style #[test] */ n = cbm_tokenize_decorator("#[test]", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "test"); for (int i = 0; i < n; i++) free(tokens[i]); /* #[derive(Debug)] */ n = cbm_tokenize_decorator("#[derive(Debug)]", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "derive"); for (int i = 0; i < n; i++) free(tokens[i]); /* Both "app" and "get" are stopwords → empty */ n = cbm_tokenize_decorator("@app.get(\"/api\")", tokens, 16); ASSERT_EQ(n, 0); /* "router" is stopword, "post" passes */ n = cbm_tokenize_decorator("@router.post(\"/api\")", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "post"); for (int i = 0; i < n; i++) free(tokens[i]); /* Too short after filtering */ n = cbm_tokenize_decorator("@x", tokens, 16); ASSERT_EQ(n, 0); /* Empty */ n = cbm_tokenize_decorator("", tokens, 16); ASSERT_EQ(n, 0); n = cbm_tokenize_decorator("@click.command", tokens, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(tokens[0], "click"); ASSERT_STR_EQ(tokens[1], "command"); for (int i = 0; i < n; i++) free(tokens[i]); n = cbm_tokenize_decorator("@celery.task", tokens, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(tokens[0], "celery"); ASSERT_STR_EQ(tokens[1], "task"); for (int i = 0; i < n; i++) free(tokens[i]); PASS(); } /* ── Decorator tags integration tests (enrichment_test.go ports) ─ */ /* Helper: check if a node's properties_json contains a specific decorator_tag */ static bool has_decorator_tag(const char *properties_json, const char *tag) { if (!properties_json || !tag) return false; yyjson_doc *doc = yyjson_read(properties_json, strlen(properties_json), 0); if (!doc) return false; yyjson_val *root = yyjson_doc_get_root(doc); yyjson_val *tags = yyjson_obj_get(root, "decorator_tags"); if (!tags || !yyjson_is_arr(tags)) { yyjson_doc_free(doc); return false; } yyjson_val *item; yyjson_arr_iter iter; yyjson_arr_iter_init(tags, &iter); while ((item = yyjson_arr_iter_next(&iter))) { if (yyjson_is_str(item) && strcmp(yyjson_get_str(item), tag) == 0) { yyjson_doc_free(doc); return true; } } yyjson_doc_free(doc); return false; } TEST(decorator_tags_python_auto_discovery) { /* Port of TestDecoratorTagAutoDiscovery: * Python file with repeated decorators (@login_required on 2 funcs, * @cache on 2 funcs, @unique_helper on 1 func). * Words on 2+ nodes become tags; unique words do not. */ const char *files[] = {"views.py"}; const char *contents[] = {"from functools import cache\n\n" "@login_required\n" "def list_orders():\n" " pass\n\n" "@login_required\n" "def get_order():\n" " pass\n\n" "@cache\n" "def compute_total():\n" " pass\n\n" "@cache\n" "def compute_tax():\n" " pass\n\n" "@unique_helper\n" "def special():\n" " pass\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); /* Find functions by name and check decorator_tags */ cbm_node_t *funcs = NULL; int fc = 0; cbm_store_find_nodes_by_label(s, proj, "Function", &funcs, &fc); /* Build name→properties_json map */ const char *list_orders_props = NULL; const char *get_order_props = NULL; const char *compute_total_props = NULL; const char *compute_tax_props = NULL; const char *special_props = NULL; for (int i = 0; i < fc; i++) { if (strcmp(funcs[i].name, "list_orders") == 0) list_orders_props = funcs[i].properties_json; else if (strcmp(funcs[i].name, "get_order") == 0) get_order_props = funcs[i].properties_json; else if (strcmp(funcs[i].name, "compute_total") == 0) compute_total_props = funcs[i].properties_json; else if (strcmp(funcs[i].name, "compute_tax") == 0) compute_tax_props = funcs[i].properties_json; else if (strcmp(funcs[i].name, "special") == 0) special_props = funcs[i].properties_json; } /* "login" and "required" appear on 2 nodes → should be tags */ ASSERT_TRUE(has_decorator_tag(list_orders_props, "login")); ASSERT_TRUE(has_decorator_tag(list_orders_props, "required")); ASSERT_TRUE(has_decorator_tag(get_order_props, "login")); ASSERT_TRUE(has_decorator_tag(get_order_props, "required")); /* "cache" appears on 2 nodes → should be a tag */ ASSERT_TRUE(has_decorator_tag(compute_total_props, "cache")); ASSERT_TRUE(has_decorator_tag(compute_tax_props, "cache")); /* "unique" and "helper" appear on only 1 node → should NOT be tags */ ASSERT_FALSE(has_decorator_tag(special_props, "unique")); ASSERT_FALSE(has_decorator_tag(special_props, "helper")); if (funcs) cbm_store_free_nodes(funcs, fc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } TEST(decorator_tags_java_class_methods) { /* Port of TestDecoratorTagJavaClassMethods: * Java class with @GetMapping, @PostMapping, @Transactional annotations. * "mapping" appears on all 4 → tag. "post" on 2 → tag. */ const char *files[] = {"Controller.java"}; const char *contents[] = {"class OwnerController {\n" " @GetMapping(\"/owners\")\n" " public void listOwners() {}\n\n" " @GetMapping(\"/owners/{id}\")\n" " public void showOwner() {}\n\n" " @PostMapping(\"/owners\")\n" " public void createOwner() {}\n\n" " @Transactional\n" " @PostMapping(\"/owners/{id}\")\n" " public void updateOwner() {}\n" "}\n"}; if (setup_lang_repo(files, contents, 1) != 0) FAIL("tmpdir"); char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); /* Find methods */ cbm_node_t *methods = NULL; int mc = 0; cbm_store_find_nodes_by_label(s, proj, "Method", &methods, &mc); /* "mapping" appears on all 4 methods → should be a tag */ for (int i = 0; i < mc; i++) { if (strcmp(methods[i].name, "listOwners") == 0 || strcmp(methods[i].name, "showOwner") == 0 || strcmp(methods[i].name, "createOwner") == 0 || strcmp(methods[i].name, "updateOwner") == 0) { ASSERT_TRUE(has_decorator_tag(methods[i].properties_json, "mapping")); } } /* "post" appears on createOwner + updateOwner → should be a tag */ for (int i = 0; i < mc; i++) { if (strcmp(methods[i].name, "createOwner") == 0 || strcmp(methods[i].name, "updateOwner") == 0) { ASSERT_TRUE(has_decorator_tag(methods[i].properties_json, "post")); } } /* "transactional" appears on only 1 method → should NOT be a tag */ for (int i = 0; i < mc; i++) { if (strcmp(methods[i].name, "updateOwner") == 0) { ASSERT_FALSE(has_decorator_tag(methods[i].properties_json, "transactional")); } } if (methods) cbm_store_free_nodes(methods, mc); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } /* ── Compile commands helpers (pass_compile_commands.c) ────────── */ TEST(compile_commands_split_command) { char *args[16]; int n; n = cbm_split_command("gcc -c main.c", args, 16); ASSERT_EQ(n, 3); ASSERT_STR_EQ(args[0], "gcc"); ASSERT_STR_EQ(args[1], "-c"); ASSERT_STR_EQ(args[2], "main.c"); for (int i = 0; i < n; i++) free(args[i]); n = cbm_split_command("gcc -DFOO=\"bar baz\" -c main.c", args, 16); ASSERT_EQ(n, 4); for (int i = 0; i < n; i++) free(args[i]); n = cbm_split_command("g++ -I/usr/include -std=c++17 -o out -c in.cpp", args, 16); ASSERT_EQ(n, 7); for (int i = 0; i < n; i++) free(args[i]); PASS(); } TEST(compile_commands_extract_flags) { const char *args[] = {"g++", "-I", "/abs/include", "-I/rel/include", "-isystem", "/sys/include", "-DFOO", "-DBAR=42", "-std=c++20", "-O2", "-Wall", "-c", "main.cpp"}; cbm_compile_flags_t *f = cbm_extract_flags(args, 13, "/project"); ASSERT_NOT_NULL(f); ASSERT_EQ(f->include_count, 3); ASSERT_EQ(f->define_count, 2); ASSERT_STR_EQ(f->standard, "c++20"); cbm_compile_flags_free(f); PASS(); } TEST(compile_commands_parse_json) { const char *json = "[\n" " {\n" " \"directory\": \"/home/user/project/build\",\n" " \"command\": \"gcc -I/home/user/project/include " "-I/home/user/project/src -DDEBUG=1 -DVERSION=\\\"1.0\\\" " "-std=c11 -o main.o -c /home/user/project/src/main.c\",\n" " \"file\": \"/home/user/project/src/main.c\"\n" " },\n" " {\n" " \"directory\": \"/home/user/project/build\",\n" " \"arguments\": [\"g++\", \"-I/home/user/project/include\", " "\"-isystem\", \"/home/user/project/third_party\", " "\"-DUSE_SSL\", \"-std=c++17\", \"-c\", " "\"/home/user/project/src/server.cpp\"],\n" " \"file\": \"/home/user/project/src/server.cpp\"\n" " },\n" " {\n" " \"directory\": \"/home/user/project/build\",\n" " \"command\": \"gcc -c /outside/repo/file.c\",\n" " \"file\": \"/outside/repo/file.c\"\n" " }\n" "]"; char **paths = NULL; cbm_compile_flags_t **flags = NULL; int n = cbm_parse_compile_commands(json, "/home/user/project", &paths, &flags); ASSERT(n >= 2); /* At least main.c and server.cpp, outside file excluded */ /* Find main.c */ int main_idx = -1, server_idx = -1; for (int i = 0; i < n; i++) { if (strcmp(paths[i], "src/main.c") == 0) main_idx = i; if (strcmp(paths[i], "src/server.cpp") == 0) server_idx = i; } ASSERT(main_idx >= 0); ASSERT_EQ(flags[main_idx]->include_count, 2); ASSERT_EQ(flags[main_idx]->define_count, 2); ASSERT_STR_EQ(flags[main_idx]->standard, "c11"); ASSERT(server_idx >= 0); ASSERT_EQ(flags[server_idx]->include_count, 2); ASSERT_EQ(flags[server_idx]->define_count, 1); ASSERT_STR_EQ(flags[server_idx]->standard, "c++17"); /* Verify outside-repo file excluded */ for (int i = 0; i < n; i++) { ASSERT(strstr(paths[i], "outside") == NULL); } /* Cleanup */ for (int i = 0; i < n; i++) { free(paths[i]); cbm_compile_flags_free(flags[i]); } free(paths); free(flags); PASS(); } TEST(compile_commands_parse_empty) { char **paths = NULL; cbm_compile_flags_t **flags = NULL; int n = cbm_parse_compile_commands("[]", "/repo", &paths, &flags); ASSERT_EQ(n, 0); free(paths); free(flags); PASS(); } TEST(compile_commands_parse_invalid) { char **paths = NULL; cbm_compile_flags_t **flags = NULL; int n = cbm_parse_compile_commands("not json", "/repo", &paths, &flags); ASSERT(n < 0); PASS(); } /* ── Suite ─────────────────────────────────────────────────────── */ /* ── Infrascan: file identification ──────────────────────────────── */ TEST(infra_is_kustomize_file) { ASSERT(cbm_is_kustomize_file("kustomization.yaml")); ASSERT(cbm_is_kustomize_file("kustomization.yml")); ASSERT(cbm_is_kustomize_file("KUSTOMIZATION.YAML")); /* case-insensitive */ ASSERT(!cbm_is_kustomize_file("deployment.yaml")); ASSERT(!cbm_is_kustomize_file("kustomize.yaml")); ASSERT(!cbm_is_kustomize_file(NULL)); PASS(); } TEST(infra_is_k8s_manifest) { const char *deploy = "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: my-app\n"; const char *plain = "name: foo\nvalue: bar\n"; const char *kust = "apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\n"; ASSERT(cbm_is_k8s_manifest("deployment.yaml", deploy)); ASSERT(!cbm_is_k8s_manifest("deployment.yaml", plain)); /* kustomize file should return false even if it has apiVersion */ ASSERT(!cbm_is_k8s_manifest("kustomization.yaml", kust)); ASSERT(!cbm_is_k8s_manifest(NULL, deploy)); ASSERT(!cbm_is_k8s_manifest("deployment.yaml", NULL)); PASS(); } /* ── Infrascan: secret detection ────────────────────────────────── */ TEST(infra_secret_detection) { /* Key-based detection */ ASSERT(cbm_is_secret_binding("JWT_SECRET", "anything")); ASSERT(cbm_is_secret_binding("API_KEY", "anything")); ASSERT(cbm_is_secret_binding("my_password", "anything")); ASSERT(cbm_is_secret_binding("AUTH_TOKEN", "anything")); ASSERT(!cbm_is_secret_binding("DATABASE_URL", "https://db.example.com")); /* Value-based detection */ ASSERT(cbm_is_secret_value("sk-1234567890abcdef12345")); ASSERT(cbm_is_secret_value("-----BEGIN RSA PRIVATE KEY-----")); ASSERT(!cbm_is_secret_value("https://db.example.com")); ASSERT(!cbm_is_secret_value("hello world")); ASSERT(!cbm_is_secret_value("8080")); /* isSecretBinding checks both */ ASSERT(cbm_is_secret_binding("ANYTHING", "sk-1234567890abcdef12345")); ASSERT(!cbm_is_secret_binding("PORT", "8080")); PASS(); } /* ── Helm Chart.yaml dependency parsing (#338) ──────────────────── */ TEST(helm_parse_chart_dependencies_issue338) { const char *src = "apiVersion: v2\n" "name: mychart\n" "version: 1.0.0\n" "dependencies:\n" " - name: postgresql\n" " repository: https://charts.bitnami.com/bitnami\n" " version: 12.x.x\n" " - name: redis\n" " repository: https://charts.bitnami.com/bitnami\n" "maintainers:\n" " - name: alice\n"; /* not a dependency — outside the block */ cbm_helm_chart_t hc; ASSERT_EQ(cbm_parse_helm_chart(src, &hc), 0); ASSERT_STR_EQ(hc.chart_name, "mychart"); ASSERT_EQ(hc.dep_count, 2); ASSERT_STR_EQ(hc.deps[0].name, "postgresql"); ASSERT_STR_EQ(hc.deps[0].repository, "https://charts.bitnami.com/bitnami"); ASSERT_STR_EQ(hc.deps[1].name, "redis"); ASSERT_STR_EQ(hc.deps[1].repository, "https://charts.bitnami.com/bitnami"); cbm_helm_chart_free(&hc); PASS(); } TEST(helm_parse_chart_no_deps_issue338) { cbm_helm_chart_t hc; ASSERT_EQ(cbm_parse_helm_chart("name: solo\nversion: 0.1.0\n", &hc), 0); ASSERT_STR_EQ(hc.chart_name, "solo"); ASSERT_EQ(hc.dep_count, 0); cbm_helm_chart_free(&hc); PASS(); } /* An entry's keys come in any order; lists nested in an entry * (import-values, tags) belong to it and name no dependency of their own. */ TEST(helm_parse_chart_dependency_entries) { const char *src = "name: app\n" "dependencies:\n" " - repository: file://../common\n" " name: common\n" " import-values:\n" " - child: default.data\n" " parent: imported\n" " - name: not-a-dependency\n" " - name: redis\n" " tags:\n" " - cache\n"; cbm_helm_chart_t hc; ASSERT_EQ(cbm_parse_helm_chart(src, &hc), 0); ASSERT_EQ(hc.dep_count, 2); ASSERT_STR_EQ(hc.deps[0].name, "common"); ASSERT_STR_EQ(hc.deps[0].repository, "file://../common"); ASSERT_STR_EQ(hc.deps[1].name, "redis"); ASSERT_STR_EQ(hc.deps[1].repository, ""); cbm_helm_chart_free(&hc); PASS(); } /* No dependency count decides what is read: the parser kept 128. */ TEST(helm_parse_chart_many_dependencies) { enum { DEPS = 130 }; char *src = malloc(DEPS * 32 + 64); ASSERT_NOT_NULL(src); int n = snprintf(src, 64, "name: umbrella\ndependencies:\n"); for (int i = 0; i < DEPS; i++) { n += snprintf(src + n, 32, " - name: dep-%03d\n", i); } cbm_helm_chart_t hc; ASSERT_EQ(cbm_parse_helm_chart(src, &hc), 0); free(src); ASSERT_EQ(hc.dep_count, DEPS); ASSERT_STR_EQ(hc.deps[DEPS - 1].name, "dep-129"); cbm_helm_chart_free(&hc); PASS(); } /* ── Infrascan: infra QN helper ─────────────────────────────────── */ /* ── Function Registry / Resolver tests ─────────────────────────── */ TEST(registry_resolve_single_candidate) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "CreateOrder", "svcA.handlers.CreateOrder", "Function"); cbm_registry_add(reg, "ValidateOrder", "svcB.validators.ValidateOrder", "Function"); /* Normal resolve unique name */ cbm_resolution_t r = cbm_registry_resolve(reg, "CreateOrder", "svcC.caller", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "svcA.handlers.CreateOrder"); /* Fuzzy resolve with unknown prefix */ cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknownPkg.CreateOrder", "svcC.caller", NULL, NULL, 0); ASSERT_TRUE(fr.ok); ASSERT_STR_EQ(fr.result.qualified_name, "svcA.handlers.CreateOrder"); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_nonexistent) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "CreateOrder", "svcA.handlers.CreateOrder", "Function"); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "NonExistent", "svcC.caller", NULL, NULL, 0); ASSERT_FALSE(fr.ok); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_multiple_best_by_distance) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Process", "svcA.handlers.Process", "Function"); cbm_registry_add(reg, "Process", "svcB.handlers.Process", "Function"); /* Caller in svcA → prefer svcA */ cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknown.Process", "svcA.other", NULL, NULL, 0); ASSERT_TRUE(fr.ok); ASSERT_STR_EQ(fr.result.qualified_name, "svcA.handlers.Process"); /* Caller in svcB → prefer svcB */ fr = cbm_registry_fuzzy_resolve(reg, "unknown.Process", "svcB.other", NULL, NULL, 0); ASSERT_TRUE(fr.ok); ASSERT_STR_EQ(fr.result.qualified_name, "svcB.handlers.Process"); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_simple_name_extraction) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "DoWork", "myproject.utils.DoWork", "Function"); /* Deeply qualified name → extract "DoWork" */ cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "some.deep.module.DoWork", "myproject.caller", NULL, NULL, 0); ASSERT_TRUE(fr.ok); ASSERT_STR_EQ(fr.result.qualified_name, "myproject.utils.DoWork"); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_empty) { cbm_registry_t *reg = cbm_registry_new(); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "SomeFunc", "myproject.caller", NULL, NULL, 0); ASSERT_FALSE(fr.ok); cbm_registry_free(reg); PASS(); } TEST(registry_exists) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Foo", "pkg.module.Foo", "Function"); cbm_registry_add(reg, "Bar", "pkg.module.Bar", "Method"); ASSERT_TRUE(cbm_registry_exists(reg, "pkg.module.Foo")); ASSERT_TRUE(cbm_registry_exists(reg, "pkg.module.Bar")); ASSERT_FALSE(cbm_registry_exists(reg, "pkg.module.Missing")); ASSERT_FALSE(cbm_registry_exists(reg, "")); cbm_registry_free(reg); PASS(); } TEST(registry_confidence_import_map) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function"); const char *keys[] = {"other"}; const char *vals[] = {"proj.other"}; cbm_resolution_t r = cbm_registry_resolve(reg, "other.Foo", "proj.pkg", keys, vals, 1); ASSERT_STR_EQ(r.qualified_name, "proj.other.Foo"); ASSERT(r.confidence > 0.90 && r.confidence <= 1.0); ASSERT_STR_EQ(r.strategy, "import_map"); cbm_registry_free(reg); PASS(); } TEST(registry_confidence_import_map_suffix) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Foo", "proj.other.sub.Foo", "Function"); const char *keys[] = {"other"}; const char *vals[] = {"proj.other"}; cbm_resolution_t r = cbm_registry_resolve(reg, "other.Foo", "proj.pkg", keys, vals, 1); ASSERT_STR_EQ(r.qualified_name, "proj.other.sub.Foo"); ASSERT(r.confidence > 0.80 && r.confidence <= 0.90); ASSERT_STR_EQ(r.strategy, "import_map_suffix"); cbm_registry_free(reg); PASS(); } TEST(registry_confidence_same_module) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function"); cbm_resolution_t r = cbm_registry_resolve(reg, "Foo", "proj.pkg", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.pkg.Foo"); ASSERT(r.confidence > 0.85 && r.confidence <= 0.95); ASSERT_STR_EQ(r.strategy, "same_module"); cbm_registry_free(reg); PASS(); } TEST(registry_confidence_unique_name) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function"); cbm_resolution_t r = cbm_registry_resolve(reg, "Bar", "proj.unrelated", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.pkg.Bar"); ASSERT(r.confidence > 0.70 && r.confidence <= 0.80); ASSERT_STR_EQ(r.strategy, "unique_name"); cbm_registry_free(reg); PASS(); } TEST(registry_confidence_suffix_match) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Process", "proj.svcA.Process", "Function"); cbm_registry_add(reg, "Process", "proj.svcB.Process", "Function"); cbm_resolution_t r = cbm_registry_resolve(reg, "Process", "proj.svcA.caller", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.svcA.Process"); ASSERT(r.confidence > 0.50 && r.confidence <= 0.60); ASSERT_STR_EQ(r.strategy, "suffix_match"); cbm_registry_free(reg); PASS(); } /* Issue #1893: a call on a library type bound to a same-named project member. * URLSession is Foundation's, not this project's, so PickedFile.data is the * wrong target — and with one candidate it won the top name-only confidence. */ TEST(registry_receiver_chain_refuses_library_unique_name_issue1893) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable"); cbm_resolution_t r = cbm_registry_resolve(reg, "URLSession.shared.data", "HomeboxUI.Net", NULL, NULL, 0); ASSERT_NULL(r.qualified_name); cbm_registry_free(reg); PASS(); } /* The same refusal on the other name-only exit, where several candidates share * the final name and import distance picks the winner. */ TEST(registry_receiver_chain_refuses_library_suffix_match_issue1893) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "data", "HomeboxUI.PickedFile.data", "Variable"); cbm_registry_add(reg, "data", "HomeboxUI.Payload.data", "Variable"); cbm_resolution_t r = cbm_registry_resolve(reg, "URLSession.shared.data", "HomeboxUI.Net", NULL, NULL, 0); ASSERT_NULL(r.qualified_name); cbm_registry_free(reg); PASS(); } /* The true positive the gate must not eat: the project extends Calendar itself, * so Calendar really is in the receiver chain. */ TEST(registry_receiver_chain_keeps_project_extension_issue1893) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "startOfDayUTC", "AuthDTOs.Calendar.startOfDayUTC", "Method"); cbm_resolution_t r = cbm_registry_resolve(reg, "Calendar.utcGregorian.startOfDayUTC", "HomeboxUI.Stats", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "AuthDTOs.Calendar.startOfDayUTC"); ASSERT_STR_EQ(r.strategy, "unique_name"); cbm_registry_free(reg); PASS(); } /* A factory chain into a nested type: Settings.builder().putList names the only * putList in the project, Settings.Builder.putList, and Settings — the chain's * root — is in the candidate's ancestry even though the candidate's immediate * parent (Builder) is not spelled in the chain. The parent-only rule refused * 9,473 such unique-name calls on elastic/elasticsearch (v0.10.8 → v0.11.0 A/B, * 2026-09-16). */ TEST(registry_receiver_chain_admits_factory_chain_into_nested_type) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "putList", "org.elasticsearch.common.settings.Settings.Builder.putList", "Method"); cbm_resolution_t r = cbm_registry_resolve( reg, "Settings.builder().putList", "org.elasticsearch.index.IndexSettings", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "org.elasticsearch.common.settings.Settings.Builder.putList"); ASSERT_STR_EQ(r.strategy, "unique_name"); /* The same shape through a value in between: RequestOptions.DEFAULT * .toBuilder().setWarningsHandler -> RequestOptions.Builder.setWarningsHandler. */ cbm_registry_add(reg, "setWarningsHandler", "org.elasticsearch.client.RequestOptions.Builder.setWarningsHandler", "Method"); r = cbm_registry_resolve(reg, "RequestOptions.DEFAULT.toBuilder().setWarningsHandler", "org.elasticsearch.client.Rest", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "org.elasticsearch.client.RequestOptions.Builder.setWarningsHandler"); cbm_registry_free(reg); PASS(); } /* The ancestry rule must not widen the gate to foreign roots: Base64 is the * JDK's, so the project's DocOffsetsCodec.getEncoder stays refused, exactly as * #1893's URLSession case above. */ TEST(registry_receiver_chain_still_refuses_foreign_root_with_ancestry_rule) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "getEncoder", "org.elasticsearch.index.codec.DocOffsetsCodec.getEncoder", "Method"); cbm_resolution_t r = cbm_registry_resolve( reg, "Base64.getEncoder", "org.elasticsearch.index.IndexSettings", NULL, NULL, 0); ASSERT_NULL(r.qualified_name); /* Nor may a package segment admit it: Math.toIntExact must not bind * org.elasticsearch.common.Math.toIntExact through "Math" appearing as an * ancestry segment ONLY IF that segment really is the type — here it is, and * that is the correct outcome (the project's own Math.toIntExact). */ cbm_registry_add(reg, "toIntExact", "org.elasticsearch.common.Math.toIntExact", "Method"); r = cbm_registry_resolve(reg, "Math.toIntExact", "org.elasticsearch.index.IndexSettings", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "org.elasticsearch.common.Math.toIntExact"); cbm_registry_free(reg); PASS(); } /* A lower-case root names a value, whose type the chain does not show. The gate * must not look at it, or every ordinary vm.load style call would be refused. */ TEST(registry_receiver_chain_ignores_lowercase_root_issue1893) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "load", "HomeboxUI.EntityListViewModel.load", "Method"); cbm_resolution_t r = cbm_registry_resolve(reg, "vm.load", "HomeboxUI.Views", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "HomeboxUI.EntityListViewModel.load"); ASSERT_STR_EQ(r.strategy, "unique_name"); cbm_registry_free(reg); PASS(); } /* An unqualified callee has no chain at all and must pass through unchanged. */ TEST(registry_receiver_chain_ignores_bare_name_issue1893) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "helper", "proj.pkg.helper", "Function"); cbm_resolution_t r = cbm_registry_resolve(reg, "helper", "proj.other", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.pkg.helper"); ASSERT_STR_EQ(r.strategy, "unique_name"); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_confidence_single) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Handler", "proj.svc.Handler", "Function"); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknownPkg.Handler", "proj.caller", NULL, NULL, 0); ASSERT_TRUE(fr.ok); ASSERT(fr.result.confidence > 0.35 && fr.result.confidence <= 0.45); ASSERT_STR_EQ(fr.result.strategy, "fuzzy"); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_confidence_distance) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Process", "proj.svcA.Process", "Function"); cbm_registry_add(reg, "Process", "proj.svcB.Process", "Function"); cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknownPkg.Process", "proj.svcA.other", NULL, NULL, 0); ASSERT_TRUE(fr.ok); ASSERT(fr.result.confidence > 0.25 && fr.result.confidence <= 0.35); ASSERT_STR_EQ(fr.result.strategy, "fuzzy"); cbm_registry_free(reg); PASS(); } TEST(registry_negative_import_rejects) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Process", "proj.billing.Process", "Function"); cbm_registry_add(reg, "Process", "proj.handler.Process", "Function"); /* Import only handler's module → should prefer handler */ const char *keys[] = {"handler"}; const char *vals[] = {"proj.handler"}; cbm_resolution_t r = cbm_registry_resolve(reg, "Process", "proj.caller", keys, vals, 1); ASSERT_STR_EQ(r.qualified_name, "proj.handler.Process"); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_import_penalty) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function"); /* Has imports but billing not imported → confidence halved */ const char *keys[] = {"other"}; const char *vals[] = {"proj.other"}; cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknown.Handler", "proj.caller", keys, vals, 1); ASSERT_TRUE(fr.ok); /* 0.40 * 0.5 = 0.20 */ ASSERT(fr.result.confidence > 0.15 && fr.result.confidence <= 0.25); cbm_registry_free(reg); PASS(); } TEST(registry_fuzzy_no_import_map_passthrough) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Handler", "proj.billing.Handler", "Function"); /* NULL import map → no penalty, full fuzzy confidence */ cbm_fuzzy_result_t fr = cbm_registry_fuzzy_resolve(reg, "unknown.Handler", "proj.caller", NULL, NULL, 0); ASSERT_TRUE(fr.ok); ASSERT(fr.result.confidence > 0.35 && fr.result.confidence <= 0.45); cbm_registry_free(reg); PASS(); } TEST(registry_find_by_name) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function"); cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function"); cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function"); cbm_registry_add(reg, "transform", "proj.utils.DataProcessor.transform", "Method"); /* FindByName returns all entries for "Foo" */ const char **foos = NULL; int foos_count = 0; cbm_registry_find_by_name(reg, "Foo", &foos, &foos_count); ASSERT_EQ(foos_count, 2); /* FindByName for unique "Bar" */ const char **bars = NULL; int bars_count = 0; cbm_registry_find_by_name(reg, "Bar", &bars, &bars_count); ASSERT_EQ(bars_count, 1); ASSERT_STR_EQ(bars[0], "proj.pkg.Bar"); /* FindByName for "transform" */ const char **transforms = NULL; int trans_count = 0; cbm_registry_find_by_name(reg, "transform", &transforms, &trans_count); ASSERT_EQ(trans_count, 1); ASSERT_STR_EQ(transforms[0], "proj.utils.DataProcessor.transform"); /* label_of */ ASSERT_STR_EQ(cbm_registry_label_of(reg, "proj.utils.DataProcessor.transform"), "Method"); ASSERT_STR_EQ(cbm_registry_label_of(reg, "proj.pkg.Foo"), "Function"); /* Total size */ ASSERT_EQ(cbm_registry_size(reg), 4); /* Resolve same-module */ cbm_resolution_t r = cbm_registry_resolve(reg, "Foo", "proj.pkg", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.pkg.Foo"); /* Resolve via import map */ const char *keys[] = {"other"}; const char *vals[] = {"proj.other"}; r = cbm_registry_resolve(reg, "other.Foo", "proj.pkg", keys, vals, 1); ASSERT_STR_EQ(r.qualified_name, "proj.other.Foo"); /* Resolve unique name */ r = cbm_registry_resolve(reg, "Bar", "proj.unrelated", NULL, NULL, 0); ASSERT_STR_EQ(r.qualified_name, "proj.pkg.Bar"); cbm_registry_free(reg); PASS(); } TEST(registry_confidence_band) { ASSERT_STR_EQ(cbm_confidence_band(0.95), "high"); ASSERT_STR_EQ(cbm_confidence_band(0.70), "high"); ASSERT_STR_EQ(cbm_confidence_band(0.55), "medium"); ASSERT_STR_EQ(cbm_confidence_band(0.45), "medium"); ASSERT_STR_EQ(cbm_confidence_band(0.40), "speculative"); ASSERT_STR_EQ(cbm_confidence_band(0.25), "speculative"); ASSERT_STR_EQ(cbm_confidence_band(0.20), ""); ASSERT_STR_EQ(cbm_confidence_band(0.0), ""); PASS(); } TEST(infra_qn_helper) { /* Port of TestInfraQN */ /* Regular infra file → __infra__ suffix */ char *qn = cbm_infra_qn("myproject", "docker-images/service/Dockerfile", "dockerfile", NULL); ASSERT_NOT_NULL(qn); ASSERT(strstr(qn, ".__infra__") != NULL); free(qn); /* Compose service → ::service_name suffix */ qn = cbm_infra_qn("myproject", "docker-compose.yml", "compose-service", "web"); ASSERT_NOT_NULL(qn); ASSERT(strstr(qn, "::web") != NULL); free(qn); PASS(); } /* ── K8s / Kustomize extraction tests ──────────────────────────── */ TEST(k8s_extract_kustomize) { const char *src = "apiVersion: kustomize.config.k8s.io/v1beta1\n" "kind: Kustomization\n" "resources:\n" " - deployment.yaml\n" " - service.yaml\n"; CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), CBM_LANG_KUSTOMIZE, "myproj", "base/kustomization.yaml", 0, NULL, NULL); ASSERT(r != NULL); ASSERT_GTE(r->imports.count, 2); bool found_deploy = false, found_svc = false; for (int i = 0; i < r->imports.count; i++) { if (r->imports.items[i].module_path && strcmp(r->imports.items[i].module_path, "deployment.yaml") == 0) found_deploy = true; if (r->imports.items[i].module_path && strcmp(r->imports.items[i].module_path, "service.yaml") == 0) found_svc = true; } ASSERT_TRUE(found_deploy); ASSERT_TRUE(found_svc); cbm_free_result(r); PASS(); } TEST(k8s_extract_manifest) { const char *src = "apiVersion: apps/v1\n" "kind: Deployment\n" "metadata:\n" " name: my-app\n" " namespace: production\n"; CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), CBM_LANG_K8S, "myproj", "k8s/deployment.yaml", 0, NULL, NULL); ASSERT(r != NULL); ASSERT_GTE(r->defs.count, 1); bool found_resource = false; for (int d = 0; d < r->defs.count; d++) { if (r->defs.items[d].label && strcmp(r->defs.items[d].label, "Resource") == 0 && r->defs.items[d].name && strstr(r->defs.items[d].name, "Deployment") != NULL) found_resource = true; } ASSERT_TRUE(found_resource); cbm_free_result(r); PASS(); } TEST(k8s_extract_manifest_no_name) { const char *src = "apiVersion: apps/v1\nkind: Deployment\n"; CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), CBM_LANG_K8S, "myproj", "k8s/deploy.yaml", 0, NULL, NULL); ASSERT(r != NULL); /* No crash — defs count may be 0 because metadata.name is absent */ ASSERT(!r->has_error); cbm_free_result(r); PASS(); } TEST(k8s_extract_manifest_multidoc) { /* Two-document YAML separated by "---": every document is an object, with * the identity QN ::///. */ const char *src = "apiVersion: apps/v1\n" "kind: Deployment\n" "metadata:\n" " name: my-app\n" "---\n" "apiVersion: v1\n" "kind: Service\n" "metadata:\n" " name: my-svc\n"; CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), CBM_LANG_K8S, "myproj", "k8s/multi.yaml", 0, NULL, NULL); ASSERT(r != NULL); ASSERT(!r->has_error); int deploy = 0; int svc = 0; for (int i = 0; i < r->defs.count; i++) { const CBMDefinition *d = &r->defs.items[i]; if (!d->label || strcmp(d->label, "Resource") != 0 || !d->name || !d->qualified_name) { continue; } if (strcmp(d->name, "Deployment/my-app") == 0 && strcmp(d->qualified_name, "myproj.k8s.multi.yaml::apps/Deployment/-/my-app") == 0) { deploy++; } if (strcmp(d->name, "Service/my-svc") == 0 && strcmp(d->qualified_name, "myproj.k8s.multi.yaml::core/Service/-/my-svc") == 0) { svc++; } } ASSERT_EQ(deploy, 1); ASSERT_EQ(svc, 1); cbm_free_result(r); PASS(); } /* ── Temp-file fixture helpers ───────────────────────────────────── */ /* Helper: write a file inside a temp dir */ static void write_temp_file(const char *dir, const char *name, const char *content) { char path[512]; /* Create subdirectories if needed */ snprintf(path, sizeof(path), "%s/%s", dir, name); char *slash = strrchr(path, '/'); if (slash) { char parent[512]; size_t plen = slash - path; memcpy(parent, path, plen); parent[plen] = '\0'; cbm_mkdir_p(parent, 0755); } /* Binary, matching test_helpers.h/repro_harness.h: text mode turns "\n" * into "\r\n" on Windows, so the bytes on disk stop matching the source * string the test hashes or measures -- the semantic-manifest tests * compare cbm_sha256_hex(string) against the pipeline's hash of this * file, and the quarantine test compares byte sizes. */ FILE *f = cbm_fopen(path, "wb"); if (f) { fputs(content, f); fclose(f); } } static bool fixture_file_matches(const char *path, const char *expected) { FILE *file = cbm_fopen(path, "rb"); if (!file) { return false; } char actual[1024]; size_t expected_length = strlen(expected); size_t offset = 0; size_t bytes = 0; bool matched = true; while ((bytes = fread(actual, 1, sizeof(actual), file)) != 0) { if (bytes > expected_length - offset || memcmp(actual, expected + offset, bytes) != 0) { matched = false; break; } offset += bytes; } matched = matched && !ferror(file) && feof(file) && offset == expected_length; if (fclose(file) != 0) { matched = false; } return matched; } /* #2034: no manual mkdir may make an absent deep fixture look indexed. */ TEST(pipeline_nested_fixture_files_are_written) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_nested_fixture_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } char long_name[480]; size_t long_length = 480 - strlen(tmp) - 1; memset(long_name, 'a', long_length); for (size_t i = 89; i < long_length - 4; i += 90) { long_name[i] = '/'; } memcpy(long_name + long_length - 4, ".txt", 4); long_name[long_length] = '\0'; char boundary[3][1026]; for (size_t i = 0; i < 3; i++) { size_t length = 1023 + i; memset(boundary[i], 'a', length); boundary[i][length] = '\0'; } const struct { const char *name; const char *content; } cases[] = { {"root.go", "package fixtures\n"}, {"pkg/one.go", "package fixtures\n\nfunc Marker() {}\n"}, {"registry/batch/two.go", "package fixtures\n"}, {"registry/batch/cronjob/strategy.go", "package fixtures\n"}, {"a/b/c/d/e/f/g/h/deep.txt", "deep\n"}, {"space dir/кириллица/中文/data.txt", "строка\nline\r\n行\n"}, {"empty/a/file.txt", ""}, {"newline/a/file.txt", "\n"}, {"siblings/one/file.txt", "one\n"}, {"siblings/two/file.txt", "two\n"}, {"partial/existing/new/branch/file.txt", "partial\n"}, {long_name, "long path\n"}, {"boundary/deep/1023.txt", boundary[0]}, {"boundary/deep/1024.txt", boundary[1]}, {"boundary/deep/1025.txt", boundary[2]}, }; bool partial_parent = cbm_mkdir_p(TH_PATH(tmp, "partial/existing"), 0755); bool matched[sizeof(cases) / sizeof(cases[0])] = {false}; for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { write_temp_file(tmp, cases[i].name, cases[i].content); } for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { matched[i] = fixture_file_matches(TH_PATH(tmp, cases[i].name), cases[i].content); if (!matched[i]) { fprintf(stderr, "missing or mismatched fixture: %s\n", cases[i].name); } } const char *replacement[] = {"longer replacement\n", "x", "", "again\n"}; bool replaced[sizeof(replacement) / sizeof(replacement[0])] = {false}; for (size_t i = 0; i < sizeof(replacement) / sizeof(replacement[0]); i++) { write_temp_file(tmp, "registry/batch/cronjob/strategy.go", replacement[i]); replaced[i] = fixture_file_matches(TH_PATH(tmp, "registry/batch/cronjob/strategy.go"), replacement[i]); } bool sibling_preserved = fixture_file_matches(TH_PATH(tmp, "siblings/one/file.txt"), "one\n"); bool extra_tail_matches = fixture_file_matches(TH_PATH(tmp, "boundary/deep/1025.txt"), boundary[1]); bool truncated_matches = fixture_file_matches(TH_PATH(tmp, "boundary/deep/1023.txt"), boundary[1]); char changed[sizeof(boundary[2])]; memcpy(changed, boundary[2], sizeof(changed)); changed[1024] = 'b'; bool changed_byte_matches = fixture_file_matches(TH_PATH(tmp, "boundary/deep/1025.txt"), changed); int cleanup_rc = th_rmtree(tmp); ASSERT_TRUE(partial_parent); ASSERT_FALSE(extra_tail_matches); ASSERT_FALSE(truncated_matches); ASSERT_FALSE(changed_byte_matches); for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { ASSERT_TRUE(matched[i]); } for (size_t i = 0; i < sizeof(replacement) / sizeof(replacement[0]); i++) { ASSERT_TRUE(replaced[i]); } ASSERT_TRUE(sibling_preserved); ASSERT_EQ(cleanup_rc, 0); PASS(); } TEST(pipeline_fixture_file_parent_is_preserved) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_fixture_parent_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "blocked", "existing bytes\n"); write_temp_file(tmp, "blocked/child/file.txt", "must not be written\n"); bool parent_preserved = fixture_file_matches(TH_PATH(tmp, "blocked"), "existing bytes\n"); bool child_present = cbm_file_exists(TH_PATH(tmp, "blocked/child/file.txt")); bool precondition_satisfied = fixture_file_matches(TH_PATH(tmp, "blocked/child/file.txt"), "must not be written\n"); int cleanup_rc = th_rmtree(tmp); ASSERT_TRUE(parent_preserved); ASSERT_FALSE(child_present); ASSERT_FALSE(precondition_satisfied); ASSERT_EQ(cleanup_rc, 0); PASS(); } TEST(pipeline_fixture_node_count_requires_exact_source_nodes) { cbm_store_t *store = cbm_store_open_memory(); ASSERT_NOT_NULL(store); ASSERT_EQ(cbm_store_upsert_project(store, "fixture", "/tmp"), CBM_STORE_OK); cbm_node_t nodes[] = { {.project = "fixture", .label = "Method", .name = "work", .qualified_name = "fixture.Service.work", .file_path = "nested/service.py"}, {.project = "fixture", .label = "Method", .name = "work", .qualified_name = "fixture.OtherService.work", .file_path = "nested/service.py"}, {.project = "fixture", .label = "Method", .name = "work", .qualified_name = "fixture.OtherFile.work", .file_path = "other/service.py"}, {.project = "fixture", .label = "Function", .name = "work", .qualified_name = "fixture.free_work", .file_path = "nested/service.py"}, }; bool inserted = true; for (size_t i = 0; i < sizeof(nodes) / sizeof(nodes[0]); i++) { if (cbm_store_upsert_node(store, &nodes[i]) <= 0) { inserted = false; } } int methods = fixture_node_count(store, "fixture", "nested/service.py", "work", "Method"); int other_file = fixture_node_count(store, "fixture", "other/service.py", "work", "Method"); int wrong_file = fixture_node_count(store, "fixture", "absent/service.py", "work", "Method"); int wrong_label = fixture_node_count(store, "fixture", "nested/service.py", "work", "Class"); int missing_name = fixture_node_count(store, "fixture", "nested/service.py", "absent", "Method"); int unavailable = fixture_node_count(NULL, "fixture", "nested/service.py", "work", "Method"); cbm_store_close(store); ASSERT_TRUE(inserted); ASSERT_EQ(methods, 2); ASSERT_EQ(other_file, 1); ASSERT_EQ(wrong_file, 0); ASSERT_EQ(wrong_label, 0); ASSERT_EQ(missing_name, 0); ASSERT_EQ(unavailable, -1); PASS(); } TEST(pipeline_nested_fixture_graph_has_endpoints) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_nested_graph_XXXXXX"); if (!cbm_mkdtemp(tmp)) { FAIL("tmpdir"); } write_temp_file(tmp, "main.go", "package main\n\nfunc main() {}\n"); write_temp_file(tmp, "registry/batch/cronjob/strategy.go", "package cronjob\n\n" "func FixtureTarget() {}\n" "func FixtureCaller() { FixtureTarget() }\n" "func FixtureDecoy() {}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/fixture.db", tmp); cbm_pipeline_t *pipeline = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); int run_rc = pipeline ? cbm_pipeline_run(pipeline) : -1; cbm_store_t *store = run_rc == 0 ? cbm_store_open_path(db_path) : NULL; const char *names[] = {"FixtureCaller", "FixtureTarget", "FixtureDecoy"}; int counts[3] = {-1, -1, -1}; bool expected_call = false; bool forbidden_call = false; if (store) { const char *project = cbm_pipeline_project_name(pipeline); for (size_t i = 0; i < 3; i++) { counts[i] = fixture_node_count(store, project, "registry/batch/cronjob/strategy.go", names[i], "Function"); } expected_call = cross_file_call_exists(store, project, "FixtureCaller", "FixtureTarget"); forbidden_call = cross_file_call_exists(store, project, "FixtureCaller", "FixtureDecoy"); cbm_store_close(store); } cbm_pipeline_free(pipeline); int cleanup_rc = th_rmtree(tmp); ASSERT_EQ(run_rc, 0); for (size_t i = 0; i < 3; i++) { ASSERT_EQ(counts[i], 1); } ASSERT_TRUE(expected_call); ASSERT_FALSE(forbidden_call); ASSERT_EQ(cleanup_rc, 0); PASS(); } /* ── Discovery-exclusion plumbing in auxiliary repo walks (#792) ── */ /* Boundary semantics of the shared exclusion predicate: anchored at the * repo root, matches the excluded dir itself and its subtree, but never * sibling names sharing a prefix. Regression guard for issue #792. */ TEST(pipeline_relpath_excluded_boundary) { char *excluded[] = {(char *)"vendor_big", (char *)"packages/big"}; /* Exact match and subtree paths are excluded. */ ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("vendor_big", excluded, 2)); ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("vendor_big/lib/package.json", excluded, 2)); ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("packages/big", excluded, 2)); ASSERT_TRUE(cbm_pipeline_relpath_is_excluded("packages/big/src/x.ts", excluded, 2)); /* Sibling names sharing the prefix are NOT excluded ('/'-boundary). */ ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_bigger", excluded, 2)); ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor", excluded, 2)); ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("packages/bigger/x.ts", excluded, 2)); /* Exclusions are root-anchored prefixes, not substring matches. */ ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("src/vendor_big/x.c", excluded, 2)); /* NULL / empty safety. */ ASSERT_FALSE(cbm_pipeline_relpath_is_excluded(NULL, excluded, 2)); ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("", excluded, 2)); ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_big", NULL, 0)); ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_big", excluded, 0)); char *with_empty[] = {(char *)""}; ASSERT_FALSE(cbm_pipeline_relpath_is_excluded("vendor_big", with_empty, 1)); PASS(); } /* Helper: does the entries array contain a package with this name? */ static int pkg_entries_has_name(const cbm_pkg_entries_t *e, const char *name) { for (int i = 0; i < e->count; i++) { if (e->items[i].pkg_name && strcmp(e->items[i].pkg_name, name) == 0) return 1; } return 0; } /* Helper: return the entry_rel registered for `name`, or NULL. */ static const char *pkg_entries_entry_for(const cbm_pkg_entries_t *e, const char *name) { for (int i = 0; i < e->count; i++) { if (e->items[i].pkg_name && strcmp(e->items[i].pkg_name, name) == 0) return e->items[i].entry_rel; } return NULL; } /* ── SwiftPM Package.swift manifest resolution (issue #551 item 1) ── * * parse_package_swift is a literal pattern-extractor (mirrors * parse_cargo_toml), not a Swift evaluator. These call cbm_pkgmap_try_parse * directly, covering the RED categories the maintainer asked for (local * path deps, remote identities, products not aliasing, targets, target-name * deps, literal + computed `path:`, and comment/string false positives) * plus fail-closed ambiguous-name cases. See pipeline_swift_cross_package_import * above for the full end-to-end proof. */ TEST(pkgmap_swift_targets_registers_module) { static const char src[] = "// swift-tools-version:5.9\n" "import PackageDescription\n" "let package = Package(\n" " name: \"Core\",\n" " targets: [.target(name: \"Core\", dependencies: [])]\n" ")\n"; cbm_pkg_entries_t entries; cbm_pkg_entries_init(&entries); bool ok = cbm_pkgmap_try_parse("Package.swift", "Core/Package.swift", src, (int)strlen(src), &entries); ASSERT_TRUE(ok); ASSERT_TRUE(pkg_entries_has_name(&entries, "Core")); ASSERT_STR_EQ(pkg_entries_entry_for(&entries, "Core"), "Core/Sources/Core"); cbm_pkg_entries_free(&entries); PASS(); } /* Products deliberately do NOT self-register a separate alias: a product * name is not generally an importable module (SwiftPM lets it alias * multiple targets, or none sharing its own name), so only the underlying * target -- under its OWN name -- registers. */ TEST(pkgmap_swift_products_do_not_register_alias) { static const char src[] = "let package = Package(\n" " name: \"Core\",\n" " products: [.library(name: \"CoreKit\", targets: [\"CoreImpl\"])],\n" " targets: [.target(name: \"CoreImpl\", dependencies: [])]\n" ")\n"; cbm_pkg_entries_t entries; cbm_pkg_entries_init(&entries); bool ok = cbm_pkgmap_try_parse("Package.swift", "Core/Package.swift", src, (int)strlen(src), &entries); ASSERT_TRUE(ok); ASSERT_FALSE(pkg_entries_has_name(&entries, "CoreKit")); ASSERT_TRUE(pkg_entries_has_name(&entries, "CoreImpl")); ASSERT_STR_EQ(pkg_entries_entry_for(&entries, "CoreImpl"), "Core/Sources/CoreImpl"); ASSERT_EQ(entries.count, 1); cbm_pkg_entries_free(&entries); PASS(); } /* Regression: a target whose `name:` is the LAST argument, immediately * followed by the call's own closing ')' with no trailing comma, must still * register. swift_quoted_literal's terminator check used to compare against * `end` with a strict '<', but every caller passes the wrapping call's own * ')' position AS `end` -- so the literal's closing quote landing exactly * on that boundary was wrongly rejected as "unterminated". Every other * fixture in this file happens to follow `name:` with `dependencies:` or a * comma, so this specific shape was previously untested and unnoticed. */ TEST(pkgmap_swift_target_name_immediately_before_close_paren) { static const char src[] = "let package = Package(\n" " name: \"Core\",\n" " targets: [.target(name: \"Core\")]\n" ")\n"; cbm_pkg_entries_t entries; cbm_pkg_entries_init(&entries); bool ok = cbm_pkgmap_try_parse("Package.swift", "Core/Package.swift", src, (int)strlen(src), &entries); ASSERT_TRUE(ok); ASSERT_TRUE(pkg_entries_has_name(&entries, "Core")); ASSERT_STR_EQ(pkg_entries_entry_for(&entries, "Core"), "Core/Sources/Core"); ASSERT_EQ(entries.count, 1); cbm_pkg_entries_free(&entries); PASS(); } /* A literal `path:` argument overrides the Sources/ convention. */ TEST(pkgmap_swift_target_honors_literal_path) { static const char src[] = "let package = Package(\n" " name: \"Core\",\n" " targets: [.target(name: \"Core\", path: \"Vendor/CoreLegacy\")]\n" ")\n"; cbm_pkg_entries_t entries; cbm_pkg_entries_init(&entries); bool ok = cbm_pkgmap_try_parse("Package.swift", "Core/Package.swift", src, (int)strlen(src), &entries); ASSERT_TRUE(ok); ASSERT_TRUE(pkg_entries_has_name(&entries, "Core")); ASSERT_STR_EQ(pkg_entries_entry_for(&entries, "Core"), "Core/Vendor/CoreLegacy"); cbm_pkg_entries_free(&entries); PASS(); } /* A `path:` argument that IS present but not a bare literal (computed) is * unknowable -- SwiftPM would not use the Sources/ convention here, * so guessing it anyway would mint a location likely to be wrong. Skip the * target entirely (fail closed), even though its `name:` is a valid * literal. */ TEST(pkgmap_swift_target_computed_path_fails_closed) { static const char src[] = "let customPath = computePath()\n" "let package = Package(\n" " name: \"Core\",\n" " targets: [.target(name: \"Core\", path: customPath)]\n" ")\n"; cbm_pkg_entries_t entries; cbm_pkg_entries_init(&entries); bool ok = cbm_pkgmap_try_parse("Package.swift", "Core/Package.swift", src, (int)strlen(src), &entries); ASSERT_TRUE(ok); ASSERT_EQ(entries.count, 0); cbm_pkg_entries_free(&entries); PASS(); } /* A `.target(` spelled inside a `//` line comment, a nesting-aware * slash-star block comment, or a string literal must never be mistaken for a live * declaration -- the bug a raw strstr scan cannot avoid. Only the one real * target registers. */ TEST(pkgmap_swift_target_in_comment_or_string_not_registered) { static const char src[] = "// .target(name: \"Decoy\")\n" "/* outer /* nested */ still a comment: .target(name: \"NestedDecoy\") */\n" "let manifestSnippet = \".target(name: \\\"StringDecoy\\\")\"\n" "let package = Package(\n" " name: \"App\",\n" " targets: [.target(name: \"App\", dependencies: [])]\n" ")\n"; cbm_pkg_entries_t entries; cbm_pkg_entries_init(&entries); bool ok = cbm_pkgmap_try_parse("Package.swift", "App/Package.swift", src, (int)strlen(src), &entries); ASSERT_TRUE(ok); ASSERT_TRUE(pkg_entries_has_name(&entries, "App")); ASSERT_FALSE(pkg_entries_has_name(&entries, "Decoy")); ASSERT_FALSE(pkg_entries_has_name(&entries, "NestedDecoy")); ASSERT_FALSE(pkg_entries_has_name(&entries, "StringDecoy")); ASSERT_EQ(entries.count, 1); cbm_pkg_entries_free(&entries); PASS(); } /* Local path + remote url dependencies (`.package(path:)` / `.package(url:)`) * mint NO entries of their own -- mirroring package.json/Cargo.toml, only a * manifest's OWN products/targets self-register. A local sibling's name is * produced by ITS OWN Package.swift when the repo-wide walk reaches it * (see repro_issue408.c's JS-workspace analog); a remote dependency has no * local path to point at, so nothing is minted (fail-closed). */ TEST(pkgmap_swift_dependencies_do_not_leak_entries) { static const char src[] = "let package = Package(\n" " name: \"App\",\n" " dependencies: [\n" " .package(path: \"../Core\"),\n" " .package(url: \"https://github.com/example/RemoteKit.git\", from: \"1.0.0\")\n" " ],\n" " targets: [.target(name: \"App\", dependencies: [])]\n" ")\n"; cbm_pkg_entries_t entries; cbm_pkg_entries_init(&entries); bool ok = cbm_pkgmap_try_parse("Package.swift", "App/Package.swift", src, (int)strlen(src), &entries); ASSERT_TRUE(ok); ASSERT_TRUE(pkg_entries_has_name(&entries, "App")); ASSERT_FALSE(pkg_entries_has_name(&entries, "Core")); ASSERT_FALSE(pkg_entries_has_name(&entries, "RemoteKit")); ASSERT_EQ(entries.count, 1); cbm_pkg_entries_free(&entries); PASS(); } /* Only App itself (the declaring target) registers -- a bare same-package * target-name dependency ("Core") and a cross-package product dependency * (Utils/UtilsPkg) name OTHER modules, not this manifest's own * products/targets, so neither mints an entry. */ TEST(pkgmap_swift_target_name_dependency_does_not_leak_entry) { static const char src[] = "let package = Package(\n" " name: \"App\",\n" " targets: [.target(name: \"App\", dependencies: [\n" " \"Core\",\n" " .product(name: \"Utils\", package: \"UtilsPkg\")\n" " ])]\n" ")\n"; cbm_pkg_entries_t entries; cbm_pkg_entries_init(&entries); bool ok = cbm_pkgmap_try_parse("Package.swift", "App/Package.swift", src, (int)strlen(src), &entries); ASSERT_TRUE(ok); ASSERT_TRUE(pkg_entries_has_name(&entries, "App")); ASSERT_FALSE(pkg_entries_has_name(&entries, "Core")); ASSERT_FALSE(pkg_entries_has_name(&entries, "Utils")); ASSERT_FALSE(pkg_entries_has_name(&entries, "UtilsPkg")); ASSERT_EQ(entries.count, 1); cbm_pkg_entries_free(&entries); PASS(); } /* Fail-closed on any `name:` that is not a bare literal: a computed * variable, and a literal concatenated with a dynamic suffix (which a * naive quote-scan would wrongly accept as "App"). Neither mints an entry, * though the manifest is still recognized and parsed. */ TEST(pkgmap_swift_ambiguous_target_name_fails_closed) { static const char dynamic_src[] = "let generatedName = \"App\" + String(buildNumber)\n" "let package = Package(\n" " name: \"App\",\n" " targets: [.target(name: generatedName, dependencies: [])]\n" ")\n"; static const char concat_src[] = "let package = Package(\n" " name: \"App\",\n" " targets: [.target(name: \"App\" + suffix, dependencies: [])]\n" ")\n"; cbm_pkg_entries_t entries; cbm_pkg_entries_init(&entries); ASSERT_TRUE(cbm_pkgmap_try_parse("Package.swift", "App/Package.swift", dynamic_src, (int)strlen(dynamic_src), &entries)); ASSERT_EQ(entries.count, 0); cbm_pkg_entries_free(&entries); cbm_pkg_entries_init(&entries); ASSERT_TRUE(cbm_pkgmap_try_parse("Package.swift", "App/Package.swift", concat_src, (int)strlen(concat_src), &entries)); ASSERT_EQ(entries.count, 0); cbm_pkg_entries_free(&entries); PASS(); } /* The repo-wide manifest walker must also recognize "Package.swift" -- a * second code path (is_pkgmap_manifest_basename) from the direct * cbm_pkgmap_try_parse calls above. */ TEST(pkgmap_swift_scan_repo_finds_nested_manifest) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_pkgmap_swift_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); char dir[512]; snprintf(dir, sizeof(dir), "%s/Core", tmpdir); cbm_mkdir(dir); write_temp_file(tmpdir, "Core/Package.swift", "let package = Package(\n" " name: \"Core\",\n" " targets: [.target(name: \"Core\", dependencies: [])]\n" ")\n"); cbm_pkg_entries_t entries; cbm_pkg_entries_init(&entries); cbm_pkgmap_scan_repo(tmpdir, &entries, NULL, 0); ASSERT_TRUE(pkg_entries_has_name(&entries, "Core")); cbm_pkg_entries_free(&entries); th_rmtree(tmpdir); PASS(); } /* The pkgmap repo walk must honor discovery exclusions (issue #792: a * gitignored huge subtree kept the pkgmap walk busy for 15 minutes). * Control run first (no exclusions → BOTH manifests parsed) so the * exclusion assertion below cannot pass vacuously. */ TEST(pkgmap_scan_repo_honors_discovery_exclusions) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_pkgmap_excl_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); write_temp_file(tmpdir, "packages/app/package.json", "{\"name\":\"@org/app\",\"main\":\"index.js\"}\n"); write_temp_file(tmpdir, "vendor_big/lib/package.json", "{\"name\":\"@org/vendored\",\"main\":\"index.js\"}\n"); /* Control: NULL exclusion list — the walk reaches and parses BOTH * manifests (proves the excluded one is reachable + parseable). */ cbm_pkg_entries_t control; cbm_pkg_entries_init(&control); cbm_pkgmap_scan_repo(tmpdir, &control, NULL, 0); ASSERT_TRUE(pkg_entries_has_name(&control, "@org/app")); ASSERT_TRUE(pkg_entries_has_name(&control, "@org/vendored")); cbm_pkg_entries_free(&control); /* With vendor_big excluded (as discovery reports for a gitignored * subtree): the walk must not descend into it. */ char *excluded[] = {(char *)"vendor_big"}; cbm_pkg_entries_t entries; cbm_pkg_entries_init(&entries); cbm_pkgmap_scan_repo(tmpdir, &entries, excluded, 1); ASSERT_TRUE(pkg_entries_has_name(&entries, "@org/app")); ASSERT_FALSE(pkg_entries_has_name(&entries, "@org/vendored")); cbm_pkg_entries_free(&entries); th_rmtree(tmpdir); PASS(); } /* ── Git history tests (port of githistory_test.go) ────────────── */ /* Port of Go TestIsTrackableFile from githistory_test.go */ TEST(githistory_is_trackable_file) { /* Source files — trackable */ ASSERT_TRUE(cbm_is_trackable_file("main.go")); ASSERT_TRUE(cbm_is_trackable_file("src/app.py")); ASSERT_TRUE(cbm_is_trackable_file("README.md")); /* node_modules — not trackable */ ASSERT_FALSE(cbm_is_trackable_file("node_modules/foo/bar.js")); /* vendor — not trackable */ ASSERT_FALSE(cbm_is_trackable_file("vendor/lib/dep.go")); /* Lock files — not trackable */ ASSERT_FALSE(cbm_is_trackable_file("package-lock.json")); ASSERT_FALSE(cbm_is_trackable_file("go.sum")); /* Binary/assets — not trackable */ ASSERT_FALSE(cbm_is_trackable_file("image.png")); /* .git directory — not trackable */ ASSERT_FALSE(cbm_is_trackable_file(".git/config")); /* __pycache__ — not trackable */ ASSERT_FALSE(cbm_is_trackable_file("__pycache__/mod.pyc")); /* Minified files — not trackable */ ASSERT_FALSE(cbm_is_trackable_file("src/style.min.css")); PASS(); } /* Port of Go TestComputeChangeCoupling from githistory_test.go */ TEST(githistory_compute_change_coupling) { /* 5 commits: * aaa: a.go, b.go, c.go * bbb: a.go, b.go * ccc: a.go, b.go * ddd: a.go, c.go * eee: d.go, e.go */ char *files_aaa[] = {"a.go", "b.go", "c.go"}; char *files_bbb[] = {"a.go", "b.go"}; char *files_ccc[] = {"a.go", "b.go"}; char *files_ddd[] = {"a.go", "c.go"}; char *files_eee[] = {"d.go", "e.go"}; cbm_commit_files_t commits[5] = { {files_aaa, 3, 0}, {files_bbb, 2, 0}, {files_ccc, 2, 0}, {files_ddd, 2, 0}, {files_eee, 2, 0}, }; cbm_change_coupling_t out[100]; int count = cbm_compute_change_coupling(commits, 5, out, 100); /* a.go + b.go co-change 3 times → should be in results */ bool found_ab = false; for (int i = 0; i < count; i++) { if ((strcmp(out[i].file_a, "a.go") == 0 && strcmp(out[i].file_b, "b.go") == 0) || (strcmp(out[i].file_a, "b.go") == 0 && strcmp(out[i].file_b, "a.go") == 0)) { found_ab = true; ASSERT_EQ(out[i].co_change_count, 3); ASSERT(out[i].coupling_score >= 0.9); } } ASSERT_TRUE(found_ab); /* d.go + e.go co-change only 1 time → below threshold of 3 */ for (int i = 0; i < count; i++) { if (strcmp(out[i].file_a, "d.go") == 0 || strcmp(out[i].file_b, "d.go") == 0) { ASSERT(0); /* d.go should not appear */ } } PASS(); } /* Port of Go TestComputeChangeCouplingSkipsLargeCommits from githistory_test.go */ TEST(githistory_coupling_skips_large_commits) { /* 25 files in one commit → exceeds 20-file threshold */ char *files[25]; char bufs[25][32]; for (int i = 0; i < 25; i++) { snprintf(bufs[i], sizeof(bufs[i]), "file%d.go", i); files[i] = bufs[i]; } cbm_commit_files_t commits[1] = {{files, 25, 0}}; cbm_change_coupling_t out[100]; int count = cbm_compute_change_coupling(commits, 1, out, 100); ASSERT_EQ(count, 0); PASS(); } /* Port of Go TestComputeChangeCouplingLimitsTo100 from githistory_test.go */ TEST(githistory_coupling_limits_output) { /* Generate many small commits to create >100 couplings. * 50 files, each pair committed 3 times. max_out=100. */ int idx = 0; char *pair_files[2450][2]; /* 50*49/2 pairs * 3 repetitions = 3675 commits */ char pair_bufs[2450][2][32]; /* Zeroed: the loop sets only files/count, and cbm_compute_change_coupling * also reads each commit's timestamp (MSan found it uninitialized). */ cbm_commit_files_t commits[3675] = {0}; int ci = 0; for (int i = 0; i < 50 && ci < 3675; i++) { for (int j = i + 1; j < 50 && ci < 3675; j++) { for (int k = 0; k < 3 && ci < 3675; k++) { snprintf(pair_bufs[idx][0], 32, "f%d.go", i); snprintf(pair_bufs[idx][1], 32, "f%d.go", j); pair_files[idx][0] = pair_bufs[idx][0]; pair_files[idx][1] = pair_bufs[idx][1]; commits[ci].files = pair_files[idx]; commits[ci].count = 2; ci++; idx++; if (idx >= 2450) idx = 0; /* reuse buffer space */ } } } cbm_change_coupling_t out[200]; int count = cbm_compute_change_coupling(commits, ci, out, 100); ASSERT(count <= 100); PASS(); } /* Port of Go TestIsImportReachable from resolver_test.go */ TEST(registry_is_import_reachable) { const char *import_vals[] = {"proj.handler", "proj.shared.utils"}; /* Exact match: proj.handler.Process → true */ ASSERT_TRUE(cbm_registry_is_import_reachable("proj.handler.Process", import_vals, 2)); /* Sub-package: proj.handler.sub.Process → true (handler contains handler) */ ASSERT_TRUE(cbm_registry_is_import_reachable("proj.handler.sub.Process", import_vals, 2)); /* Nested match: proj.shared.utils.Helper → true */ ASSERT_TRUE(cbm_registry_is_import_reachable("proj.shared.utils.Helper", import_vals, 2)); /* Unrelated: proj.billing.Process → false */ ASSERT_FALSE(cbm_registry_is_import_reachable("proj.billing.Process", import_vals, 2)); /* Completely unrelated: unrelated.pkg.Func → false */ ASSERT_FALSE(cbm_registry_is_import_reachable("unrelated.pkg.Func", import_vals, 2)); PASS(); } /* Port of FindEndingWith portion from Go TestFunctionRegistry in pipeline_test.go */ TEST(registry_find_ending_with) { cbm_registry_t *reg = cbm_registry_new(); cbm_registry_add(reg, "Foo", "proj.pkg.Foo", "Function"); cbm_registry_add(reg, "Bar", "proj.pkg.Bar", "Function"); cbm_registry_add(reg, "Foo", "proj.other.Foo", "Function"); cbm_registry_add(reg, "transform", "proj.utils.DataProcessor.transform", "Method"); /* FindEndingWith "DataProcessor.transform" → 1 match */ const char **matches = NULL; int count = cbm_registry_find_ending_with(reg, "DataProcessor.transform", &matches); ASSERT_EQ(count, 1); ASSERT_STR_EQ(matches[0], "proj.utils.DataProcessor.transform"); free(matches); /* FindEndingWith "Foo" → 2 matches */ matches = NULL; count = cbm_registry_find_ending_with(reg, "Foo", &matches); ASSERT_EQ(count, 2); /* Both should be present (order may vary) */ bool found_pkg = false, found_other = false; for (int i = 0; i < count; i++) { if (strcmp(matches[i], "proj.pkg.Foo") == 0) found_pkg = true; if (strcmp(matches[i], "proj.other.Foo") == 0) found_other = true; } ASSERT_TRUE(found_pkg); ASSERT_TRUE(found_other); free(matches); /* FindEndingWith "Nonexistent" → 0 matches */ matches = NULL; count = cbm_registry_find_ending_with(reg, "Nonexistent", &matches); ASSERT_EQ(count, 0); cbm_registry_free(reg); PASS(); } /* ═══════════════════════════════════════════════════════════════════ * Incremental reindex tests * ═══════════════════════════════════════════════════════════════════ */ /* Helper: create a simple 2-file Go project for incremental tests */ 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)) { return -1; } snprintf(g_incr_dbpath, sizeof(g_incr_dbpath), "%s/test.db", g_incr_tmpdir); char path[512]; FILE *f; /* main.go — calls Helper() */ snprintf(path, sizeof(path), "%s/main.go", g_incr_tmpdir); f = fopen(path, "w"); if (!f) { return -1; } fprintf(f, "package main\n\nfunc main() {\n\tHelper()\n}\n"); fclose(f); /* helper.go — defines Helper() */ snprintf(path, sizeof(path), "%s/helper.go", g_incr_tmpdir); f = fopen(path, "w"); if (!f) { return -1; } fprintf(f, "package main\n\nfunc Helper() string {\n\treturn \"hello\"\n}\n"); fclose(f); return 0; } static void cleanup_incremental_repo(void) { th_rmtree(g_incr_tmpdir); } /* Atomic-publish cancellation seam. Production invokes this hook after the * staging database is complete, closed, and integrity-valid, but immediately * before it can replace the last committed database. Keeping the hook on the * pipeline instance avoids process-global failpoints and makes cancellation * tests deterministic even when test runners gain concurrency. */ extern void cbm_pipeline_set_before_publish_hook_for_tests( cbm_pipeline_t *p, void (*hook)(cbm_pipeline_t *, const char *, void *), void *ctx); typedef struct { const char *project; const char *candidate; char staging_path[768]; int calls; bool staging_existed; bool staging_was_valid; int staged_candidates; } publish_cancel_ctx_t; typedef struct { char staging_path[CBM_SZ_4K]; int calls; bool staging_was_valid; } publish_observe_ctx_t; static void observe_publish_boundary(cbm_pipeline_t *p, const char *staging_path, void *arg) { (void)p; publish_observe_ctx_t *ctx = (publish_observe_ctx_t *)arg; ctx->calls++; if (!staging_path) { return; } int n = snprintf(ctx->staging_path, sizeof(ctx->staging_path), "%s", staging_path); if (n < 0 || (size_t)n >= sizeof(ctx->staging_path)) { ctx->staging_path[0] = '\0'; return; } cbm_store_t *staging = cbm_store_open_path_existing(staging_path); if (staging) { ctx->staging_was_valid = cbm_store_check_integrity(staging); cbm_store_close(staging); } } typedef struct { int calls; } publish_rename_fail_ctx_t; static int fail_publish_rename(const char *staging_path, const char *final_path, void *arg) { publish_rename_fail_ctx_t *ctx = (publish_rename_fail_ctx_t *)arg; ctx->calls++; return staging_path && final_path ? CBM_NOT_FOUND : 0; } static bool pipeline_fixture_file_equals(const char *path, const char *expected) { FILE *f = cbm_fopen(path, "rb"); if (!f) { return false; } size_t expected_len = strlen(expected); char actual[64]; size_t n = fread(actual, 1, sizeof(actual), f); bool ok = n == expected_len && memcmp(actual, expected, expected_len) == 0 && fgetc(f) == EOF; (void)fclose(f); return ok; } static void cancel_at_publish_boundary(cbm_pipeline_t *p, const char *staging_path, void *arg) { publish_cancel_ctx_t *ctx = (publish_cancel_ctx_t *)arg; ctx->calls++; if (staging_path && staging_path[0]) { snprintf(ctx->staging_path, sizeof(ctx->staging_path), "%s", staging_path); struct stat st; ctx->staging_existed = stat(staging_path, &st) == 0; if (ctx->staging_existed) { cbm_store_t *staging = cbm_store_open_path(staging_path); if (staging) { ctx->staging_was_valid = cbm_store_check_integrity(staging); ctx->staged_candidates = count_nodes_named(staging, ctx->project, ctx->candidate); cbm_store_close(staging); } } } cbm_pipeline_cancel(p); } static bool sqlite_artifacts_absent(const char *db_path) { struct stat st; if (!db_path || !db_path[0] || stat(db_path, &st) == 0) { return false; } char sidecar[832]; snprintf(sidecar, sizeof(sidecar), "%s-wal", db_path); if (stat(sidecar, &st) == 0) { return false; } snprintf(sidecar, sizeof(sidecar), "%s-shm", db_path); if (stat(sidecar, &st) == 0) { return false; } snprintf(sidecar, sizeof(sidecar), "%s-journal", db_path); return stat(sidecar, &st) != 0; } static bool write_go_file(const char *dir, const char *name, const char *source) { char path[512]; snprintf(path, sizeof(path), "%s/%s", dir, name); FILE *f = fopen(path, "w"); if (!f) { return false; } bool ok = fputs(source, f) >= 0; fclose(f); return ok; } /* ═══════════════════════════════════════════════════════════════════ * FastAPI Depends() edge tracking (PR #66, fix #27) * ═══════════════════════════════════════════════════════════════════ */ TEST(pipeline_fastapi_depends_edges) { /* Depends(get_current_user) should produce a CALLS edge from the * endpoint to the dependency function. */ const char *files[] = {"auth.py", "routes.py"}; const char *contents[] = {/* auth.py: defines get_current_user */ "def get_current_user(token: str):\n" " return decode_token(token)\n", /* routes.py: endpoint depends on get_current_user */ "from fastapi import Depends\n" "from auth import get_current_user\n\n" "def get_profile(user = Depends(get_current_user)):\n" " return {\"user\": user}\n"}; if (setup_lang_repo(files, contents, 2) != 0) { FAIL("tmpdir"); } char db[512]; snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir); 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 *proj = cbm_pipeline_project_name(p); /* Check CALLS edges for fastapi_depends strategy */ cbm_edge_t *edges = NULL; int edge_count = 0; cbm_store_find_edges_by_type(s, proj, "CALLS", &edges, &edge_count); bool found_depends_edge = false; for (int i = 0; i < edge_count; i++) { if (edges[i].properties_json && strstr(edges[i].properties_json, "fastapi_depends")) { found_depends_edge = true; break; } } if (edges) { cbm_store_free_edges(edges, edge_count); } ASSERT_TRUE(found_depends_edge); cbm_store_close(s); cbm_pipeline_free(p); teardown_lang_repo(); PASS(); } /* DLL resolve test removed after the associated builds received a Windows * Defender Wacatac.B!ml verdict. The opaque verdict did not attribute the * result to this feature; see issue #89 for the historical observation. */ /* ═══════════════════════════════════════════════════════════════════ * Incremental reindex * ═══════════════════════════════════════════════════════════════════ */ TEST(incremental_full_then_noop) { /* Full index, then re-run → should detect no changes and skip */ if (setup_incremental_repo() != 0) { FAIL("setup failed"); } /* First: full index */ cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); /* Verify nodes exist */ cbm_store_t *s = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(s); int nodes_before = cbm_store_count_nodes(s, project); ASSERT_GT(nodes_before, 0); cbm_store_close(s); /* Second: incremental — nothing changed → should be no-op */ p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); s = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(s); int nodes_after = cbm_store_count_nodes(s, project); /* Node count should be same (no duplicates, no loss) */ ASSERT_EQ(nodes_after, nodes_before); cbm_store_close(s); free(project); cleanup_incremental_repo(); PASS(); } TEST(incremental_detects_changed_file) { /* Full index, modify one file, re-index → changed file re-parsed */ if (setup_incremental_repo() != 0) { FAIL("setup failed"); } /* First: full index */ cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); /* Modify helper.go — add a new function */ char path[512]; snprintf(path, sizeof(path), "%s/helper.go", g_incr_tmpdir); FILE *f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "package main\n\n" "func Helper() string {\n\treturn \"hello\"\n}\n\n" "func NewFunc() int {\n\treturn 42\n}\n"); fclose(f); /* Second: incremental — should detect change and re-index */ p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); /* Verify node count increased (NewFunc was added) */ cbm_store_t *s = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(s); int nodes_after = cbm_store_count_nodes(s, project); ASSERT_GT(nodes_after, 0); cbm_store_close(s); cbm_pipeline_free(p); free(project); cleanup_incremental_repo(); PASS(); } TEST(full_reindex_recovers_when_previous_coverage_is_unreadable) { if (setup_incremental_repo() != 0) { FAIL("setup failed"); } cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); cbm_store_t *s = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(s); int nodes_before = cbm_store_count_nodes(s, project); ASSERT_GT(nodes_before, 0); /* Simulate an unreadable prior coverage generation while leaving the * graph and file hashes healthy enough to otherwise run incrementally. */ ASSERT_EQ( cbm_store_exec(s, "ALTER TABLE index_coverage RENAME COLUMN detail TO broken_detail;"), CBM_STORE_OK); cbm_store_close(s); char path[512]; snprintf(path, sizeof(path), "%s/helper.go", g_incr_tmpdir); FILE *f = fopen(path, "a"); ASSERT_NOT_NULL(f); fprintf(f, "\nfunc MustNotBeIndexed() int { return 7 }\n"); fclose(f); p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); /* An exact-manifest delta routes to an isolated full generation, which * does not depend on the damaged coverage table and repairs it atomically. */ s = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(s); ASSERT_GT(cbm_store_count_nodes(s, project), nodes_before); cbm_node_t *recovered = NULL; int recovered_count = 0; ASSERT_EQ( cbm_store_find_nodes_by_name(s, project, "MustNotBeIndexed", &recovered, &recovered_count), CBM_STORE_OK); ASSERT_EQ(recovered_count, 1); cbm_store_free_nodes(recovered, recovered_count); cbm_store_close(s); free(project); cleanup_incremental_repo(); PASS(); } TEST(incremental_detects_deleted_file) { /* Full index, delete a file, re-index → deleted file's nodes removed */ if (setup_incremental_repo() != 0) { FAIL("setup failed"); } /* First: full index */ cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); /* Delete helper.go */ char path[512]; snprintf(path, sizeof(path), "%s/helper.go", g_incr_tmpdir); unlink(path); /* Second: incremental — should remove Helper nodes */ p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); /* Verify node count decreased (Helper's file was deleted) */ cbm_store_t *s = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(s); int nodes_after = cbm_store_count_nodes(s, project); ASSERT_GT(nodes_after, 0); /* still has main.go nodes */ cbm_store_close(s); cbm_pipeline_free(p); free(project); cleanup_incremental_repo(); PASS(); } TEST(incremental_new_file_added) { /* Full index, add a new file, re-index → new file's nodes appear */ if (setup_incremental_repo() != 0) { FAIL("setup failed"); } /* First: full index */ cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); /* Add extra.go */ char path[512]; snprintf(path, sizeof(path), "%s/extra.go", g_incr_tmpdir); FILE *f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "package main\n\nfunc Extra() bool {\n\treturn true\n}\n"); fclose(f); /* Second: incremental — should pick up Extra */ p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(s); int nodes_after = cbm_store_count_nodes(s, project); ASSERT_GT(nodes_after, 0); cbm_store_close(s); cbm_pipeline_free(p); free(project); cleanup_incremental_repo(); PASS(); } /* Cancellation at the final publish boundary must never destroy the last good * full index. Adding two files to the two-file baseline deliberately exceeds * the incremental router's 1.5x file-count bound (4 > 2 + 2/2), forcing the * full-reindex path while an existing committed DB is present. */ TEST(cancelled_full_reindex_preserves_committed_db) { if (setup_incremental_repo() != 0) { FAIL("setup failed"); } cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); ASSERT_NOT_NULL(project); cbm_store_t *live = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(live); ASSERT_TRUE(cbm_store_check_integrity(live)); int baseline_nodes = cbm_store_count_nodes(live, project); int baseline_helper = count_nodes_named(live, project, "Helper"); ASSERT_GT(baseline_nodes, 0); ASSERT_GT(baseline_helper, 0); ASSERT_EQ(count_nodes_named(live, project, "CandidateFull"), 0); cbm_store_close(live); ASSERT_TRUE(write_go_file(g_incr_tmpdir, "candidate_full.go", "package main\n\nfunc CandidateFull() int { return 41 }\n")); ASSERT_TRUE(write_go_file(g_incr_tmpdir, "force_full.go", "package main\n\nfunc ForceFullRoute() int { return 42 }\n")); publish_cancel_ctx_t hook = { .project = project, .candidate = "CandidateFull", }; p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); cbm_pipeline_set_before_publish_hook_for_tests(p, cancel_at_publish_boundary, &hook); int rc = cbm_pipeline_run(p); cbm_pipeline_free(p); live = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(live); bool live_valid = cbm_store_check_integrity(live); int nodes_after = cbm_store_count_nodes(live, project); int helper_after = count_nodes_named(live, project, "Helper"); int published_candidates = count_nodes_named(live, project, "CandidateFull"); cbm_store_close(live); bool staging_cleaned = sqlite_artifacts_absent(hook.staging_path); free(project); cleanup_incremental_repo(); ASSERT_EQ(hook.calls, 1); ASSERT_TRUE(hook.staging_existed); ASSERT_TRUE(hook.staging_was_valid); ASSERT_GT(hook.staged_candidates, 0); /* anti-vacuous: new full output was staged */ /* Cancelling at the publish boundary aborts before the rename, so the * committed database is untouched -- which is exactly what this code says. * It was a bare -1 while every publish failure collapsed to one value. */ ASSERT_EQ(rc, CBM_PIPELINE_ABORT_PRESERVE_DB); ASSERT_TRUE(live_valid); ASSERT_EQ(nodes_after, baseline_nodes); ASSERT_EQ(helper_after, baseline_helper); ASSERT_EQ(published_candidates, 0); ASSERT_TRUE(staging_cleaned); PASS(); } /* The same contract applies to the incremental path: the modified file is * present in a complete staging DB at the hook, but cancellation leaves the * prior committed snapshot queryable and removes every staging artifact. */ TEST(cancelled_incremental_reindex_preserves_committed_db) { if (setup_incremental_repo() != 0) { FAIL("setup failed"); } cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); ASSERT_NOT_NULL(project); cbm_store_t *live = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(live); ASSERT_TRUE(cbm_store_check_integrity(live)); int baseline_nodes = cbm_store_count_nodes(live, project); int baseline_helper = count_nodes_named(live, project, "Helper"); ASSERT_GT(baseline_nodes, 0); ASSERT_GT(baseline_helper, 0); ASSERT_EQ(count_nodes_named(live, project, "CandidateIncremental"), 0); cbm_store_close(live); ASSERT_TRUE(write_go_file(g_incr_tmpdir, "helper.go", "package main\n\n" "func Helper() string { return \"hello\" }\n\n" "func CandidateIncremental() int { return 43 }\n")); publish_cancel_ctx_t hook = { .project = project, .candidate = "CandidateIncremental", }; p = cbm_pipeline_new(g_incr_tmpdir, g_incr_dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); cbm_pipeline_set_before_publish_hook_for_tests(p, cancel_at_publish_boundary, &hook); int rc = cbm_pipeline_run(p); cbm_pipeline_free(p); live = cbm_store_open_path(g_incr_dbpath); ASSERT_NOT_NULL(live); bool live_valid = cbm_store_check_integrity(live); int nodes_after = cbm_store_count_nodes(live, project); int helper_after = count_nodes_named(live, project, "Helper"); int published_candidates = count_nodes_named(live, project, "CandidateIncremental"); cbm_store_close(live); bool staging_cleaned = sqlite_artifacts_absent(hook.staging_path); free(project); cleanup_incremental_repo(); ASSERT_EQ(hook.calls, 1); ASSERT_TRUE(hook.staging_existed); ASSERT_TRUE(hook.staging_was_valid); ASSERT_GT(hook.staged_candidates, 0); /* anti-vacuous: changed output was staged */ /* As above: an incremental run cancelled at the publish boundary preserves * the committed database, and now reports that specifically. */ ASSERT_EQ(rc, CBM_PIPELINE_ABORT_PRESERVE_DB); ASSERT_TRUE(live_valid); ASSERT_EQ(nodes_after, baseline_nodes); ASSERT_EQ(helper_after, baseline_helper); ASSERT_EQ(published_candidates, 0); ASSERT_TRUE(staging_cleaned); PASS(); } TEST(backup_failed_publish_failure_preserves_final_sidecars) { if (setup_incremental_repo() != 0) { FAIL("setup failed"); } char final_path[512]; char wal_path[544]; char shm_path[544]; char journal_path[544]; snprintf(final_path, sizeof(final_path), "%s/blocked.db", g_incr_tmpdir); snprintf(wal_path, sizeof(wal_path), "%s-wal", final_path); snprintf(shm_path, sizeof(shm_path), "%s-shm", final_path); snprintf(journal_path, sizeof(journal_path), "%s-journal", final_path); /* Invalid SQLite makes backup fail. The live sidecars may contain the only * recoverable state, so the rebuilt generation must be refused without * changing any member of the old database family. */ ASSERT_EQ(th_write_file(final_path, "corrupt-main"), 0); ASSERT_EQ(th_write_file(wal_path, "live-wal"), 0); ASSERT_EQ(th_write_file(shm_path, "live-shm"), 0); ASSERT_EQ(th_write_file(journal_path, "live-journal"), 0); publish_observe_ctx_t hook = {0}; cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, final_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); cbm_pipeline_set_before_publish_hook_for_tests(p, observe_publish_boundary, &hook); int rc = cbm_pipeline_run(p); cbm_pipeline_free(p); bool final_preserved = pipeline_fixture_file_equals(final_path, "corrupt-main"); bool wal_preserved = pipeline_fixture_file_equals(wal_path, "live-wal"); bool shm_preserved = pipeline_fixture_file_equals(shm_path, "live-shm"); bool journal_preserved = pipeline_fixture_file_equals(journal_path, "live-journal"); (void)cbm_unlink(final_path); (void)cbm_unlink(wal_path); (void)cbm_unlink(shm_path); (void)cbm_unlink(journal_path); cleanup_incremental_repo(); ASSERT_EQ(hook.calls, 1); ASSERT_TRUE(hook.staging_was_valid); ASSERT_TRUE(rc != 0); ASSERT_TRUE(final_preserved); ASSERT_TRUE(wal_preserved); ASSERT_TRUE(shm_preserved); ASSERT_TRUE(journal_preserved); PASS(); } TEST(backup_failed_rename_failure_preserves_corrupt_main) { if (setup_incremental_repo() != 0) { FAIL("setup failed"); } char final_path[512]; snprintf(final_path, sizeof(final_path), "%s/corrupt.db", g_incr_tmpdir); ASSERT_EQ(th_write_file(final_path, "corrupt-main-before-rename"), 0); publish_observe_ctx_t observe = {0}; publish_rename_fail_ctx_t rename_fail = {0}; cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, final_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); cbm_pipeline_set_before_publish_hook_for_tests(p, observe_publish_boundary, &observe); cbm_pipeline_set_rename_hook_for_tests(p, fail_publish_rename, &rename_fail); int rc = cbm_pipeline_run(p); cbm_pipeline_free(p); bool final_preserved = pipeline_fixture_file_equals(final_path, "corrupt-main-before-rename"); (void)cbm_unlink(final_path); cleanup_incremental_repo(); ASSERT_EQ(observe.calls, 1); ASSERT_TRUE(observe.staging_was_valid); ASSERT_EQ(rename_fail.calls, 1); ASSERT_TRUE(rc != 0); ASSERT_TRUE(final_preserved); PASS(); } #ifdef __linux__ static void cleanup_long_db_fixture(char *deep_dir, const char *root, const char *db_path, const char *staging_path) { (void)cbm_unlink(db_path); (void)cbm_remove_db_sidecars(db_path); /* The unfixed full-dump path writes to the first 1023 bytes of the * staging name. Remove that sibling so the RED test cleans up after * itself as well as the fixed implementation. */ if (staging_path && strlen(staging_path) >= CBM_SZ_1K) { char truncated[CBM_SZ_1K]; memcpy(truncated, staging_path, sizeof(truncated) - 1); truncated[sizeof(truncated) - 1] = '\0'; (void)cbm_unlink(truncated); (void)cbm_remove_db_sidecars(truncated); } size_t root_len = strlen(root); while (strlen(deep_dir) > root_len) { (void)cbm_rmdir(deep_dir); char *slash = strrchr(deep_dir, '/'); if (!slash) { break; } *slash = '\0'; } (void)cbm_rmdir(root); } TEST(full_reindex_preserves_exact_long_db_path) { if (setup_incremental_repo() != 0) { FAIL("setup failed"); } char *created_root = th_mktempdir("cbm_long_db"); ASSERT_NOT_NULL(created_root); char root[256]; snprintf(root, sizeof(root), "%s", created_root); char deep_dir[1600]; snprintf(deep_dir, sizeof(deep_dir), "%s", root); static const char component[] = "/segment_abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"; while (strlen(deep_dir) < 1100) { size_t used = strlen(deep_dir); ASSERT_TRUE(used + sizeof(component) < sizeof(deep_dir)); memcpy(deep_dir + used, component, sizeof(component)); } ASSERT_TRUE(cbm_mkdir_p(deep_dir, 0755)); char db_path[1600]; int db_n = snprintf(db_path, sizeof(db_path), "%s/graph.db", deep_dir); ASSERT_TRUE(db_n > CBM_SZ_1K && (size_t)db_n < sizeof(db_path)); publish_observe_ctx_t hook = {0}; cbm_pipeline_t *p = cbm_pipeline_new(g_incr_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); char *project = strdup(cbm_pipeline_project_name(p)); ASSERT_NOT_NULL(project); cbm_pipeline_set_before_publish_hook_for_tests(p, observe_publish_boundary, &hook); int rc = cbm_pipeline_run(p); cbm_pipeline_free(p); int node_count = -1; cbm_store_t *published = rc == 0 ? cbm_store_open_path_existing(db_path) : NULL; if (published) { node_count = cbm_store_count_nodes(published, project); cbm_store_close(published); } bool stray_truncated_db = false; if (strlen(hook.staging_path) >= CBM_SZ_1K) { char truncated[CBM_SZ_1K]; memcpy(truncated, hook.staging_path, sizeof(truncated) - 1); truncated[sizeof(truncated) - 1] = '\0'; stray_truncated_db = access(truncated, F_OK) == 0; } free(project); cleanup_long_db_fixture(deep_dir, root, db_path, hook.staging_path); cleanup_incremental_repo(); ASSERT_EQ(hook.calls, 1); ASSERT_TRUE(strlen(hook.staging_path) >= CBM_SZ_1K); ASSERT_EQ(rc, 0); ASSERT_GT(node_count, 0); ASSERT_FALSE(stray_truncated_db); PASS(); } #endif TEST(incremental_downgrade_preserves_scope_and_artifact_across_change_noop_delete) { char tmpdir[256]; char artifact_tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_mode_scope_XXXXXX"); snprintf(artifact_tmpdir, sizeof(artifact_tmpdir), "/tmp/cbm_mode_artifact_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmpdir)); ASSERT_NOT_NULL(cbm_mkdtemp(artifact_tmpdir)); char dbpath[512]; char cancelled_import_path[512]; char retry_import_path[512]; char deleted_import_path[512]; snprintf(dbpath, sizeof(dbpath), "%s/test.db", tmpdir); snprintf(cancelled_import_path, sizeof(cancelled_import_path), "%s/cancelled.db", artifact_tmpdir); snprintf(retry_import_path, sizeof(retry_import_path), "%s/retry.db", artifact_tmpdir); snprintf(deleted_import_path, sizeof(deleted_import_path), "%s/deleted.db", artifact_tmpdir); ASSERT_EQ(th_write_file(TH_PATH(tmpdir, "main.go"), "package main\n\nfunc main() {}\n"), 0); ASSERT_TRUE(cbm_mkdir_p(TH_PATH(tmpdir, "tools"), 0755)); ASSERT_EQ(th_write_file(TH_PATH(tmpdir, "tools/util.go"), "package tools\n\nfunc StoredBefore() string { return \"old\" }\n"), 0); cbm_pipeline_t *pipeline = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(pipeline); cbm_pipeline_set_persistence(pipeline, true); ASSERT_EQ(cbm_pipeline_run(pipeline), 0); char *project = strdup(cbm_pipeline_project_name(pipeline)); ASSERT_NOT_NULL(project); cbm_pipeline_free(pipeline); ASSERT_TRUE(cbm_artifact_exists(tmpdir)); ASSERT_EQ(th_write_file(TH_PATH(tmpdir, "tools/util.go"), "package tools\n\nfunc StoredAfter() string { return \"new\" }\n"), 0); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_incremental_test_cancel_after_predump_once(); observed_fast_run_t cancelled = run_observed_fast_pipeline(tmpdir, dbpath); int cancelled_live_before; int cancelled_live_after; observe_named_generation(dbpath, project, "StoredBefore", "StoredAfter", &cancelled_live_before, &cancelled_live_after); imported_generation_t cancelled_artifact = import_artifact_generation(tmpdir, cancelled_import_path, project); cbm_pipeline_incremental_test_reset_faults(); observed_fast_run_t retry = run_observed_fast_pipeline(tmpdir, dbpath); int retry_live_before; int retry_live_after; observe_named_generation(dbpath, project, "StoredBefore", "StoredAfter", &retry_live_before, &retry_live_after); bool retry_full_mode = stored_mode_is_full(dbpath, project); imported_generation_t retry_artifact = import_artifact_generation(tmpdir, retry_import_path, project); cbm_pipeline_incremental_test_reset_faults(); observed_fast_run_t noop = run_observed_fast_pipeline(tmpdir, dbpath); ASSERT_EQ(cbm_unlink(TH_PATH(tmpdir, "tools/util.go")), 0); cbm_pipeline_incremental_test_reset_faults(); observed_fast_run_t deleted = run_observed_fast_pipeline(tmpdir, dbpath); int deleted_live_before; int deleted_live_after; observe_named_generation(dbpath, project, "StoredBefore", "StoredAfter", &deleted_live_before, &deleted_live_after); bool delete_full_mode = stored_mode_is_full(dbpath, project); cbm_store_t *store = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(store); cbm_file_hash_t deleted_hash = {0}; int deleted_hash_rc = cbm_store_get_file_hash(store, project, "tools/util.go", &deleted_hash); cbm_store_clear_file_hash(&deleted_hash); cbm_store_close(store); imported_generation_t deleted_artifact = import_artifact_generation(tmpdir, deleted_import_path, project); cbm_pipeline_incremental_test_reset_faults(); free(project); th_rmtree(artifact_tmpdir); th_rmtree(tmpdir); ASSERT_EQ(cancelled.rc, CBM_PIPELINE_ABORT_PRESERVE_DB); ASSERT_EQ(cancelled.route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_TRUE(cancelled.tools_excluded); ASSERT_EQ(cancelled_live_before, 1); ASSERT_EQ(cancelled_live_after, 0); ASSERT_EQ(cancelled.publish.rename_calls, 0); ASSERT_EQ(cancelled.publish.export_count, 0); ASSERT_EQ(cancelled_artifact.rc, 0); ASSERT_EQ(cancelled_artifact.before_nodes, 1); ASSERT_EQ(cancelled_artifact.after_nodes, 0); ASSERT_EQ(retry.rc, 0); ASSERT_EQ(retry.route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_TRUE(retry.tools_excluded); ASSERT_TRUE(retry_full_mode); ASSERT_EQ(retry_live_before, 0); ASSERT_EQ(retry_live_after, 1); ASSERT_EQ(retry.publish.rename_calls, 1); ASSERT_EQ(retry.publish.exports_before_publish, 0); ASSERT_EQ(retry.publish.export_count, 1); ASSERT_EQ(retry_artifact.rc, 0); ASSERT_EQ(retry_artifact.before_nodes, 0); ASSERT_EQ(retry_artifact.after_nodes, 1); ASSERT_EQ(noop.rc, 0); ASSERT_EQ(noop.route, CBM_INCREMENTAL_ROUTE_NOOP); ASSERT_TRUE(noop.tools_excluded); /* Proven on the live generation: nothing is published, the artifact is * still refreshed from the live database. */ ASSERT_EQ(noop.publish.rename_calls, 0); ASSERT_EQ(noop.publish.export_count, 1); ASSERT_EQ(deleted.rc, 0); ASSERT_EQ(deleted.route, CBM_INCREMENTAL_ROUTE_FORCED_FULL); ASSERT_TRUE(deleted.tools_excluded); ASSERT_EQ(deleted.publish.rename_calls, 1); ASSERT_EQ(deleted.publish.exports_before_publish, 0); ASSERT_EQ(deleted.publish.export_count, 1); ASSERT_EQ(deleted_hash_rc, CBM_STORE_NOT_FOUND); ASSERT_TRUE(delete_full_mode); ASSERT_EQ(deleted_live_before, 0); ASSERT_EQ(deleted_live_after, 0); ASSERT_EQ(deleted_artifact.rc, 0); ASSERT_EQ(deleted_artifact.before_nodes, 0); ASSERT_EQ(deleted_artifact.after_nodes, 0); PASS(); } TEST(incremental_fast_preserves_mode_skipped_tools_dir) { /* Regression: 2026-04-13. A fast-mode reindex after a full-mode index * was silently destroying every file under FAST_SKIP_DIRS directories * (`tools`, `scripts`, `bin`, `build`, `docs`, ...) by classifying them * as deleted in find_deleted_files even though they still existed on * disk. The Skyline graph lost packages/mcp/src/tools/ (18 files / ~500 * nodes) mid-session when a concurrent /develop run obediently called * mode='fast'. This test pins the additive semantics: lesser-mode * reindexes must NOT delete files that are merely outside the current * pass's discovery scope. Fix: find_deleted_files now stat()s each * stored-but-missing file and only purges it if it is truly absent * from disk. */ char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_modeskip_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) { FAIL("tmpdir"); } char dbpath[512]; snprintf(dbpath, sizeof(dbpath), "%s/test.db", tmpdir); char path[512]; FILE *f; /* main.go — root-level production code (visible in fast and full) */ snprintf(path, sizeof(path), "%s/main.go", tmpdir); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "package main\n\nfunc main() {\n}\n"); fclose(f); /* tools/util.go — production code under a FAST_SKIP_DIRS directory. * Full mode indexes it; fast mode skips it via the discover.c heuristic. */ char tools_dir[512]; snprintf(tools_dir, sizeof(tools_dir), "%s/tools", tmpdir); cbm_mkdir_p(tools_dir, 0755); snprintf(path, sizeof(path), "%s/tools/util.go", tmpdir); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "package tools\n\nfunc Util() string {\n\treturn \"u\"\n}\n"); fclose(f); /* Step 1: full-mode index — both files should be present */ cbm_pipeline_t *p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); cbm_store_t *s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); cbm_node_t *tools_nodes_before = NULL; int tools_count_before = 0; cbm_store_find_nodes_by_file(s, project, "tools/util.go", &tools_nodes_before, &tools_count_before); ASSERT_GT(tools_count_before, 0); /* full mode must see tools/util.go */ cbm_store_free_nodes(tools_nodes_before, tools_count_before); int total_before = cbm_store_count_nodes(s, project); cbm_store_close(s); /* Step 2: fast-mode reindex — tools/util.go MUST survive (additive semantics) */ p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FAST); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); cbm_node_t *tools_nodes_after = NULL; int tools_count_after = 0; cbm_store_find_nodes_by_file(s, project, "tools/util.go", &tools_nodes_after, &tools_count_after); /* The critical assertion: tools/util.go nodes must still be present after * a fast-mode reindex that skipped the tools/ directory. Before the fix, * this was 0. */ ASSERT_GT(tools_count_after, 0); ASSERT_EQ(tools_count_after, tools_count_before); /* same nodes, untouched */ cbm_store_free_nodes(tools_nodes_after, tools_count_after); /* Sanity: total node count should not have collapsed by ~the size of tools/ */ int total_after = cbm_store_count_nodes(s, project); ASSERT_GTE(total_after, total_before); /* additive — never less */ cbm_store_close(s); /* Step 3: mutate main.go and fast reindex — forces dump_and_persist to * run (instead of the noop early-return path that step 2 hit). This is * the real dangerous path: the gbuf gets loaded, mutated for main.go, * dumped back to disk. tools/util.go must survive THAT cycle, not just * the trivial noop path. Audit finding from 2026-04-13. */ snprintf(path, sizeof(path), "%s/main.go", tmpdir); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "package main\n\nfunc main() {\n\tprintln(\"changed\")\n}\n"); fclose(f); /* Bump mtime explicitly — some filesystems have coarse mtime resolution * and the rewrite could land in the same tick as the original write. */ #ifndef _WIN32 struct stat mst; if (stat(path, &mst) == 0) { struct timespec times[2]; times[0].tv_sec = mst.st_atime; times[0].tv_nsec = 0; times[1].tv_sec = mst.st_mtime + 5; times[1].tv_nsec = 0; utimensat(AT_FDCWD, path, times, 0); } #endif /* !_WIN32 */ p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FAST); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); cbm_node_t *tools_nodes_run3 = NULL; int tools_count_run3 = 0; cbm_store_find_nodes_by_file(s, project, "tools/util.go", &tools_nodes_run3, &tools_count_run3); /* tools/util.go nodes must STILL be present after a fast reindex that * actually ran the full dump_and_persist cycle (not the noop fast-path). */ ASSERT_EQ(tools_count_run3, tools_count_before); cbm_store_free_nodes(tools_nodes_run3, tools_count_run3); cbm_store_close(s); /* Step 4: actually delete tools/util.go from disk and full-reindex. * Now it really is gone, so its nodes should be purged. This pins the * other half of the contract: the stat-based check correctly identifies * truly-deleted files as deleted. */ snprintf(path, sizeof(path), "%s/tools/util.go", tmpdir); unlink(path); p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); cbm_node_t *tools_nodes_deleted = NULL; int tools_count_deleted = 0; cbm_store_find_nodes_by_file(s, project, "tools/util.go", &tools_nodes_deleted, &tools_count_deleted); ASSERT_EQ(tools_count_deleted, 0); /* truly deleted → purged */ cbm_store_free_nodes(tools_nodes_deleted, tools_count_deleted); cbm_store_close(s); free(project); th_rmtree(tmpdir); PASS(); } TEST(incremental_k8s_manifest_indexed) { /* Full index with a k8s manifest, then add a new manifest via incremental. * Verifies that cbm_pipeline_pass_k8s() runs during incremental re-index. */ char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_k8s_incr_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) { FAIL("tmpdir"); } char dbpath[512]; snprintf(dbpath, sizeof(dbpath), "%s/test.db", tmpdir); char path[512]; FILE *f; /* Initial manifest */ snprintf(path, sizeof(path), "%s/deploy.yaml", tmpdir); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: my-app\n"); fclose(f); /* Full index */ cbm_pipeline_t *p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); /* Verify Resource node created by full index */ cbm_store_t *s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); cbm_node_t *nodes = NULL; int count = 0; cbm_store_find_nodes_by_label(s, project, "Resource", &nodes, &count); ASSERT_GT(count, 0); cbm_store_free_nodes(nodes, count); cbm_store_close(s); /* Add a second manifest — incremental should pick it up */ snprintf(path, sizeof(path), "%s/svc.yaml", tmpdir); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "apiVersion: v1\nkind: Service\nmetadata:\n name: my-svc\n"); fclose(f); /* Incremental re-index */ p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); /* Verify both Resource nodes now present */ s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); nodes = NULL; count = 0; cbm_store_find_nodes_by_label(s, project, "Resource", &nodes, &count); ASSERT_GTE(count, 2); cbm_store_free_nodes(nodes, count); cbm_store_close(s); free(project); th_rmtree(tmpdir); PASS(); } TEST(incremental_kustomize_entries_indexed) { /* Verifies that a kustomization.yaml added after the initial full index * is indexed incrementally: its entry becomes an IMPORTS edge to the * manifest it names. */ char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_kust_incr_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) { FAIL("tmpdir"); } char dbpath[512]; snprintf(dbpath, sizeof(dbpath), "%s/test.db", tmpdir); char path[512]; FILE *f; /* Initial resource manifest (gives full index something to find) */ snprintf(path, sizeof(path), "%s/deploy.yaml", tmpdir); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: my-app\n"); fclose(f); /* Full index */ cbm_pipeline_t *p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); char *project = strdup(cbm_pipeline_project_name(p)); cbm_pipeline_free(p); /* Add kustomization.yaml */ snprintf(path, sizeof(path), "%s/kustomization.yaml", tmpdir); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "apiVersion: kustomize.config.k8s.io/v1beta1\n" "kind: Kustomization\n" "resources:\n" " - deploy.yaml\n"); fclose(f); /* Incremental re-index */ p = cbm_pipeline_new(tmpdir, dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); /* Verify the overlay's entry was resolved: kustomization.yaml IMPORTS deploy.yaml */ cbm_store_t *s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); cbm_edge_t *edges = NULL; int edge_count = 0; cbm_store_find_edges_by_type(s, project, "IMPORTS", &edges, &edge_count); bool found_kust = false; for (int i = 0; i < edge_count && !found_kust; i++) { cbm_node_t src = {0}; cbm_node_t tgt = {0}; if (cbm_store_find_node_by_id(s, edges[i].source_id, &src) == CBM_STORE_OK && cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK) { found_kust = src.file_path && strcmp(src.file_path, "kustomization.yaml") == 0 && tgt.file_path && strcmp(tgt.file_path, "deploy.yaml") == 0; } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); } if (edges) { cbm_store_free_edges(edges, edge_count); } cbm_store_close(s); ASSERT_TRUE(found_kust); free(project); th_rmtree(tmpdir); PASS(); } /* ── Index lock tests ───────────────────────────────────────────── */ TEST(pipeline_lock_try_acquire) { /* First try-lock should succeed */ ASSERT_TRUE(cbm_pipeline_try_lock()); /* Second try-lock should fail (already held) */ ASSERT_FALSE(cbm_pipeline_try_lock()); /* Release, then re-acquire should succeed */ cbm_pipeline_unlock(); ASSERT_TRUE(cbm_pipeline_try_lock()); cbm_pipeline_unlock(); PASS(); } TEST(pipeline_lock_blocking) { /* Lock, then unlock — basic sanity */ cbm_pipeline_lock(); cbm_pipeline_unlock(); /* Should be immediately re-acquirable */ cbm_pipeline_lock(); cbm_pipeline_unlock(); PASS(); } /* Thread function that tries to acquire the lock and records result */ static atomic_int g_thread_acquired = 0; static atomic_int g_thread_done = 0; static void *try_lock_thread(void *arg) { (void)arg; if (cbm_pipeline_try_lock()) { atomic_store(&g_thread_acquired, 1); cbm_pipeline_unlock(); } else { atomic_store(&g_thread_acquired, 0); } atomic_store(&g_thread_done, 1); return NULL; } TEST(pipeline_lock_contention) { /* Main thread holds lock, spawned thread should fail try_lock */ cbm_pipeline_lock(); atomic_store(&g_thread_acquired, -1); atomic_store(&g_thread_done, 0); cbm_thread_t tid; int rc = cbm_thread_create(&tid, 0, try_lock_thread, NULL); ASSERT_EQ(rc, 0); /* Wait for thread to finish */ cbm_thread_join(&tid); /* Thread should NOT have acquired the lock */ ASSERT_EQ(atomic_load(&g_thread_acquired), 0); cbm_pipeline_unlock(); PASS(); } TEST(pipeline_lock_release_allows_contender) { /* Main thread acquires and releases, then spawned thread should succeed */ cbm_pipeline_lock(); cbm_pipeline_unlock(); atomic_store(&g_thread_acquired, -1); atomic_store(&g_thread_done, 0); cbm_thread_t tid; int rc = cbm_thread_create(&tid, 0, try_lock_thread, NULL); ASSERT_EQ(rc, 0); cbm_thread_join(&tid); /* Thread SHOULD have acquired the lock */ ASSERT_EQ(atomic_load(&g_thread_acquired), 1); PASS(); } /* ── Resource management & internal helper tests ─────────────────── */ TEST(pipeline_empty_path) { /* Empty string repo path — should handle gracefully */ cbm_pipeline_t *p = cbm_pipeline_new("", NULL, CBM_MODE_FULL); /* Implementation may return NULL or a valid pipeline with empty project name. * Either behavior is acceptable — the key is no crash. */ if (p) { cbm_pipeline_free(p); } PASS(); } TEST(pipeline_project_name_content) { /* Verify project name is derived from the repo_path */ cbm_pipeline_t *p = cbm_pipeline_new("/home/user/my-project", NULL, CBM_MODE_FULL); ASSERT_NOT_NULL(p); const char *name = cbm_pipeline_project_name(p); ASSERT_NOT_NULL(name); ASSERT_TRUE(strlen(name) > 0); /* Should contain "my-project" as part of the derived name */ ASSERT_TRUE(strstr(name, "my-project") != NULL); cbm_pipeline_free(p); PASS(); } TEST(pipeline_cancel_sets_flag) { /* Verify cancel sets the flag so subsequent run exits early */ cbm_pipeline_t *p = cbm_pipeline_new("/tmp/nonexistent", NULL, CBM_MODE_FULL); ASSERT_NOT_NULL(p); /* Cancel before run */ cbm_pipeline_cancel(p); /* Cancelled pipeline should return quickly (either -1 from cancel or from * missing path — both are acceptable; key is no hang) */ int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, -1); cbm_pipeline_free(p); PASS(); } TEST(pipeline_double_cancel) { /* Calling cancel twice should not crash */ cbm_pipeline_t *p = cbm_pipeline_new("/tmp/nonexistent", NULL, CBM_MODE_FULL); ASSERT_NOT_NULL(p); cbm_pipeline_cancel(p); cbm_pipeline_cancel(p); cbm_pipeline_free(p); PASS(); } TEST(pipeline_double_free_prevention) { /* free(NULL) after free should not crash. We can't truly double-free * the same pointer, but we verify NULL is safe as documented. */ cbm_pipeline_free(NULL); cbm_pipeline_free(NULL); PASS(); } TEST(trackable_source_files) { /* Common source extensions are trackable */ ASSERT_TRUE(cbm_is_trackable_file("main.go")); ASSERT_TRUE(cbm_is_trackable_file("src/handler.py")); ASSERT_TRUE(cbm_is_trackable_file("lib/server.js")); ASSERT_TRUE(cbm_is_trackable_file("components/App.tsx")); ASSERT_TRUE(cbm_is_trackable_file("pkg/svc.rs")); ASSERT_TRUE(cbm_is_trackable_file("config.yaml")); PASS(); } TEST(trackable_rejects_images) { /* Image and binary files are not trackable */ ASSERT_FALSE(cbm_is_trackable_file("logo.png")); ASSERT_FALSE(cbm_is_trackable_file("photo.jpg")); ASSERT_FALSE(cbm_is_trackable_file("icon.gif")); ASSERT_FALSE(cbm_is_trackable_file("diagram.svg")); PASS(); } TEST(trackable_rejects_minified) { /* Minified files are not trackable */ ASSERT_FALSE(cbm_is_trackable_file("bundle.min.js")); ASSERT_FALSE(cbm_is_trackable_file("style.min.css")); PASS(); } TEST(trackable_rejects_vendor_dirs) { /* Files in vendor/node_modules/__pycache__ are not trackable */ ASSERT_FALSE(cbm_is_trackable_file("vendor/github.com/lib/dep.go")); ASSERT_FALSE(cbm_is_trackable_file("node_modules/lodash/index.js")); ASSERT_FALSE(cbm_is_trackable_file("__pycache__/module.pyc")); ASSERT_FALSE(cbm_is_trackable_file(".git/objects/pack/data")); PASS(); } TEST(trackable_rejects_lock_files) { /* Lock files are not trackable */ ASSERT_FALSE(cbm_is_trackable_file("package-lock.json")); ASSERT_FALSE(cbm_is_trackable_file("go.sum")); ASSERT_FALSE(cbm_is_trackable_file("yarn.lock")); PASS(); } TEST(test_path_directory_patterns) { /* Test directory patterns: __tests__, test/, tests/, spec/ */ ASSERT_TRUE(cbm_is_test_path("__tests__/Component.js")); ASSERT_TRUE(cbm_is_test_path("tests/test_main.py")); ASSERT_TRUE(cbm_is_test_path("spec/handler_spec.rb")); PASS(); } TEST(test_path_suffix_patterns) { /* Various language-specific test suffix patterns */ ASSERT_TRUE(cbm_is_test_path("handler.spec.ts")); ASSERT_TRUE(cbm_is_test_path("handler.test.tsx")); ASSERT_TRUE(cbm_is_test_path("handler_test.go")); ASSERT_TRUE(cbm_is_test_path("test_handler.py")); /* Non-test files */ ASSERT_FALSE(cbm_is_test_path("handler.ts")); ASSERT_FALSE(cbm_is_test_path("contest.go")); ASSERT_FALSE(cbm_is_test_path("latest.py")); PASS(); } TEST(test_func_name_go_patterns) { /* Go test function names: Test + uppercase char */ ASSERT_TRUE(cbm_is_test_func_name("TestFoo")); ASSERT_TRUE(cbm_is_test_func_name("TestHTTPHandler")); /* Non-test: "Test" alone or Test + lowercase */ ASSERT_FALSE(cbm_is_test_func_name("Testable")); /* lowercase 'a' after Test */ PASS(); } TEST(test_func_name_js_helpers) { /* JS/TS test helper function names */ ASSERT_TRUE(cbm_is_test_func_name("it")); ASSERT_TRUE(cbm_is_test_func_name("describe")); ASSERT_TRUE(cbm_is_test_func_name("test")); ASSERT_TRUE(cbm_is_test_func_name("beforeEach")); ASSERT_TRUE(cbm_is_test_func_name("afterEach")); PASS(); } TEST(env_var_name_valid) { /* Valid env var names: uppercase + underscores, at least 2 chars with uppercase */ ASSERT_TRUE(cbm_is_env_var_name("DATABASE_URL")); ASSERT_TRUE(cbm_is_env_var_name("API_KEY")); ASSERT_TRUE(cbm_is_env_var_name("PORT")); ASSERT_TRUE(cbm_is_env_var_name("DB_2")); ASSERT_TRUE(cbm_is_env_var_name("MY_VAR_123")); PASS(); } TEST(env_var_name_invalid) { /* Invalid: too short, lowercase, mixed case, empty */ ASSERT_FALSE(cbm_is_env_var_name("A")); /* single char */ ASSERT_FALSE(cbm_is_env_var_name("port")); /* lowercase */ ASSERT_FALSE(cbm_is_env_var_name("apiKey")); /* camelCase */ ASSERT_FALSE(cbm_is_env_var_name("__")); /* no uppercase */ ASSERT_FALSE(cbm_is_env_var_name("")); /* empty */ ASSERT_FALSE(cbm_is_env_var_name("123")); /* digits only */ PASS(); } TEST(config_ext_positive) { /* Config file extensions recognized */ ASSERT_TRUE(cbm_has_config_extension(".env")); ASSERT_TRUE(cbm_has_config_extension("config.yaml")); ASSERT_TRUE(cbm_has_config_extension("config.yml")); ASSERT_TRUE(cbm_has_config_extension("settings.toml")); ASSERT_TRUE(cbm_has_config_extension("app.ini")); ASSERT_TRUE(cbm_has_config_extension("data.json")); ASSERT_TRUE(cbm_has_config_extension("app.properties")); PASS(); } TEST(config_ext_negative) { /* Non-config extensions rejected */ ASSERT_FALSE(cbm_has_config_extension("main.go")); ASSERT_FALSE(cbm_has_config_extension("app.py")); ASSERT_FALSE(cbm_has_config_extension("handler.rs")); ASSERT_FALSE(cbm_has_config_extension("data.csv")); ASSERT_FALSE(cbm_has_config_extension("README.md")); PASS(); } TEST(split_camel_basic) { /* Basic camelCase splitting */ char *parts[16]; int n; n = cbm_split_camel_case("getCamelCase", parts, 16); ASSERT_EQ(n, 3); ASSERT_STR_EQ(parts[0], "get"); ASSERT_STR_EQ(parts[1], "Camel"); ASSERT_STR_EQ(parts[2], "Case"); for (int i = 0; i < n; i++) free(parts[i]); PASS(); } TEST(split_camel_single_word) { /* Single lowercase word — no splits */ char *parts[16]; int n = cbm_split_camel_case("hello", parts, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(parts[0], "hello"); for (int i = 0; i < n; i++) free(parts[i]); PASS(); } TEST(split_camel_empty) { /* Empty string — should return 0 or 1 empty part */ char *parts[16]; int n = cbm_split_camel_case("", parts, 16); for (int i = 0; i < n; i++) free(parts[i]); /* Either 0 parts or 1 empty part is acceptable */ ASSERT_TRUE(n >= 0 && n <= 1); PASS(); } TEST(tokenize_decorator_login_required) { /* @login_required → ["login", "required"] */ char *tokens[16]; int n = cbm_tokenize_decorator("@login_required", tokens, 16); ASSERT_EQ(n, 2); ASSERT_STR_EQ(tokens[0], "login"); ASSERT_STR_EQ(tokens[1], "required"); for (int i = 0; i < n; i++) free(tokens[i]); PASS(); } TEST(tokenize_decorator_single) { /* @Override → ["override"] */ char *tokens[16]; int n = cbm_tokenize_decorator("@Override", tokens, 16); ASSERT_EQ(n, 1); ASSERT_STR_EQ(tokens[0], "override"); for (int i = 0; i < n; i++) free(tokens[i]); PASS(); } TEST(split_command_basic) { /* Basic command splitting with spaces */ char *args[16]; int n = cbm_split_command("gcc -c main.c -o main.o", args, 16); ASSERT_EQ(n, 5); ASSERT_STR_EQ(args[0], "gcc"); ASSERT_STR_EQ(args[1], "-c"); ASSERT_STR_EQ(args[2], "main.c"); ASSERT_STR_EQ(args[3], "-o"); ASSERT_STR_EQ(args[4], "main.o"); for (int i = 0; i < n; i++) free(args[i]); PASS(); } TEST(split_command_quoted) { /* Command with quoted arguments */ char *args[16]; int n = cbm_split_command("gcc -DFOO=\"bar baz\" main.c", args, 16); ASSERT_GTE(n, 3); ASSERT_STR_EQ(args[0], "gcc"); for (int i = 0; i < n; i++) free(args[i]); PASS(); } TEST(split_command_empty) { /* Empty command string */ char *args[16]; int n = cbm_split_command("", args, 16); ASSERT_EQ(n, 0); PASS(); } TEST(parse_range_with_count) { /* "10,5" → start=10, count=5 */ int start, count; cbm_parse_range("10,5", &start, &count); ASSERT_EQ(start, 10); ASSERT_EQ(count, 5); PASS(); } TEST(parse_range_single) { /* "42" → start=42, count=1 */ int start, count; cbm_parse_range("42", &start, &count); ASSERT_EQ(start, 42); ASSERT_EQ(count, 1); PASS(); } TEST(parse_range_zero_count) { /* "100,0" → start=100, count=0 */ int start, count; cbm_parse_range("100,0", &start, &count); ASSERT_EQ(start, 100); ASSERT_EQ(count, 0); PASS(); } TEST(parse_name_status_basic) { /* Parse standard git diff --name-status output */ const char *input = "M\tsrc/main.go\n" "A\tsrc/new.go\n" "D\tsrc/old.go\n"; cbm_changed_file_t files[16]; int n = cbm_parse_name_status(input, files, 16); /* Exact count depends on which files pass cbm_is_trackable_file */ ASSERT_GTE(n, 0); ASSERT_LTE(n, 3); PASS(); } TEST(parse_name_status_empty) { /* Empty input */ cbm_changed_file_t files[16]; int n = cbm_parse_name_status("", files, 16); ASSERT_EQ(n, 0); PASS(); } TEST(parse_hunks_empty) { /* Empty input */ cbm_changed_hunk_t hunks[16]; int n = cbm_parse_hunks("", hunks, 16); ASSERT_EQ(n, 0); PASS(); } TEST(coupling_empty_commits) { /* Zero commits → zero couplings */ cbm_change_coupling_t results[16]; int n = cbm_compute_change_coupling(NULL, 0, results, 16); ASSERT_EQ(n, 0); PASS(); } TEST(coupling_single_file_commit) { /* Commits with single files → no pairs → zero couplings */ char *f1[] = {"a.go"}; char *f2[] = {"b.go"}; cbm_commit_files_t commits[] = {{f1, 1, 0}, {f2, 1, 0}}; cbm_change_coupling_t results[16]; int n = cbm_compute_change_coupling(commits, 2, results, 16); ASSERT_EQ(n, 0); PASS(); } TEST(fqn_compute_basic) { /* Basic FQN: project.dir.name */ char *fqn = cbm_pipeline_fqn_compute("proj", "pkg/handler.go", "Serve"); ASSERT_NOT_NULL(fqn); ASSERT_TRUE(strstr(fqn, "proj") != NULL); ASSERT_TRUE(strstr(fqn, "Serve") != NULL); free(fqn); PASS(); } TEST(fqn_compute_strips_ext) { /* FQN should strip file extension */ char *fqn = cbm_pipeline_fqn_compute("proj", "main.go", "main"); ASSERT_NOT_NULL(fqn); /* Should not contain ".go" */ ASSERT_TRUE(strstr(fqn, ".go") == NULL); ASSERT_TRUE(strstr(fqn, "main") != NULL); free(fqn); PASS(); } TEST(fqn_module_basic) { /* Module QN: project.dir.parts */ char *mod = cbm_pipeline_fqn_module("proj", "pkg/util/helper.go"); ASSERT_NOT_NULL(mod); ASSERT_TRUE(strstr(mod, "proj") != NULL); free(mod); PASS(); } TEST(fqn_folder_basic) { /* Folder QN from directory path */ char *folder = cbm_pipeline_fqn_folder("proj", "pkg/util"); ASSERT_NOT_NULL(folder); ASSERT_TRUE(strstr(folder, "proj") != NULL); free(folder); PASS(); } TEST(project_name_special_chars) { /* Project name with colons, slashes — should be normalized */ char *name = cbm_project_name_from_path("/home/user/my:project"); ASSERT_NOT_NULL(name); /* Colons should be converted to dashes */ ASSERT_TRUE(strchr(name, ':') == NULL); ASSERT_TRUE(strchr(name, '/') == NULL); free(name); PASS(); } TEST(project_name_trailing_slash) { /* Trailing slash should not affect result */ char *a = cbm_project_name_from_path("/home/user/project"); ASSERT_NOT_NULL(a); free(a); PASS(); } /* ── Complexity propagation pass tests (Tier B) ────────────────── */ /* Find the first Function/Method node with `name` in a label set. Returns a * borrowed pointer into `funcs` or NULL. */ static const cbm_node_t *find_node_named(cbm_node_t *funcs, int count, const char *name) { for (int i = 0; i < count; i++) { if (strcmp(funcs[i].name, name) == 0) return &funcs[i]; } return NULL; } /* The complexity pass propagates loop_depth along CALLS edges into * transitive_loop_depth and flags call-graph cycles as recursive. Caller and * callee live in one file so the calls resolve intra-file (most reliable). */ TEST(pipeline_complexity_transitive_loop_depth) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_cx_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("tmpdir"); write_temp_file(tmpdir, "cx.go", "package p\n\n" "func work() {}\n\n" "func inner() {\n" "\tfor i := 0; i < 10; i++ {\n" "\t\tfor j := 0; j < 10; j++ {\n" "\t\t\twork()\n" "\t\t}\n" "\t}\n" "}\n\n" "func outer() {\n" "\tfor i := 0; i < 10; i++ {\n" "\t\tinner()\n" "\t}\n" "}\n\n" "func recur(n int) {\n" "\tif n > 0 {\n" "\t\trecur(n - 1)\n" "\t}\n" "}\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/cx.db", tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); cbm_node_t *funcs = NULL; int func_count = 0; ASSERT_EQ(cbm_store_find_nodes_by_label(s, project, "Function", &funcs, &func_count), CBM_STORE_OK); ASSERT_GT(func_count, 0); /* inner: two nested loops, callee work() has depth 0 → tld == 2 */ const cbm_node_t *inner = find_node_named(funcs, func_count, "inner"); ASSERT_NOT_NULL(inner); ASSERT_NOT_NULL(inner->properties_json); ASSERT_TRUE(strstr(inner->properties_json, "\"transitive_loop_depth\":2") != NULL); /* outer: own depth 1 + inner's tld 2 → tld == 3 (interprocedural) */ const cbm_node_t *outer = find_node_named(funcs, func_count, "outer"); ASSERT_NOT_NULL(outer); ASSERT_NOT_NULL(outer->properties_json); ASSERT_TRUE(strstr(outer->properties_json, "\"transitive_loop_depth\":3") != NULL); /* recur: self-recursion → recursive:true flagged by the cycle guard */ const cbm_node_t *recur = find_node_named(funcs, func_count, "recur"); ASSERT_NOT_NULL(recur); ASSERT_NOT_NULL(recur->properties_json); ASSERT_TRUE(strstr(recur->properties_json, "\"recursive\":true") != NULL); cbm_store_free_nodes(funcs, func_count); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmpdir); PASS(); } /* Regression for #334: the plausibility gate compares committed (extracted) * node count against persisted rows. committed_nodes must be captured BEFORE * cbm_gbuf_dump_to_sqlite frees the gbuf node index — otherwise it reads 0 and * the gate is silently inert. Drives the real pipeline (not a synthetic count) * and asserts committed_nodes is populated and matches what was persisted. */ TEST(pipeline_committed_counts_match_persisted) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/committed_test.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); ASSERT_EQ(rc, 0); int committed_nodes = -1; int committed_edges = -1; cbm_pipeline_get_committed_counts(p, &committed_nodes, &committed_edges); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); int persisted_nodes = cbm_store_count_nodes(s, project); cbm_store_close(s); /* The bug captured committed_nodes after the node index was freed → 0. */ ASSERT_GT(committed_nodes, 0); /* A faithful full dump persists exactly what it committed. */ ASSERT_EQ(committed_nodes, persisted_nodes); /* The gate must NOT flag a healthy full index as degraded. */ ASSERT_FALSE(cbm_dump_verify_is_degraded(committed_nodes, persisted_nodes, CBM_DUMP_VERIFY_DEFAULT_RATIO, CBM_DUMP_VERIFY_MIN_FLOOR)); cbm_pipeline_free(p); teardown_test_repo(); PASS(); } /* Reproduce-first (perf, linux-kernel finding): the extraction back-pressure * gate must stop re-paying the full collect+nap tax on every file pull once a * full nap cycle has failed to reclaim under budget. With CBM_MEM_BUDGET_MB=1 * the test process RSS is permanently over budget and NOTHING napping can * change that (the resident floor IS the process) — napping is provably futile. * OLD behavior: one full 40-spin nap cycle per pulled file (kernel index: ~63k * pulls × ~120 ms ÷ 12 workers ≈ 390 s of idle workers at 79% avg CPU). * FIXED: the first futile cycle flips a shared flag; later pulls proceed with * the designed soft overshoot. Cycle count then can't exceed one cycle per * worker (workers already inside the gate when the flag flips) plus re-probes. * Four workers keep this semantic regression deterministic under TSan while * still exercising the parallel path. RED on the unfixed gate: cycles == file * count (64) > workers+2. * The counter (cbm_pp_bp_nap_cycles) makes this deterministic — no timing. * * The gate lives ONLY in the parallel extract path, so the fixture MUST exceed * MIN_FILES_FOR_PARALLEL (50) — else the run routes sequential, the gate never * fires, and the test would pass vacuously (cycles==0). The engagement assert * below (cycles >= 1) is a hard guard against that regressing silently. */ /* Shared fixture for the back-pressure tests: 64 tiny Go files, above * MIN_FILES_FOR_PARALLEL (50) so the parallel extract path — the only phase * with a memory gate — is the one that runs. */ static bool write_backpressure_fixture(void) { snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/cbm_test_XXXXXX"); if (!cbm_mkdtemp(g_tmpdir)) { return false; } for (int i = 0; i < 64; i++) { char path[512]; snprintf(path, sizeof(path), "%s/f%02d.go", g_tmpdir, i); FILE *f = fopen(path, "w"); if (!f) { return false; } fprintf(f, "package main\n\nfunc F%02d() int {\n\treturn %d\n}\n", i, i); fclose(f); } return true; } /* Rewrite every fixture file with a second definition. An unchanged repo * routes incremental_manifest → incremental.noop (nothing is re-extracted, so * no gate can fire); changing all 64 files makes the next run re-extract them * all through the parallel path. */ static bool mutate_backpressure_fixture(void) { for (int i = 0; i < 64; i++) { char path[512]; snprintf(path, sizeof(path), "%s/f%02d.go", g_tmpdir, i); FILE *f = fopen(path, "w"); if (!f) { return false; } fprintf(f, "package main\n\nfunc F%02d() int {\n\treturn %d\n}\n\n" "func G%02d() int {\n\treturn F%02d() + 1\n}\n", i, i, i, i); fclose(f); } return true; } /* Whole-file read for byte-identity assertions on a published database. */ static unsigned char *read_file_bytes(const char *path, size_t *len_out) { *len_out = 0; FILE *f = fopen(path, "rb"); if (!f) { return NULL; } long size = fseek(f, 0, SEEK_END) == 0 ? ftell(f) : -1; if (size < 0) { fclose(f); return NULL; } rewind(f); unsigned char *buf = malloc((size_t)size + 1); if (!buf) { fclose(f); return NULL; } size_t got = fread(buf, 1, (size_t)size, f); fclose(f); if (got != (size_t)size) { free(buf); return NULL; } *len_out = got; return buf; } TEST(pipeline_backpressure_futile_nap_disengages) { /* 64 tiny files: > MIN_FILES_FOR_PARALLEL (50) so the parallel path (and its * back-pressure gate) actually runs; old-code cycles (~64) >> the bound. */ if (!write_backpressure_fixture()) { FAIL("failed to create fixture"); } /* 1 MB budget: over-budget on every pull, unreclaimable by napping. * Set via the test hook, NOT setenv + cbm_mem_init: the init-once guard * makes any re-init keep whatever budget the FIRST in-process init * computed. The old env dance either failed to apply the 1 MB budget * (some earlier test's init won the guard) or applied it permanently * (this test's init won) — the "restore" re-init was then a silent * no-op and the 1 MB budget leaked into every later budget consumer * in the runner (mem_over_budget_low_rss went red suite-order-wide). */ size_t saved_budget = cbm_mem_budget(); cbm_mem_set_budget_for_tests((size_t)1024 * 1024); ASSERT_TRUE(cbm_mem_budget() > 0); ASSERT_TRUE(cbm_mem_over_budget()); cbm_pp_bp_nap_cycles_reset(); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/backpressure.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); /* This test measures the shared futility latch, not allocator scalability. * High-core TSan runs can turn unrelated slab bookkeeping into an 18-way * lock convoy. Four workers preserve the parallel semantic while making its * bound host-independent. cbm_pipeline_run joins its workers before return, * so restoring the process environment after freeing p cannot race them. */ enum { TEST_WORKERS = 4 }; const char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; if (old_workers && !saved_workers) { cbm_mem_set_budget_for_tests(saved_budget); cbm_pipeline_free(p); teardown_test_repo(); FAIL("failed to save CBM_WORKERS"); } if (cbm_setenv("CBM_WORKERS", "4", 1) != 0) { free(saved_workers); cbm_mem_set_budget_for_tests(saved_budget); cbm_pipeline_free(p); teardown_test_repo(); FAIL("failed to set CBM_WORKERS"); } int rc = cbm_pipeline_run(p); long cycles = cbm_pp_bp_nap_cycles(); /* Restore the caller-visible budget BEFORE asserting. */ cbm_mem_set_budget_for_tests(saved_budget); cbm_pipeline_free(p); int restore_workers_rc = saved_workers ? cbm_setenv("CBM_WORKERS", saved_workers, 1) : cbm_unsetenv("CBM_WORKERS"); free(saved_workers); teardown_test_repo(); ASSERT_EQ(restore_workers_rc, 0); /* Decision A (#1997 #832): a budget that never drains is not a soft * overshoot any more — after the latch and ONE confirmation cycle the * attempt fails whole with the named code (rc==0 was the advisory era). */ ASSERT_EQ(rc, CBM_PIPELINE_ABORT_OVER_BUDGET); /* Engagement guard (anti-vacuous): the gate must have actually run — the * parallel path taken and the 1 MB budget exceeded on every pull. cycles==0 * means the fixture routed sequential (or the gate was compiled out) and * this test proved NOTHING; fail loudly rather than pass vacuously. */ if (cycles < 1) { FAIL("back-pressure gate never engaged (cycles==0) — fixture routed sequential?"); } /* Futile napping must disengage: at most one in-flight cycle per worker * plus the single confirmation cycle and a small margin, never one per * file (64). */ long bound = TEST_WORKERS + 3; if (cycles > bound) { char msg[128]; snprintf(msg, sizeof(msg), "nap cycles %ld > bound %ld (gate re-paid per pull)", cycles, bound); FAIL(msg); } PASS(); } /* Decision A (#1997 #832): once back-pressure is futile AND one confirmation * cycle still ends over budget, the attempt fails WHOLE with a named code — * no partial graph is published, no stage residue remains, and the previously * serving generation is byte-identical. */ TEST(pipeline_over_budget_after_futility_fails_whole_and_preserves_db) { if (!write_backpressure_fixture()) { FAIL("failed to create fixture"); } enum { TEST_WORKERS = 4 }; const char *old_workers = getenv("CBM_WORKERS"); char *saved_workers = old_workers ? strdup(old_workers) : NULL; if ((old_workers && !saved_workers) || cbm_setenv("CBM_WORKERS", "4", 1) != 0) { free(saved_workers); teardown_test_repo(); FAIL("failed to pin CBM_WORKERS"); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/budget.db", g_tmpdir); /* Generation 1 at the process budget: publishes normally. */ cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); int rc_first = p ? cbm_pipeline_run(p) : -1; char *project = p ? strdup(cbm_pipeline_project_name(p)) : NULL; cbm_pipeline_free(p); int nodes_before = -1; bool valid_before = false; cbm_store_t *live = rc_first == 0 && project ? cbm_store_open_path(db_path) : NULL; if (live) { valid_before = cbm_store_check_integrity(live); nodes_before = cbm_store_count_nodes(live, project); cbm_store_close(live); } size_t before_len = 0; unsigned char *before = read_file_bytes(db_path, &before_len); /* Generation 2 at a 1 MiB budget over a CHANGED repo (every file gains a * definition, so the run must re-extract all 64): over budget on every * probe and unreclaimable by napping, so futility latches and the * confirmation cycle still ends over budget. */ bool mutated = mutate_backpressure_fixture(); size_t saved_budget = cbm_mem_budget(); cbm_mem_set_budget_for_tests((size_t)1024 * 1024); bool over_at_start = cbm_mem_over_budget(); cbm_pp_bp_nap_cycles_reset(); p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); int rc_second = p ? cbm_pipeline_run(p) : -1; long cycles = cbm_pp_bp_nap_cycles(); /* Restore the caller-visible budget BEFORE any assertion. */ cbm_mem_set_budget_for_tests(saved_budget); cbm_pipeline_free(p); size_t after_len = 0; unsigned char *after = read_file_bytes(db_path, &after_len); int stage_count = count_generation_stage_artifacts(g_tmpdir, "budget.db"); int nodes_after = -1; bool valid_after = false; live = cbm_store_open_path(db_path); if (live) { valid_after = cbm_store_check_integrity(live); nodes_after = project ? cbm_store_count_nodes(live, project) : -1; cbm_store_close(live); } bool bytes_identical = before && after && before_len == after_len && memcmp(before, after, before_len) == 0; free(before); free(after); free(project); int restore_workers_rc = saved_workers ? cbm_setenv("CBM_WORKERS", saved_workers, 1) : cbm_unsetenv("CBM_WORKERS"); free(saved_workers); teardown_test_repo(); ASSERT_EQ(restore_workers_rc, 0); ASSERT_EQ(rc_first, 0); ASSERT_TRUE(valid_before); ASSERT_GT(nodes_before, 0); ASSERT_TRUE(before_len > 0); ASSERT_TRUE(mutated); ASSERT_TRUE(over_at_start); /* The named failure — not the cancel sentinel, not success. */ ASSERT_EQ(rc_second, CBM_PIPELINE_ABORT_OVER_BUDGET); /* Engagement guard (anti-vacuous): fail-whole needs the latching cycle AND * the confirmation cycle; fewer means the gate never decided anything. */ if (cycles < 2) { FAIL("back-pressure gate never confirmed futility (cycles<2) — fixture routed sequential?"); } /* The serving generation is untouched: same bytes, still valid, same * graph; and the failed attempt left no stage residue behind. */ ASSERT_TRUE(bytes_identical); ASSERT_TRUE(valid_after); ASSERT_EQ(nodes_after, nodes_before); ASSERT_EQ(stage_count, 0); PASS(); } /* TS cross-registry test hooks (ts_lsp.c) — extern to avoid pulling the * tree-sitter-typed ts_lsp.h into this store-level test. */ extern long cbm_ts_full_registry_builds(void); extern void cbm_ts_full_registry_builds_reset(void); /* Reproduce-first (ms-typescript finding, 2026-07-07): the SEQUENTIAL * cross-LSP driver must resolve TS files through the SHARED prebuilt * registry, never a full per-file build. cbm_run_ts_lsp_cross registers * stdlib + EVERY cross-file def + finalizes once PER FILE — O(files x defs). * On an 81k-file TS corpus that ground one core for hours (74% of stack * samples inside build_qn_index), and when the supervisor's quiet-timeout * killed the crawl mid-pass, the stale extraction marker blamed innocent * files, quarantining four of them one 15-minute retry at a time. * * The fixture stays UNDER MIN_FILES_FOR_PARALLEL (50) so the pipeline * routes through the sequential driver — the path that lacked the * shared-registry prepare. * RED on the unfixed driver: full builds == TS file count (40). * GREEN: full builds == 0 AND the cross-file TS call still resolves * (quality guard — the shared path must not lose the edge). */ TEST(pipeline_seq_ts_cross_uses_shared_registry) { snprintf(g_tmpdir, sizeof(g_tmpdir), "/tmp/cbm_test_XXXXXX"); if (!cbm_mkdtemp(g_tmpdir)) { FAIL("failed to create temp dir"); } char path[512]; snprintf(path, sizeof(path), "%s/shared.ts", g_tmpdir); FILE *f = fopen(path, "w"); if (!f) { FAIL("failed to create fixture file"); } fputs("export function sharedHelper(): number {\n return 42;\n}\n", f); fclose(f); for (int i = 0; i < 39; i++) { snprintf(path, sizeof(path), "%s/caller%02d.ts", g_tmpdir, i); f = fopen(path, "w"); if (!f) { FAIL("failed to create fixture file"); } fprintf(f, "import { sharedHelper } from \"./shared\";\n" "export function caller%02d(): number {\n return sharedHelper();\n}\n", i); fclose(f); } char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/seqts.db", g_tmpdir); cbm_ts_full_registry_builds_reset(); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); long builds = cbm_ts_full_registry_builds(); /* Quality guard FIRST: the cross-file call must resolve either way. */ cbm_store_t *s = cbm_store_open_path(db_path); bool linked = false; if (s) { linked = cross_file_call_exists(s, cbm_pipeline_project_name(p), "caller00", "sharedHelper"); cbm_store_close(s); } cbm_pipeline_free(p); teardown_test_repo(); ASSERT_EQ(rc, 0); ASSERT_TRUE(linked); /* The point: ZERO full per-file registry builds on the sequential path. */ ASSERT_EQ(builds, 0); PASS(); } /* Build a real extraction surface without publishing a database. The returned * JSON outlives both the extraction and collector arenas. */ static char *pipeline_namespace_surface(const char *source, bool include_defs) { CBMFileResult *fr = cbm_extract_file(source, (int)strlen(source), CBM_LANG_PYTHON, "namespace_probe", "pkg/__init__.py", 0, NULL, NULL); if (!fr || fr->has_error || fr->parse_incomplete) { cbm_free_result(fr); return NULL; } CBMArena arena; cbm_arena_init(&arena); CBMFileResult *cache[] = {fr}; cbm_file_info_t files[] = {{.rel_path = "pkg/__init__.py", .language = CBM_LANG_PYTHON}}; char *modules[] = {NULL}; int starts[2] = {0, 0}; int def_count = 0; CBMLSPDef *defs = include_defs ? cbm_pxc_collect_all_defs(NULL, &arena, cache, files, 1, "namespace_probe", modules, &def_count, starts) : NULL; cbm_lsp_surface_row_t *rows = NULL; int row_count = 0; char *json = NULL; if (cbm_lsp_surface_build_rows(NULL, "namespace_probe", cache, files, 1, defs, starts, &rows, &row_count) == 0 && row_count == 1 && rows[0].defs_json) { json = strdup(rows[0].defs_json); } cbm_store_free_lsp_surfaces(rows, row_count); free(defs); free(modules[0]); cbm_arena_destroy(&arena); cbm_free_result(fr); return json; } TEST(pipeline_python_namespace_change_admission) { const char *sources[] = { "def target(value):\n return 1\n", "def target(value):\n return 2\n", "def target(value, extra):\n return 1\n", "from first import Target as Alias\n", "from second import Target as Alias\n", "from first import Target as Alias\nfrom second import Target as Alias\n", "from second import Target as Alias\nfrom first import Target as Alias\n", "__all__ = []\n", "__all__ = ['Alias']\n", "__all__ = dynamic_names()\n", "# empty namespace\n", }; enum { count = (int)(sizeof(sources) / sizeof(sources[0])) }; char *json[count]; bool complete = true; for (int i = 0; i < count; i++) { json[i] = pipeline_namespace_surface(sources[i], true); complete = complete && json[i] != NULL; } int body = -9, signature = -9, alias = -9, order = -9; int empty = -9, literal = -9, unknown = -9, deletion = -9, transition = -9; const char *non_python = "{\"v\":3,\"lang\":0,\"lsp\":[],\"reg\":[],\"py\":null}"; if (complete) { body = cbm_lsp_surface_python_change(json[0], json[1]); signature = cbm_lsp_surface_python_change(json[0], json[2]); alias = cbm_lsp_surface_python_change(json[3], json[4]); order = cbm_lsp_surface_python_change(json[5], json[6]); empty = cbm_lsp_surface_python_change(json[10], json[7]); literal = cbm_lsp_surface_python_change(json[7], json[8]); unknown = cbm_lsp_surface_python_change(json[7], json[9]); deletion = cbm_lsp_surface_python_change(json[10], NULL); transition = cbm_lsp_surface_python_change(non_python, json[10]); } for (int i = 0; i < count; i++) { free(json[i]); } ASSERT_TRUE(complete); ASSERT_EQ(body, 0); ASSERT_EQ(signature, 1); ASSERT_EQ(alias, 1); ASSERT_EQ(order, 1); ASSERT_EQ(empty, 1); ASSERT_EQ(literal, 1); ASSERT_EQ(unknown, 1); ASSERT_EQ(deletion, 1); ASSERT_EQ(transition, 1); ASSERT_EQ(cbm_lsp_surface_python_change(non_python, NULL), 0); ASSERT_EQ(cbm_lsp_surface_python_change(non_python, non_python), 0); PASS(); } static char *pipeline_namespace_payload_json(const char *json) { yyjson_doc *doc = json ? yyjson_read(json, strlen(json), 0) : NULL; if (!doc) { return NULL; } yyjson_val *payload = yyjson_obj_get(yyjson_doc_get_root(doc), "py"); char *copy = payload ? yyjson_val_write(payload, 0, NULL) : NULL; yyjson_doc_free(doc); return copy; } TEST(pipeline_python_namespace_codec_owns_ordered_facts) { const char *source = "def plain(value):\n return value\n" "from .one import First as Alias\n" "from .two import Second as Alias\n" "__all__ = ['Alias', '_private', '', 'odd|name']\n" "__all__ += ('tail',)\n" "__all__ = []\n" "__all__ = dynamic_names()\n"; char *full = pipeline_namespace_surface(source, true); char *probe = pipeline_namespace_surface(source, false); char *full_facts = pipeline_namespace_payload_json(full); char *probe_facts = pipeline_namespace_payload_json(probe); bool parity = full_facts && probe_facts && strcmp(full_facts, probe_facts) == 0; bool distinct_defs = full && probe && strcmp(full, probe) != 0; CBMArena arena; cbm_arena_init(&arena); cbm_lsp_python_namespace_t copied = {0}, failed = {0}; cbm_py_namespace_test_copy_fail_after(0); int failed_rc = cbm_lsp_surface_python_namespace_from_json(&arena, full, &failed); cbm_py_namespace_test_copy_fail_after(-1); int rc = cbm_lsp_surface_python_namespace_from_json(&arena, full, &copied); free(full); free(probe); free(full_facts); free(probe_facts); /* The original result, collector, JSON documents and serialized bytes are * all gone before the copied strings and nested arrays are inspected. */ bool origin = rc == 1 && copied.module_qn && copied.rel_path && strcmp(copied.module_qn, "namespace_probe.pkg.__init__") == 0 && strcmp(copied.rel_path, "pkg/__init__.py") == 0; bool valid = rc == 1 && cbm_py_namespace_facts_valid(&copied.facts) && copied.facts.status == CBM_PY_NS_COMPLETE && copied.facts.language == CBM_LANG_PYTHON; int imports = 0; bool import_order = true, names = false, append = false, empty = false, unknown = false; if (valid) { for (int i = 0; i < copied.facts.count; i++) { const CBMPyNamespaceFact *f = &copied.facts.items[i]; if (f->kind == CBM_PY_NS_IMPORT_NAME) { const char *module = imports == 0 ? "one" : "two"; const char *member = imports == 0 ? "First" : "Second"; import_order = import_order && imports < 2 && f->relative_level == 1 && f->module_name && strcmp(f->module_name, module) == 0 && f->member_name && strcmp(f->member_name, member) == 0 && f->local_name && strcmp(f->local_name, "Alias") == 0; imports++; } if (f->kind == CBM_PY_NS_ALL_SET && f->name_count == 4) { names = f->sequence_kind == CBM_PY_NS_SEQUENCE_LIST && strcmp(f->names[0], "Alias") == 0 && strcmp(f->names[1], "_private") == 0 && strcmp(f->names[2], "") == 0 && strcmp(f->names[3], "odd|name") == 0; } if (f->kind == CBM_PY_NS_ALL_APPEND) { append = f->sequence_kind == CBM_PY_NS_SEQUENCE_TUPLE && f->name_count == 1 && strcmp(f->names[0], "tail") == 0; } empty = empty || (f->kind == CBM_PY_NS_ALL_SET && f->name_count == 0 && f->names == NULL); unknown = unknown || f->kind == CBM_PY_NS_ALL_UNKNOWN; } } bool failure_empty = failed.facts.items == NULL && failed.facts.count == 0 && failed.module_qn == NULL && failed.rel_path == NULL; cbm_arena_destroy(&arena); ASSERT_TRUE(parity); ASSERT_TRUE(distinct_defs); ASSERT_EQ(failed_rc, -1); ASSERT_TRUE(failure_empty); ASSERT_EQ(rc, 1); ASSERT_TRUE(origin); ASSERT_TRUE(valid); ASSERT_EQ(imports, 2); ASSERT_TRUE(import_order); ASSERT_TRUE(names); ASSERT_TRUE(append); ASSERT_TRUE(empty); ASSERT_TRUE(unknown); PASS(); } TEST(pipeline_python_namespace_codec_rejects_non_authoritative_rows) { /* Numeric values below are the frozen namespace wire values (version 2 * changed which statements extraction proves harmless, not the wire * shape). Even an empty LSP array must validate its namespace envelope, * and a payload of an older facts version is no authority: its facts * were extracted under other rules, so the incremental run that reads * it re-extracts in full. */ const char *bad[] = { "{\"v\":1,\"lsp\":[]}", "{\"v\":3,\"lang\":1,\"lsp\":[]}", "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":null}", "{\"v\":3,\"lang\":1.0,\"lsp\":[],\"py\":null}", "{\"v\":3,\"v\":3,\"lang\":0,\"lsp\":[],\"py\":null}", "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":3,\"failure\":2,\"events\":[]}}", "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[]}}", "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":99,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[]}}", "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\\u0000suffix\",\"status\":2," "\"failure\":0,\"events\":[]}}", "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[" "{\"k\":999,\"d\":0,\"s\":0,\"r\":0,\"f\":0,\"l\":0," "\"n\":null,\"m\":null,\"i\":null,\"a\":[]}]}}", "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[" "{\"k\":8,\"d\":0,\"s\":1,\"r\":0,\"f\":0,\"l\":4294967296," "\"n\":null,\"m\":null,\"i\":null,\"a\":[]}]}}", }; bool rejected = true; for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) { CBMArena arena; cbm_arena_init(&arena); cbm_lsp_python_namespace_t facts = {0}; CBMLSPDef *defs = NULL; int ns_rc = cbm_lsp_surface_python_namespace_from_json(&arena, bad[i], &facts); int defs_rc = cbm_lsp_surface_defs_from_json(&arena, bad[i], &defs); int compare_rc = cbm_lsp_surface_python_change(bad[i], bad[i]); int delete_rc = cbm_lsp_surface_python_change(bad[i], NULL); rejected = rejected && ns_rc == -1 && defs_rc == -1 && compare_rc == -1 && delete_rc == -1 && facts.facts.items == NULL && defs == NULL; cbm_arena_destroy(&arena); } const char *unknown = "{\"v\":3,\"lang\":1,\"lsp\":[],\"reg\":[],\"py\":{\"v\":2,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[" "{\"k\":7,\"d\":0,\"s\":0,\"r\":7,\"f\":0,\"l\":0," "\"n\":null,\"m\":null,\"i\":null,\"a\":[]}]}}"; CBMArena arena; cbm_arena_init(&arena); cbm_lsp_python_namespace_t facts = {0}; int unknown_rc = cbm_lsp_surface_python_namespace_from_json(&arena, unknown, &facts); int equal_rc = cbm_lsp_surface_python_change(unknown, unknown); bool retained = unknown_rc == 1 && facts.facts.count == 1 && facts.facts.items[0].kind == CBM_PY_NS_UNKNOWN && facts.facts.items[0].reason == CBM_PY_NS_REASON_PARSE; cbm_arena_destroy(&arena); ASSERT_EQ(CBM_PY_NAMESPACE_FACTS_VERSION, 2u); /* the rows above spell it */ ASSERT_TRUE(rejected); ASSERT_EQ(unknown_rc, 1); ASSERT_EQ(equal_rc, 0); ASSERT_TRUE(retained); PASS(); } /* Object arrays are non-flat in yyjson. Exercise ordered decoding and arena * ownership across a large surface, including nested string arrays. */ TEST(pipeline_lsp_surface_large_object_array_decode) { const int count = 8192; const size_t capacity = (size_t)count * 192 + 64; char *json = malloc(capacity); ASSERT_NOT_NULL(json); size_t used = (size_t)snprintf(json, capacity, "{\"v\":3,\"lang\":0,\"py\":null,\"lsp\":["); for (int i = 0; i < count; i++) { int n = snprintf(json + used, capacity - used, "%s{\"qn\":\"pkg.f%d\",\"sn\":\"f%d\",\"lb\":\"Function\"," "\"spt\":[\"str\",\"int\"],\"dec\":[\"first\",\"second\"]}", i ? "," : "", i, i); ASSERT_TRUE(n > 0 && (size_t)n < capacity - used); used += (size_t)n; } snprintf(json + used, capacity - used, "]}"); CBMArena arena; cbm_arena_init(&arena); CBMLSPDef *defs = NULL; int decoded = cbm_lsp_surface_defs_from_json(&arena, json, &defs); free(json); ASSERT_EQ(decoded, count); ASSERT_NOT_NULL(defs); for (int i = 0; i < count; i++) { char expected[64]; snprintf(expected, sizeof(expected), "pkg.f%d", i); ASSERT_STR_EQ(defs[i].qualified_name, expected); snprintf(expected, sizeof(expected), "f%d", i); ASSERT_STR_EQ(defs[i].short_name, expected); ASSERT_STR_EQ(defs[i].label, "Function"); ASSERT_EQ(defs[i].signature_param_count, 2); ASSERT_STR_EQ(defs[i].signature_param_types[0], "str"); ASSERT_STR_EQ(defs[i].signature_param_types[1], "int"); ASSERT_STR_EQ(defs[i].decorators[0], "first"); ASSERT_STR_EQ(defs[i].decorators[1], "second"); ASSERT_NULL(defs[i].decorators[2]); } cbm_arena_destroy(&arena); PASS(); } /* The closure-repair route lives and dies by two properties of the persisted * per-file LSP surface: a BODY edit must leave the surface_sha unchanged (the * early cutoff -- no dependent recomputation owed), while a SIGNATURE edit * must change it (dependents owe re-resolution). Each run here is a fresh * full index (DB removed in between) so what is compared is the published * surface of the code as it stands, not any incremental behaviour. */ TEST(pipeline_lsp_surface_persisted_and_body_edit_invariant) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_surface_persist_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); char src_a[512]; char db_path[512]; snprintf(src_a, sizeof(src_a), "%s/a.py", tmp); snprintf(db_path, sizeof(db_path), "%s/surface.db", tmp); ASSERT_EQ(th_write_file(src_a, "def surface_probe(x):\n return 1\n"), 0); char sha_baseline[128] = {0}; char json_probe[64] = {0}; char project[256] = {0}; for (int round = 0; round < 3; round++) { if (round == 1) { /* Body edit: same def surface, different body. */ ASSERT_EQ(th_write_file(src_a, "def surface_probe(x):\n return 2 + x\n"), 0); } else if (round == 2) { /* Signature edit: the surface other files consume changes. */ ASSERT_EQ(th_write_file(src_a, "def surface_probe(x, y):\n return 1\n"), 0); } cbm_unlink(db_path); cbm_remove_db_sidecars(db_path); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); if (round == 0) { snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(p)); } cbm_pipeline_free(p); cbm_store_t *store = cbm_store_open_path(db_path); ASSERT_NOT_NULL(store); cbm_lsp_surface_row_t *rows = NULL; int row_count = 0; ASSERT_EQ(cbm_store_get_lsp_surfaces(store, project, &rows, &row_count), CBM_STORE_OK); cbm_store_close(store); const cbm_lsp_surface_row_t *row_a = NULL; for (int i = 0; i < row_count; i++) { if (strcmp(rows[i].rel_path, "a.py") == 0) { row_a = &rows[i]; } } ASSERT_NOT_NULL(row_a); ASSERT_TRUE(row_a->surface_sha && row_a->surface_sha[0]); if (round == 0) { snprintf(sha_baseline, sizeof(sha_baseline), "%s", row_a->surface_sha); /* The row is the versioned codec envelope (codec version 3) with * the def present. */ snprintf(json_probe, sizeof(json_probe), "%.20s", row_a->defs_json); ASSERT_TRUE(strstr(row_a->defs_json, "\"v\":3") != NULL); ASSERT_TRUE(strstr(row_a->defs_json, "surface_probe") != NULL); } else if (round == 1) { ASSERT_STR_EQ(row_a->surface_sha, sha_baseline); } else { ASSERT_TRUE(strcmp(row_a->surface_sha, sha_baseline) != 0); } cbm_store_free_lsp_surfaces(rows, row_count); } (void)json_probe; th_rmtree(tmp); PASS(); } TEST(pipeline_ensemble_routing_edges) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_ens_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("failed to create temp dir"); char path[512]; /* Production class with two items */ snprintf(path, sizeof(path), "%s/MyProduction.cls", tmpdir); FILE *f = fopen(path, "w"); if (!f) { th_rmtree(tmpdir); FAIL("fopen production"); } fprintf(f, "Class MyApp.MyProduction Extends Ens.Production\n" "{\n" "XData ProductionDefinition\n" "{\n" "\n" " \n" " \n" " \n" " \n" "\n" "}\n" "}\n"); fclose(f); /* Service class with OnProcessInput that routes to MyOperation via literal */ snprintf(path, sizeof(path), "%s/MyService.cls", tmpdir); f = fopen(path, "w"); if (!f) { th_rmtree(tmpdir); FAIL("fopen service"); } fprintf(f, "Class MyApp.MyService Extends Ens.BusinessService\n" "{\n" "Method OnProcessInput(pRequest As %%Library.Object," " Output pResponse As %%Library.Object) As %%Status\n" "{\n" " Do ..SendRequestSync(\"MyOperation\", pRequest, .pResponse)\n" " Quit $$$OK\n" "}\n" "}\n"); fclose(f); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/ens.db", tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Route nodes with __route__ensemble__ qn emitted for both production items */ cbm_node_t *routes = NULL; int route_count = 0; cbm_store_find_nodes_by_label(s, project, "Route", &routes, &route_count); int ens_route_count = 0; for (int i = 0; i < route_count; i++) { if (routes[i].qualified_name && strstr(routes[i].qualified_name, "__route__ensemble__") != NULL) ens_route_count++; } ASSERT_GTE(ens_route_count, 2); cbm_store_free_nodes(routes, route_count); /* At least one ASYNC_CALLS edge from SendRequestSync literal match */ int async_calls = cbm_store_count_edges_by_type(s, project, "ASYNC_CALLS"); ASSERT_GTE(async_calls, 1); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmpdir); PASS(); } TEST(pipeline_ensemble_routing_attr_does_not_leak_across_items) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_ens_attr_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("failed to create temp dir"); char path[512]; /* The first item declares no ClassName. Attribute lookup used to scan * forward from the tag with no bound, so it inherited ClassName from the * SECOND item and emitted a Route for an item that does not name a class, * pairing it with the wrong implementation. Only the well-formed item may * produce a Route. */ snprintf(path, sizeof(path), "%s/AttrProduction.cls", tmpdir); FILE *f = fopen(path, "w"); if (!f) { th_rmtree(tmpdir); FAIL("fopen production"); } fprintf(f, "Class MyApp.AttrProduction Extends Ens.Production\n" "{\n" "XData ProductionDefinition\n" "{\n" "\n" " \n" " \n" " \n" " \n" "\n" "}\n" "}\n"); fclose(f); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/ens.db", tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); cbm_node_t *routes = NULL; int route_count = 0; cbm_store_find_nodes_by_label(s, project, "Route", &routes, &route_count); int ens_route_count = 0; for (int i = 0; i < route_count; i++) { if (routes[i].qualified_name && strstr(routes[i].qualified_name, "__route__ensemble__") != NULL) ens_route_count++; } ASSERT_EQ(ens_route_count, 1); cbm_store_free_nodes(routes, route_count); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmpdir); PASS(); } TEST(pipeline_ensemble_routing_settings_targets) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_ens_set_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("failed to create temp dir"); char path[512]; /* A production whose ONLY routing information is declarative: a * TargetConfigNames setting on the router item. No class file defines * SendRequestSync anywhere, so every ASYNC_CALLS edge this run produces * must have come from the settings path. */ snprintf(path, sizeof(path), "%s/RouterProduction.cls", tmpdir); FILE *f = fopen(path, "w"); if (!f) { th_rmtree(tmpdir); FAIL("fopen production"); } fprintf(f, "Class MyApp.RouterProduction Extends Ens.Production\n" "{\n" "XData ProductionDefinition\n" "{\n" "\n" " \n" " OpA,OpB\n" " \n" " \n" " \n" " \n" " \n" "\n" "}\n" "}\n"); fclose(f); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/ens.db", tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); /* Router -> OpA and Router -> OpB, both derived from TargetConfigNames. * Before the Route-qn fix this was 0: the settings loop looked the source * item up by its bare qn, which cbm_gbuf_find_by_qn (an exact hashtable * lookup) never matches, so every edge was skipped by `continue`. */ int async_calls = cbm_store_count_edges_by_type(s, project, "ASYNC_CALLS"); ASSERT_EQ(async_calls, 2); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmpdir); PASS(); } TEST(pipeline_ensemble_routing_unterminated_item_is_safe) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_ens_bad_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("failed to create temp dir"); char path[512]; /* Truncated XData: an "), reading 7 bytes past the allocation and scanning * unbounded heap from there. Under ASan this aborts the run. */ snprintf(path, sizeof(path), "%s/BadProduction.cls", tmpdir); FILE *f = fopen(path, "w"); if (!f) { th_rmtree(tmpdir); FAIL("fopen production"); } fprintf(f, "Class MyApp.BadProduction Extends Ens.Production\n" "{\n" "XData ProductionDefinition\n" "{\n" "\n" " \n" "\n" "}\n" "}\n"); fclose(f); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/ens.db", tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); /* The well-formed prefix is still honoured: the item is parsed and its * Route node emitted. Malformed input degrades, it does not disappear. */ cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); cbm_node_t *routes = NULL; int route_count = 0; cbm_store_find_nodes_by_label(s, project, "Route", &routes, &route_count); int ens_route_count = 0; for (int i = 0; i < route_count; i++) { if (routes[i].qualified_name && strstr(routes[i].qualified_name, "__route__ensemble__") != NULL) ens_route_count++; } ASSERT_EQ(ens_route_count, 1); cbm_store_free_nodes(routes, route_count); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmpdir); PASS(); } TEST(pipeline_ensemble_routing_method_scoping) { /* Verifies scan_source_for_send_targets scopes to the calling method body. * Two methods in the same class each route to a different operation; each * must produce exactly one edge to its own target with no cross-contamination. */ char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_ens2_XXXXXX"); if (!cbm_mkdtemp(tmpdir)) FAIL("failed to create temp dir"); char path[512]; snprintf(path, sizeof(path), "%s/TwoProd.cls", tmpdir); FILE *f = fopen(path, "w"); if (!f) { th_rmtree(tmpdir); FAIL("fopen production"); } fprintf(f, "Class MyApp.TwoProd Extends Ens.Production\n" "{\n" "XData ProductionDefinition\n" "{\n" "\n" " \n" " \n" " \n" " \n" " \n" " \n" "\n" "}\n" "}\n"); fclose(f); snprintf(path, sizeof(path), "%s/TwoMethodService.cls", tmpdir); f = fopen(path, "w"); if (!f) { th_rmtree(tmpdir); FAIL("fopen service"); } fprintf(f, "Class MyApp.TwoMethodService Extends Ens.BusinessService\n" "{\n" "Method MethodOne(pRequest As %%Library.Object," " Output pResponse As %%Library.Object) As %%Status\n" "{\n" " Do ..SendRequestSync(\"OperationA\", pRequest, .pResponse)\n" " Quit $$$OK\n" "}\n" "Method MethodTwo(pRequest As %%Library.Object," " Output pResponse As %%Library.Object) As %%Status\n" "{\n" " Do ..SendRequestSync(\"OperationB\", pRequest, .pResponse)\n" " Quit $$$OK\n" "}\n" "}\n"); fclose(f); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/ens2.db", tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(tmpdir, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); cbm_node_t *m1_nodes = NULL, *m2_nodes = NULL; int m1_count = 0, m2_count = 0; cbm_store_find_nodes_by_name(s, project, "MethodOne", &m1_nodes, &m1_count); cbm_store_find_nodes_by_name(s, project, "MethodTwo", &m2_nodes, &m2_count); ASSERT_GTE(m1_count, 1); ASSERT_GTE(m2_count, 1); cbm_node_t *route_nodes = NULL; int rn_count = 0; cbm_store_find_nodes_by_label(s, project, "Route", &route_nodes, &rn_count); int64_t route_a_id = 0, route_b_id = 0; for (int i = 0; i < rn_count; i++) { if (route_nodes[i].qualified_name) { if (strstr(route_nodes[i].qualified_name, "__route__ensemble__") && strstr(route_nodes[i].qualified_name, "OperationA")) route_a_id = route_nodes[i].id; if (strstr(route_nodes[i].qualified_name, "__route__ensemble__") && strstr(route_nodes[i].qualified_name, "OperationB")) route_b_id = route_nodes[i].id; } } cbm_store_free_nodes(route_nodes, rn_count); ASSERT_NEQ(route_a_id, 0); ASSERT_NEQ(route_b_id, 0); cbm_edge_t *m1_edges = NULL; int m1_ec = 0; cbm_store_find_edges_by_source_type(s, m1_nodes[0].id, "ASYNC_CALLS", &m1_edges, &m1_ec); int m1_to_a = 0, m1_to_b = 0; for (int i = 0; i < m1_ec; i++) { if (m1_edges[i].target_id == route_a_id) m1_to_a++; if (m1_edges[i].target_id == route_b_id) m1_to_b++; } ASSERT_GTE(m1_to_a, 1); ASSERT_EQ(m1_to_b, 0); cbm_edge_t *m2_edges = NULL; int m2_ec = 0; cbm_store_find_edges_by_source_type(s, m2_nodes[0].id, "ASYNC_CALLS", &m2_edges, &m2_ec); int m2_to_a = 0, m2_to_b = 0; for (int i = 0; i < m2_ec; i++) { if (m2_edges[i].target_id == route_a_id) m2_to_a++; if (m2_edges[i].target_id == route_b_id) m2_to_b++; } ASSERT_GTE(m2_to_b, 1); ASSERT_EQ(m2_to_a, 0); cbm_store_free_edges(m1_edges, m1_ec); cbm_store_free_edges(m2_edges, m2_ec); cbm_store_free_nodes(m1_nodes, m1_count); cbm_store_free_nodes(m2_nodes, m2_count); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmpdir); PASS(); } /* #1260: CALLS edges from the function named `func` whose target qn ends * with "." (segment-anchored). A NULL suffix counts every * CALLS edge to a node named "Run". */ static int iris_cls_calls(cbm_store_t *s, const char *project, const char *func, const char *target_suffix) { cbm_node_t *fn = NULL; int fn_count = 0; cbm_store_find_nodes_by_name(s, project, func, &fn, &fn_count); int n = 0; for (int fi = 0; fi < fn_count; fi++) { cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(s, fn[fi].id, "CALLS", &edges, &ec); for (int ei = 0; ei < ec; ei++) { cbm_node_t tgt = {0}; if (cbm_store_find_node_by_id(s, edges[ei].target_id, &tgt) != 0) { continue; } if (!target_suffix) { if (tgt.name && strcmp(tgt.name, "Run") == 0) { n++; } } else if (tgt.qualified_name) { size_t ql = strlen(tgt.qualified_name); size_t sl = strlen(target_suffix); if (ql > sl && tgt.qualified_name[ql - sl - 1] == '.' && strcmp(tgt.qualified_name + ql - sl, target_suffix) == 0) { n++; } } cbm_node_free_fields(&tgt); } cbm_store_free_edges(edges, ec); } cbm_store_free_nodes(fn, fn_count); return n; } TEST(pipeline_python_iris_cls_class_aware_calls) { /* The #1260 repro: two ObjectScript classes share a method name. Python * callers must reach the class named in iris.cls(""), and a call * that names no indexed class must not borrow an edge from one that does. */ char *tmp = th_mktempdir("cbm_iris_cls"); ASSERT_NOT_NULL(tmp); const char *run_body = "{\n" "ClassMethod Run(x As %String) As %String\n" "{\n" " Quit x\n" "}\n" "}\n"; char cls[256]; snprintf(cls, sizeof(cls), "Class Pkg.A Extends %%RegisteredObject\n%s", run_body); write_temp_file(tmp, "A.cls", cls); snprintf(cls, sizeof(cls), "Class Pkg.B Extends %%RegisteredObject\n%s", run_body); write_temp_file(tmp, "B.cls", cls); /* A third, unrelated Run: a bare "Run" callee could weak-match it. */ write_temp_file(tmp, "util.py", "def Run(x):\n" " return x\n"); write_temp_file(tmp, "caller.py", "import iris\n" "\n" "class Helper:\n" " def Run(self, x):\n" " return x\n" "\n" "def want_a():\n" " return iris.cls(\"Pkg.A\").Run(\"x\")\n" "\n" "def want_b():\n" " return iris.cls(\"Pkg.B\").Run(\"x\")\n" "\n" "def want_nonexistent():\n" " return iris.cls(\"Pkg.DoesNotExist\").Run(\"x\")\n" "\n" "def unrelated_receiver():\n" " return some_random_thing.Run(\"x\")\n" "\n" "def via_classmethodvalue(db):\n" " return db.classMethodValue(\"Pkg.B\", \"Run\", \"x\")\n" "\n" "def via_invoke(db):\n" " return db.invokeClassMethod(\"Pkg.A\", \"Run\", \"x\")\n"); char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/iris_cls.db", tmp); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_store_t *s = cbm_store_open_path(db_path); ASSERT_NOT_NULL(s); const char *project = cbm_pipeline_project_name(p); ASSERT_EQ(iris_cls_calls(s, project, "want_a", "Pkg.A.Run"), 1); ASSERT_EQ(iris_cls_calls(s, project, "want_a", NULL), 1); ASSERT_EQ(iris_cls_calls(s, project, "want_b", "Pkg.B.Run"), 1); ASSERT_EQ(iris_cls_calls(s, project, "want_b", NULL), 1); ASSERT_EQ(iris_cls_calls(s, project, "want_a", "util.Run"), 0); ASSERT_EQ(iris_cls_calls(s, project, "want_b", "util.Run"), 0); ASSERT_EQ(iris_cls_calls(s, project, "want_a", "Helper.Run"), 0); ASSERT_EQ(iris_cls_calls(s, project, "want_b", "Helper.Run"), 0); ASSERT_EQ(iris_cls_calls(s, project, "want_nonexistent", NULL), 0); ASSERT_EQ(iris_cls_calls(s, project, "unrelated_receiver", NULL), 0); ASSERT_EQ(iris_cls_calls(s, project, "via_classmethodvalue", "Pkg.B.Run"), 1); ASSERT_EQ(iris_cls_calls(s, project, "via_invoke", "Pkg.A.Run"), 1); ASSERT_EQ(iris_cls_calls(s, project, "via_invoke", NULL), 1); cbm_store_close(s); cbm_pipeline_free(p); th_rmtree(tmp); PASS(); } /* #518/#519 item-7 regression: the DELTA merge is the warm path most users * hit. It used to write nodes_fts with a hand-rolled four-column INSERT of * its own; with a fifth `body` column that literal leaves prose NULL for every * node arriving by delta, invisibly — a full reindex still looks perfect. * Routing the write through cbm_store_fts_rebuild() is what this test binds: * revert pipeline_delta.c to a four-column INSERT and it fails. */ TEST(pipeline_delta_patch_indexes_docstring_into_fts_body) { char *td = th_mktempdir("cbm_delta_fts"); ASSERT_NOT_NULL(td); char dbpath[512]; snprintf(dbpath, sizeof(dbpath), "%s/delta.db", td); cbm_store_t *store = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(store); const char *proj = "deltafts"; ASSERT_EQ(cbm_store_upsert_project(store, proj, td), CBM_STORE_OK); /* A previous generation already on disk, so the patch runs against a real * id watermark rather than an empty database. */ cbm_node_t existing = {.project = proj, .label = "Function", .name = "alreadyThere", .qualified_name = "deltafts.main.alreadyThere", .file_path = "main.c"}; ASSERT_TRUE(cbm_store_upsert_node(store, &existing) > 0); ASSERT_EQ(cbm_store_fts_rebuild(store, NULL, 0), CBM_STORE_OK); /* The delta patch: preseed proxies the resident rows and sets the * watermark, then the NEW node is minted above it. */ cbm_gbuf_t *gb = cbm_gbuf_new(proj, td); ASSERT_NOT_NULL(gb); int64_t max_db_id = cbm_delta_preseed(store, proj, gb); ASSERT_TRUE(max_db_id > 0); int64_t new_id = cbm_gbuf_upsert_node( gb, "Section", "Upgrading", "deltafts.README.Upgrading", "README.md", 1, 9, "{\"docstring\":\"migrates the retention ledger before the cutover\"}"); ASSERT_TRUE(new_id > max_db_id); ASSERT_EQ(cbm_delta_patch(store, proj, gb, max_db_id, NULL, 0), 0); cbm_gbuf_free(gb); /* nodes_fts is contentless, so a column-filtered MATCH is the only way to * observe WHICH column a token landed in — and the only assertion that * distinguishes "indexed" from "indexed without its prose". */ sqlite3_stmt *st = NULL; ASSERT_EQ(sqlite3_prepare_v2(cbm_store_get_db(store), "SELECT rowid FROM nodes_fts WHERE nodes_fts MATCH ?1", -1, &st, NULL), SQLITE_OK); sqlite3_bind_text(st, 1, "body:retention", -1, SQLITE_TRANSIENT); ASSERT_EQ(sqlite3_step(st), SQLITE_ROW); ASSERT_EQ((long long)sqlite3_column_int64(st, 0), (long long)new_id); ASSERT_EQ(sqlite3_step(st), SQLITE_DONE); sqlite3_finalize(st); /* The identifier columns are still written on the same path. */ st = NULL; ASSERT_EQ(sqlite3_prepare_v2(cbm_store_get_db(store), "SELECT COUNT(*) FROM nodes_fts WHERE nodes_fts MATCH ?1", -1, &st, NULL), SQLITE_OK); sqlite3_bind_text(st, 1, "name:Upgrading", -1, SQLITE_TRANSIENT); ASSERT_EQ(sqlite3_step(st), SQLITE_ROW); ASSERT_EQ(sqlite3_column_int(st, 0), 1); sqlite3_finalize(st); cbm_store_close(store); th_rmtree(td); PASS(); } /* End-to-end for #518/#519: source → docstring → properties JSON → nodes_fts * `body` → findable. Each layer has its own test; this one proves they connect. * It is also the guard on the size budget: build_def_props drops an oversized * field ATOMICALLY, so a 500-byte section body that did not fit the 2 KB * properties buffer would vanish silently and every narrower test would still * pass. */ TEST(pipeline_markdown_and_config_prose_reaches_fts_body) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_prose_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); char path[512]; char dbpath[512]; snprintf(dbpath, sizeof(dbpath), "%s/prose.db", tmp); /* A section body well past the 500-byte cap, so the truncating path — the * one that produces the longest properties JSON — is what gets indexed. */ snprintf(path, sizeof(path), "%s/README.md", tmp); FILE *f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "# Installation\n\nThe phlogiston bootstrap provisions a workstation.\n"); for (int i = 0; i < 40; i++) { fprintf(f, "Filler prose line %d that pads the section well past the cap.\n", i); } fclose(f); snprintf(path, sizeof(path), "%s/META.yaml", tmp); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "name: widget\ndescription: Aggregates quicksilver telemetry per shard.\n"); fclose(f); /* One code file so the run is a normal index rather than a docs-only edge. */ snprintf(path, sizeof(path), "%s/main.go", tmp); f = fopen(path, "w"); ASSERT_NOT_NULL(f); fprintf(f, "package main\n\nfunc main() {}\n"); fclose(f); cbm_pipeline_t *p = cbm_pipeline_new(tmp, dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); cbm_store_t *s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); sqlite3_stmt *st = NULL; ASSERT_EQ(sqlite3_prepare_v2(cbm_store_get_db(s), "SELECT COUNT(*) FROM nodes_fts WHERE nodes_fts MATCH ?1", -1, &st, NULL), SQLITE_OK); const char *cases[] = {"body:phlogiston", "body:quicksilver"}; for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { sqlite3_reset(st); sqlite3_bind_text(st, 1, cases[i], -1, SQLITE_TRANSIENT); ASSERT_EQ(sqlite3_step(st), SQLITE_ROW); ASSERT_GT(sqlite3_column_int(st, 0), 0); } sqlite3_finalize(st); cbm_store_close(s); th_rmtree(tmp); PASS(); } /* A graph with no Function or Method nodes at all -- a struct-only or * config-only project -- must run pass_semantic_edges cleanly. Its phase-1 * scan used to hand qsort() a NULL base with count 0, before the func_count * early-out in phase 1b could run. glibc declares qsort's base nonnull, so * UBSan on the Linux leg reports that call; the macOS SDK carries no such * attribute, so the sanitizer is silent there. The label counts pin the * fixture to what it claims: at least one Struct and zero Function/Method, * i.e. the scan genuinely finds nothing to sort. */ /* ── Semantic pass, batched == unbatched ─────────────────────────────── */ /* Under memory pressure the semantic pass tokenizes, counts and vectorizes * in headroom-sized batches of functions (tokenizing twice) instead of * holding every function's tokens at once. The graph must not be able to * tell: the same repo indexed with CBM_SEM_BATCH=5 (forced batches, 30 * functions -> 6 batches) yields byte-identical node vectors, token vectors * and SEMANTICALLY_RELATED edges. Rows are compared by qualified name, never * by node id: parallel extraction hands out ids in worker order. */ static void write_sem_family(const char *base, const char *file, const char *subject) { char path[512]; snprintf(path, sizeof(path), "%s/%s", base, file); char body[4096]; snprintf(body, sizeof(body), "package main\n\n" "// Parse%sConfig reads the %s config file and returns the parsed %s config.\n" "func Parse%sConfig(path string) (*%sConfig, error) {\n" "\treturn Load%sConfig(path)\n}\n\n" "// Load%sConfig loads the %s config from disk and validates it.\n" "func Load%sConfig(path string) (*%sConfig, error) {\n" "\tcfg := &%sConfig{}\n\tValidate%sConfig(cfg)\n\treturn cfg, nil\n}\n\n" "// Validate%sConfig checks the %s config for missing fields.\n" "func Validate%sConfig(cfg *%sConfig) bool {\n\treturn cfg != nil\n}\n\n" "// Write%sConfig serializes the %s config back to disk.\n" "func Write%sConfig(path string, cfg *%sConfig) error {\n" "\tValidate%sConfig(cfg)\n\treturn nil\n}\n\n" /* A near-duplicate of Load: same doc, same body, same calls -- the * pair the pass must relate, in every venue. */ "// Load%sConfigFile loads the %s config from disk and validates it.\n" "func Load%sConfigFile(path string) (*%sConfig, error) {\n" "\tcfg := &%sConfig{}\n\tValidate%sConfig(cfg)\n\treturn cfg, nil\n}\n\n" "type %sConfig struct{ Name string }\n", subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject); th_write_file(path, body); } /* Step both statements in lockstep; every column of every row must match. */ static bool sem_same_rows(sqlite3 *a, sqlite3 *b, const char *sql, int *rows) { sqlite3_stmt *sa = NULL; sqlite3_stmt *sb = NULL; bool same = sqlite3_prepare_v2(a, sql, -1, &sa, NULL) == SQLITE_OK && sqlite3_prepare_v2(b, sql, -1, &sb, NULL) == SQLITE_OK; *rows = 0; while (same) { int ra = sqlite3_step(sa); int rb = sqlite3_step(sb); if (ra != rb) { same = false; break; } if (ra != SQLITE_ROW) { break; } int cols = sqlite3_column_count(sa); for (int c = 0; c < cols && same; c++) { const unsigned char *va = sqlite3_column_text(sa, c); const unsigned char *vb = sqlite3_column_text(sb, c); same = (va == NULL) == (vb == NULL) && (!va || strcmp((const char *)va, (const char *)vb) == 0); } (*rows)++; } sqlite3_finalize(sa); sqlite3_finalize(sb); return same; } TEST(pipeline_semantic_batched_matches_unbatched) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_sembatch_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); static const char *const subjects[] = {"User", "Server", "Client", "Cache", "Queue", "Mail"}; for (size_t i = 0; i < sizeof(subjects) / sizeof(subjects[0]); i++) { char file[64]; snprintf(file, sizeof(file), "%s_config.go", subjects[i]); write_sem_family(tmp, file, subjects[i]); } char db_plain[512]; char db_batched[512]; snprintf(db_plain, sizeof(db_plain), "%s/plain.db", tmp); snprintf(db_batched, sizeof(db_batched), "%s/batched.db", tmp); /* The default threshold (0.75) admits no pair on a 30-function fixture; * 0.3 admits the near-duplicates. The test is about equality, not the bar. */ cbm_setenv("CBM_SEMANTIC_THRESHOLD", "0.3", 1); cbm_unsetenv("CBM_SEM_BATCH"); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_plain, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); p = cbm_pipeline_new(tmp, db_batched, CBM_MODE_FULL); ASSERT_NOT_NULL(p); cbm_setenv("CBM_SEM_BATCH", "5", 1); /* read by the pass, once per run */ int rc = cbm_pipeline_run(p); cbm_unsetenv("CBM_SEM_BATCH"); cbm_unsetenv("CBM_SEMANTIC_THRESHOLD"); cbm_pipeline_free(p); ASSERT_EQ(rc, 0); cbm_store_t *sp = cbm_store_open_path(db_plain); cbm_store_t *sb = cbm_store_open_path(db_batched); ASSERT_NOT_NULL(sp); ASSERT_NOT_NULL(sb); sqlite3 *a = cbm_store_get_db(sp); sqlite3 *b = cbm_store_get_db(sb); /* The fixture must exercise batching: more functions than the forced * batch, and a semantic pass that actually stored vectors and edges. */ sqlite3_stmt *st = NULL; ASSERT_EQ( sqlite3_prepare_v2(a, "SELECT COUNT(*) FROM nodes WHERE label = 'Function'", -1, &st, NULL), SQLITE_OK); ASSERT_EQ(sqlite3_step(st), SQLITE_ROW); int functions = sqlite3_column_int(st, 0); sqlite3_finalize(st); ASSERT_GT(functions, 5); int rows = 0; ASSERT_TRUE(sem_same_rows(a, b, "SELECT n.qualified_name, hex(v.vector) FROM node_vectors v " "JOIN nodes n ON n.id = v.node_id ORDER BY n.qualified_name", &rows)); ASSERT_EQ(rows, functions); ASSERT_TRUE(sem_same_rows( a, b, "SELECT token, hex(vector), idf FROM token_vectors ORDER BY token", &rows)); ASSERT_GT(rows, 0); ASSERT_TRUE( sem_same_rows(a, b, "SELECT s.qualified_name, t.qualified_name, e.properties FROM edges e " "JOIN nodes s ON s.id = e.source_id JOIN nodes t ON t.id = e.target_id " "WHERE e.type = 'SEMANTICALLY_RELATED' ORDER BY 1, 2", &rows)); ASSERT_GT(rows, 0); cbm_store_close(sp); cbm_store_close(sb); th_rmtree(tmp); PASS(); } /* CBM_SEM_PAIR_SIGNALS (+ _FLOOR) is the semantic engine's measuring * harness: it records each scored pair with its signal values, from a floor * below the threshold up. Recording must never change the graph: the same * SEMANTICALLY_RELATED rows with and without it, one admitted line per edge, * and every pair under the threshold recorded but never admitted. */ TEST(pipeline_semantic_pair_signals_never_change_the_graph) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_semsig_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); static const char *const subjects[] = {"User", "Server", "Client", "Cache", "Queue", "Mail"}; for (size_t i = 0; i < sizeof(subjects) / sizeof(subjects[0]); i++) { char file[64]; snprintf(file, sizeof(file), "%s_config.go", subjects[i]); write_sem_family(tmp, file, subjects[i]); } char db_plain[512]; char db_signals[512]; char tsv[512]; snprintf(db_plain, sizeof(db_plain), "%s/plain.db", tmp); snprintf(db_signals, sizeof(db_signals), "%s/signals.db", tmp); snprintf(tsv, sizeof(tsv), "%s/pairs.tsv", tmp); cbm_setenv("CBM_SEMANTIC_THRESHOLD", "0.3", 1); /* as above: admits the near-duplicates */ cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_plain, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); cbm_setenv("CBM_SEM_PAIR_SIGNALS", tsv, 1); cbm_setenv("CBM_SEM_PAIR_SIGNALS_FLOOR", "0.05", 1); p = cbm_pipeline_new(tmp, db_signals, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); cbm_unsetenv("CBM_SEM_PAIR_SIGNALS"); cbm_unsetenv("CBM_SEM_PAIR_SIGNALS_FLOOR"); cbm_unsetenv("CBM_SEMANTIC_THRESHOLD"); cbm_pipeline_free(p); ASSERT_EQ(rc, 0); cbm_store_t *sp = cbm_store_open_path(db_plain); cbm_store_t *ss = cbm_store_open_path(db_signals); ASSERT_NOT_NULL(sp); ASSERT_NOT_NULL(ss); int edges = 0; ASSERT_TRUE( sem_same_rows(cbm_store_get_db(sp), cbm_store_get_db(ss), "SELECT s.qualified_name, t.qualified_name, e.properties FROM edges e " "JOIN nodes s ON s.id = e.source_id JOIN nodes t ON t.id = e.target_id " "WHERE e.type = 'SEMANTICALLY_RELATED' ORDER BY 1, 2", &edges)); ASSERT_GT(edges, 0); cbm_store_close(sp); cbm_store_close(ss); /* qa qb score tfidf ri minhash api type decorator struct proximity * same_file admitted loc_a loc_b */ FILE *f = cbm_fopen(tsv, "r"); ASSERT_NOT_NULL(f); char line[4096]; int lines = 0; int admitted = 0; int below = 0; while (fgets(line, sizeof(line), f)) { char *field[16]; int n = 0; for (char *tok = strtok(line, "\t\n"); tok && n < 16; tok = strtok(NULL, "\t\n")) { field[n++] = tok; } ASSERT_EQ(n, 15); float score = strtof(field[2], NULL); bool is_admitted = strcmp(field[12], "1") == 0; if (score < 0.3F) { below++; ASSERT_FALSE(is_admitted); } admitted += is_admitted ? 1 : 0; lines++; } fclose(f); ASSERT_EQ(admitted, edges); ASSERT_GT(below, 0); /* the floor recorded pairs under the threshold */ ASSERT_GT(lines, admitted); th_rmtree(tmp); PASS(); } /* Every SEMANTICALLY_RELATED edge carries "p": the calibrated probability for * its score AS STORED (three decimals), so one shown score has one p. */ TEST(pipeline_semantic_edges_carry_p) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_semp_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); static const char *const subjects[] = {"User", "Server", "Client", "Cache", "Queue", "Mail"}; for (size_t i = 0; i < sizeof(subjects) / sizeof(subjects[0]); i++) { char file[64]; snprintf(file, sizeof(file), "%s_config.go", subjects[i]); write_sem_family(tmp, file, subjects[i]); } char db[512]; snprintf(db, sizeof(db), "%s/p.db", tmp); cbm_setenv("CBM_SEMANTIC_THRESHOLD", "0.3", 1); /* as above: admits the near-duplicates */ cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); cbm_unsetenv("CBM_SEMANTIC_THRESHOLD"); cbm_pipeline_free(p); ASSERT_EQ(rc, 0); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); sqlite3_stmt *st = NULL; ASSERT_EQ(sqlite3_prepare_v2(cbm_store_get_db(s), "SELECT properties FROM edges WHERE type = 'SEMANTICALLY_RELATED'", -1, &st, NULL), SQLITE_OK); int edges = 0; while (sqlite3_step(st) == SQLITE_ROW) { const char *props = (const char *)sqlite3_column_text(st, 0); ASSERT_NOT_NULL(props); yyjson_doc *doc = yyjson_read(props, strlen(props), 0); ASSERT_NOT_NULL(doc); yyjson_val *score = yyjson_obj_get(yyjson_doc_get_root(doc), "score"); yyjson_val *pv = yyjson_obj_get(yyjson_doc_get_root(doc), "p"); ASSERT_TRUE(score && yyjson_is_num(score)); ASSERT_TRUE(pv && yyjson_is_num(pv)); double want = cbm_sem_calibrated_p((float)yyjson_get_num(score)); ASSERT_FLOAT_EQ(yyjson_get_num(pv), want, 0.006); yyjson_doc_free(doc); edges++; } sqlite3_finalize(st); cbm_store_close(s); ASSERT_GT(edges, 0); th_rmtree(tmp); PASS(); } /* The p a stored candidate row of a section embedding diagrams of family fam * must carry: the curve of the row's kind and position (read from its * evidence) at its score, two decimals; -1 for an unknown kind or position. * *is_function: a function candidate (top 5). */ static double doc_row_want_family(const char *evidence, cbm_sem_doc_format_t fmt, cbm_sem_diag_family_t fam, float score, bool *is_function) { static const struct { const char *name; cbm_sem_doc_kind_t kind; } KINDS[] = {{"\"kind\":\"function\"", CBM_SEM_DOC_KIND_FUNCTION}, {"\"kind\":\"local\"", CBM_SEM_DOC_KIND_LOCAL}, {"\"kind\":\"file\"", CBM_SEM_DOC_KIND_FILE}}; static const struct { const char *name; cbm_sem_doc_pos_t pos; } POSITIONS[] = {{"\"position\":\"global\"", CBM_SEM_DOC_POS_GLOBAL}, {"\"position\":\"local\"", CBM_SEM_DOC_POS_LOCAL}, {"\"position\":\"outside\"", CBM_SEM_DOC_POS_OUTSIDE}}; int k = -1; int p = -1; for (int i = 0; evidence && i < 3; i++) { k = k < 0 && strstr(evidence, KINDS[i].name) ? i : k; p = p < 0 && strstr(evidence, POSITIONS[i].name) ? i : p; } *is_function = k == 0; return k < 0 || p < 0 ? -1.0 : cbm_sem_p_2dp(cbm_sem_doc_section_p(fmt, fam, KINDS[k].kind, POSITIONS[p].pos, score)); } /* doc_row_want_family for a section without a diagram. */ static double doc_row_want(const char *evidence, cbm_sem_doc_format_t fmt, float score, bool *is_function) { return doc_row_want_family(evidence, fmt, CBM_SEM_DIAG_NONE, score, is_function); } /* Count a section's stored doc -> code candidates (section QN containing * heading) and check each row's shape: a function within the top * CBM_SEM_DOC_TOP_K (top: the best of them), score at or above the floor, p * the curve of its kind and position at two decimals, never a test * function. Returns -1 on a bad row. */ static int doc_candidate_rows_for(cbm_store_t *s, const char *heading, char *top, size_t top_sz) { sqlite3_stmt *st = NULL; if (sqlite3_prepare_v2(cbm_store_get_db(s), "SELECT target_qn, rank, score, p, evidence FROM doc_link_candidates " "WHERE instr(section_qn, ?1) > 0 ORDER BY rank", -1, &st, NULL) != SQLITE_OK) { return -1; } sqlite3_bind_text(st, 1, heading, -1, SQLITE_TRANSIENT); int n = 0; bool ok = true; bool have_top = false; while (ok && sqlite3_step(st) == SQLITE_ROW) { const char *target = (const char *)sqlite3_column_text(st, 0); int rank = sqlite3_column_int(st, 1); double score = sqlite3_column_double(st, 2); double p = sqlite3_column_double(st, 3); bool is_function = false; double want = doc_row_want((const char *)sqlite3_column_text(st, 4), CBM_SEM_DOC_FMT_MARKDOWN, (float)score, &is_function); ok = target && rank >= 1 && (!is_function || rank <= CBM_SEM_DOC_TOP_K) && score >= (double)CBM_SEM_DOC_MIN_SCORE && p == want && !strstr(target, ".Test"); if (ok && is_function && !have_top && top) { snprintf(top, top_sz, "%s", target); have_top = true; } n++; } sqlite3_finalize(st); return ok ? n : -1; } /* The doc -> code candidate fixture: six Go config families, a test whose * comment is the section's own text, and a README with a Mail-configuration * section and a Changelog section. */ static void write_doc_candidate_fixture(const char *tmp) { static const char *const subjects[] = {"User", "Server", "Client", "Cache", "Queue", "Mail"}; for (size_t i = 0; i < sizeof(subjects) / sizeof(subjects[0]); i++) { char file[64]; snprintf(file, sizeof(file), "%s_config.go", subjects[i]); write_sem_family(tmp, file, subjects[i]); } /* A test whose comment is the section's own text: the best match of all, * and still never a candidate. */ write_temp_file(tmp, "mail_config_test.go", "package main\n\n" "// TestParseMailConfig: the mail config file is parsed by ParseMailConfig,\n" "// which loads the mail config from disk and validates the mail config\n" "// fields before anything reads them. Missing fields fail the validation,\n" "// and the parsed mail config can be written back to disk afterwards.\n" "func TestParseMailConfig(t *testing.T) {\n" "\tParseMailConfig(\"mail.conf\")\n}\n"); write_temp_file(tmp, "README.md", "# Mail configuration\n\n" "The mail config file is parsed by ParseMailConfig, which loads the mail " "config from disk and validates the mail config fields before anything " "reads them. Missing fields fail the validation, and the parsed mail " "config can be written back to disk afterwards.\n\n" "## Changelog\n\n" "The mail config loader now validates missing fields and writes the mail " "config back to disk after parsing, so a parsed mail config file always " "carries every field it needs.\n"); } /* Doc -> code candidates end to end: a full index publishes a section's * calibrated candidates (the best match first, a test function never, a * changelog section none), and a delta-route reindex keeps them -- its proxy * buffer cannot recompute them, so the clone's rows must survive. */ TEST(pipeline_doc_candidates_published_and_kept_by_delta) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_doccand_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_doc_candidate_fixture(tmp); char db[512]; snprintf(db, sizeof(db), "%s/doccand.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); char top[512] = ""; int mail = doc_candidate_rows_for(s, "Mail-configuration", top, sizeof(top)); int changelog = doc_candidate_rows_for(s, "Changelog", NULL, 0); cbm_store_close(s); ASSERT_GT(mail, 0); ASSERT_NOT_NULL(strstr(top, "Mail")); ASSERT_EQ(changelog, 0); /* Delta route: an edit that keeps every definition name (closure repair * declines added names and new files -- the name must match the fixture's * case exactly, or a case-sensitive filesystem gets a new file); the * candidates are kept as they were. */ char edited[512]; snprintf(edited, sizeof(edited), "%s/User_config.go", tmp); FILE *ef = cbm_fopen(edited, "a"); ASSERT_NOT_NULL(ef); (void)fputs("\n// edited after the first index\n", ef); (void)fclose(ef); cbm_pipeline_incremental_test_reset_faults(); p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); cbm_pipeline_free(p); s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); char top_after[512] = ""; int mail_after = doc_candidate_rows_for(s, "Mail-configuration", top_after, sizeof(top_after)); cbm_store_close(s); ASSERT_EQ(mail_after, mail); ASSERT_STR_EQ(top_after, top); th_rmtree(tmp); cbm_pipeline_incremental_test_reset_faults(); PASS(); } /* Every doc_link_candidates row in a store (0 without the table); -1 on an * error. */ static int doc_candidate_row_count(cbm_store_t *s) { sqlite3 *db = cbm_store_get_db(s); sqlite3_stmt *st = NULL; if (sqlite3_prepare_v2(db, "SELECT count(*) FROM sqlite_master WHERE type='table' AND " "name='doc_link_candidates'", -1, &st, NULL) != SQLITE_OK) { return -1; } bool has_table = sqlite3_step(st) == SQLITE_ROW && sqlite3_column_int(st, 0) > 0; sqlite3_finalize(st); if (!has_table) { return 0; } if (sqlite3_prepare_v2(db, "SELECT count(*) FROM doc_link_candidates", -1, &st, NULL) != SQLITE_OK) { return -1; } int n = sqlite3_step(st) == SQLITE_ROW ? sqlite3_column_int(st, 0) : -1; sqlite3_finalize(st); return n; } /* doc_candidates off: the same repository indexed with the switch off has no * candidate rows and exactly the nodes and edges of the default run, which has * candidates (the default is on in full mode). */ TEST(pipeline_doc_candidates_off_stores_none_and_keeps_graph) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_doccand_off_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_doc_candidate_fixture(tmp); /* The databases live outside the indexed tree, so both runs discover the * same files. */ char dbdir[256]; snprintf(dbdir, sizeof(dbdir), "/tmp/cbm_doccand_db_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(dbdir)); char db_on[512]; char db_off[512]; snprintf(db_on, sizeof(db_on), "%s/on.db", dbdir); snprintf(db_off, sizeof(db_off), "%s/off.db", dbdir); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_on, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT(cbm_pipeline_doc_candidates_enabled(p)); ASSERT_EQ(cbm_pipeline_run(p), 0); char project[512]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(p)); cbm_pipeline_free(p); p = cbm_pipeline_new(tmp, db_off, CBM_MODE_FULL); ASSERT_NOT_NULL(p); cbm_pipeline_set_doc_candidates_enabled(p, false); ASSERT(!cbm_pipeline_doc_candidates_enabled(p)); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); cbm_store_t *on = cbm_store_open_path(db_on); cbm_store_t *off = cbm_store_open_path(db_off); ASSERT_NOT_NULL(on); ASSERT_NOT_NULL(off); int rows_on = doc_candidate_row_count(on); int rows_off = doc_candidate_row_count(off); int nodes_on = cbm_store_count_nodes(on, project); int nodes_off = cbm_store_count_nodes(off, project); int edges_on = cbm_store_count_edges(on, project); int edges_off = cbm_store_count_edges(off, project); cbm_store_close(on); cbm_store_close(off); ASSERT_GT(rows_on, 0); ASSERT_EQ(rows_off, 0); ASSERT_GT(nodes_on, 0); ASSERT_EQ(nodes_off, nodes_on); ASSERT_EQ(edges_off, edges_on); th_rmtree(tmp); th_rmtree(dbdir); cbm_pipeline_incremental_test_reset_faults(); PASS(); } /* doc_candidates off on a delta-route reindex: the route that otherwise keeps * the clone's candidate rows replaces them by none, so switching off leaves no * stale candidates behind. */ TEST(pipeline_doc_candidates_off_clears_rows_on_delta) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_doccand_delta_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_doc_candidate_fixture(tmp); char db[512]; snprintf(db, sizeof(db), "%s/doccand.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); int before = doc_candidate_row_count(s); cbm_store_close(s); ASSERT_GT(before, 0); /* The same name-preserving edit as the delta test above: the closure * repair route. */ char edited[512]; snprintf(edited, sizeof(edited), "%s/User_config.go", tmp); FILE *ef = cbm_fopen(edited, "a"); ASSERT_NOT_NULL(ef); (void)fputs("\n// edited after the first index\n", ef); (void)fclose(ef); cbm_pipeline_incremental_test_reset_faults(); p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); cbm_pipeline_set_doc_candidates_enabled(p, false); ASSERT_EQ(cbm_pipeline_run(p), 0); ASSERT_EQ(cbm_pipeline_incremental_test_last_route(), CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR); cbm_pipeline_free(p); s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); int after = doc_candidate_row_count(s); cbm_store_close(s); ASSERT_EQ(after, 0); th_rmtree(tmp); cbm_pipeline_incremental_test_reset_faults(); PASS(); } /* One index run of the doc-candidate fixture in tmp into db. set < 0 leaves * doc_candidates unset (the index mode decides); 0 / 1 set it explicitly. */ typedef struct { bool enabled_before; /* the switch as the pipeline reports it before the run */ int rows; /* doc_link_candidates rows; -1 on an error */ int sem; /* SEMANTICALLY_RELATED edges */ int sim; /* SIMILAR_TO edges */ int nodes; int edges; } doc_mode_run_t; static doc_mode_run_t doc_candidates_mode_run(const char *tmp, const char *db, cbm_index_mode_t mode, int set) { doc_mode_run_t r = {.rows = -1}; cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, mode); if (!p) { return r; } if (set >= 0) { cbm_pipeline_set_doc_candidates_enabled(p, set == 1); } r.enabled_before = cbm_pipeline_doc_candidates_enabled(p); /* A threshold low enough that the fixture's near-duplicates relate, so * the SEMANTICALLY_RELATED count shows whether the edge phases ran. */ cbm_setenv("CBM_SEMANTIC_THRESHOLD", "0.3", 1); int rc = cbm_pipeline_run(p); cbm_unsetenv("CBM_SEMANTIC_THRESHOLD"); char project[512]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(p)); cbm_pipeline_free(p); cbm_store_t *s = rc == 0 ? cbm_store_open_path(db) : NULL; if (!s) { return r; } r.rows = doc_candidate_row_count(s); r.sem = cbm_store_count_edges_by_type(s, project, "SEMANTICALLY_RELATED"); r.sim = cbm_store_count_edges_by_type(s, project, "SIMILAR_TO"); r.nodes = cbm_store_count_nodes(s, project); r.edges = cbm_store_count_edges(s, project); cbm_store_close(s); return r; } /* doc_candidates unset: the index mode decides -- a full index stores * candidate rows, moderate and fast store none. Moderate still runs the * semantic pass (its SEMANTICALLY_RELATED edges), only without the doc * phase. Each run writes a fresh database outside the indexed tree. */ TEST(pipeline_doc_candidates_default_follows_mode) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_doccand_mode_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_doc_candidate_fixture(tmp); char dbdir[256]; snprintf(dbdir, sizeof(dbdir), "/tmp/cbm_doccand_mdb_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(dbdir)); char db_full[512]; char db_moderate[512]; char db_fast[512]; snprintf(db_full, sizeof(db_full), "%s/full.db", dbdir); snprintf(db_moderate, sizeof(db_moderate), "%s/moderate.db", dbdir); snprintf(db_fast, sizeof(db_fast), "%s/fast.db", dbdir); doc_mode_run_t full = doc_candidates_mode_run(tmp, db_full, CBM_MODE_FULL, -1); doc_mode_run_t moderate = doc_candidates_mode_run(tmp, db_moderate, CBM_MODE_MODERATE, -1); doc_mode_run_t fast = doc_candidates_mode_run(tmp, db_fast, CBM_MODE_FAST, -1); th_rmtree(tmp); th_rmtree(dbdir); cbm_pipeline_incremental_test_reset_faults(); ASSERT_TRUE(full.enabled_before); ASSERT_GT(full.rows, 0); ASSERT_FALSE(moderate.enabled_before); ASSERT_EQ(moderate.rows, 0); ASSERT_GT(moderate.sem, 0); ASSERT_FALSE(fast.enabled_before); ASSERT_EQ(fast.rows, 0); ASSERT_EQ(fast.sem, 0); PASS(); } /* An explicit doc_candidates wins over the mode in both directions: full + * off stores none, moderate + on and fast + on store rows. Fast keeps its * contract -- the doc-only semantic pass adds no SEMANTICALLY_RELATED or * SIMILAR_TO edge, and the graph is exactly fast's default graph. */ TEST(pipeline_doc_candidates_explicit_wins_over_mode) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_doccand_expl_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_doc_candidate_fixture(tmp); char dbdir[256]; snprintf(dbdir, sizeof(dbdir), "/tmp/cbm_doccand_edb_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(dbdir)); char db_full_off[512]; char db_moderate_on[512]; char db_fast_on[512]; char db_fast[512]; snprintf(db_full_off, sizeof(db_full_off), "%s/full_off.db", dbdir); snprintf(db_moderate_on, sizeof(db_moderate_on), "%s/moderate_on.db", dbdir); snprintf(db_fast_on, sizeof(db_fast_on), "%s/fast_on.db", dbdir); snprintf(db_fast, sizeof(db_fast), "%s/fast.db", dbdir); doc_mode_run_t full_off = doc_candidates_mode_run(tmp, db_full_off, CBM_MODE_FULL, 0); doc_mode_run_t moderate_on = doc_candidates_mode_run(tmp, db_moderate_on, CBM_MODE_MODERATE, 1); doc_mode_run_t fast_on = doc_candidates_mode_run(tmp, db_fast_on, CBM_MODE_FAST, 1); doc_mode_run_t fast = doc_candidates_mode_run(tmp, db_fast, CBM_MODE_FAST, -1); th_rmtree(tmp); th_rmtree(dbdir); cbm_pipeline_incremental_test_reset_faults(); ASSERT_FALSE(full_off.enabled_before); ASSERT_EQ(full_off.rows, 0); ASSERT_TRUE(moderate_on.enabled_before); ASSERT_GT(moderate_on.rows, 0); ASSERT_GT(moderate_on.sem, 0); ASSERT_TRUE(fast_on.enabled_before); ASSERT_GT(fast_on.rows, 0); ASSERT_EQ(fast_on.sem, 0); ASSERT_EQ(fast_on.sim, 0); ASSERT_EQ(fast.rows, 0); ASSERT_GT(fast.nodes, 0); ASSERT_EQ(fast_on.nodes, fast.nodes); ASSERT_EQ(fast_on.edges, fast.edges); PASS(); } #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API /* The incremental legacy-partial route mirrors the full path: a fast reindex * with doc_candidates explicitly on recomputes candidate rows over the * rehydrated project and still adds no SEMANTICALLY_RELATED / SIMILAR_TO * edge. */ TEST(pipeline_doc_candidates_fast_on_legacy_incremental) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_doccand_finc_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); write_doc_candidate_fixture(tmp); char dbdir[256]; snprintf(dbdir, sizeof(dbdir), "/tmp/cbm_doccand_fidb_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(dbdir)); char db[512]; snprintf(db, sizeof(db), "%s/fast.db", dbdir); doc_mode_run_t first = doc_candidates_mode_run(tmp, db, CBM_MODE_FAST, -1); char edited[512]; snprintf(edited, sizeof(edited), "%s/User_config.go", tmp); FILE *ef = cbm_fopen(edited, "a"); ASSERT_NOT_NULL(ef); (void)fputs("\n// edited after the first index\n", ef); (void)fclose(ef); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_incremental_test_force_legacy_partial_once(); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FAST); ASSERT_NOT_NULL(p); cbm_pipeline_set_doc_candidates_enabled(p, true); cbm_setenv("CBM_SEMANTIC_THRESHOLD", "0.3", 1); int rc = cbm_pipeline_run(p); cbm_unsetenv("CBM_SEMANTIC_THRESHOLD"); ASSERT_EQ(rc, 0); cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route(); char project[512]; snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(p)); cbm_pipeline_free(p); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); int rows = doc_candidate_row_count(s); int sem = cbm_store_count_edges_by_type(s, project, "SEMANTICALLY_RELATED"); int sim = cbm_store_count_edges_by_type(s, project, "SIMILAR_TO"); cbm_store_close(s); th_rmtree(tmp); th_rmtree(dbdir); cbm_pipeline_incremental_test_reset_faults(); ASSERT_EQ(first.rows, 0); ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_LEGACY_PARTIAL); ASSERT_GT(rows, 0); ASSERT_EQ(sem, 0); ASSERT_EQ(sim, 0); PASS(); } #endif /* One Python family per subject: parse / load / validate / write, each with * a docstring, so the functions' corpus has the subject words to match. */ static void write_py_family(const char *base, const char *subject) { char path[512]; snprintf(path, sizeof(path), "%s/%s.py", base, subject); char body[4096]; snprintf(body, sizeof(body), "def parse_%s_config(path):\n" " \"\"\"Parse the %s config file and return the parsed %s settings.\"\"\"\n" " return load_%s_config(path)\n\n" "def load_%s_config(path):\n" " \"\"\"Load the %s config from disk and validate every %s field.\"\"\"\n" " cfg = {}\n validate_%s_config(cfg)\n return cfg\n\n" "def validate_%s_config(cfg):\n" " \"\"\"Check the %s config for missing fields.\"\"\"\n" " return cfg is not None\n\n" "def write_%s_config(path, cfg):\n" " \"\"\"Write the %s config back to disk after validating it.\"\"\"\n" " validate_%s_config(cfg)\n", subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject, subject); th_write_file(path, body); } /* The candidate count, and the bad rows, of one section (its QN contains * heading and its file ends in ext): p must be the format's curve value at * two decimals and the tfidf at or above the format's first stored band. */ static int doc_rows_of(cbm_store_t *s, const char *heading, const char *ext, cbm_sem_doc_format_t fmt, const char *forbidden_target, int *bad) { sqlite3_stmt *st = NULL; *bad = 0; if (sqlite3_prepare_v2( cbm_store_get_db(s), "SELECT c.target_qn, c.score, c.p, c.evidence FROM doc_link_candidates c " "JOIN nodes n ON n.qualified_name = c.section_qn " "WHERE instr(c.section_qn, ?1) > 0 AND n.file_path LIKE ?2", -1, &st, NULL) != SQLITE_OK) { return -1; } char like[32]; snprintf(like, sizeof(like), "%%%s", ext); sqlite3_bind_text(st, 1, heading, -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 2, like, -1, SQLITE_TRANSIENT); int n = 0; while (sqlite3_step(st) == SQLITE_ROW) { const char *target = (const char *)sqlite3_column_text(st, 0); float score = (float)sqlite3_column_double(st, 1); double p = sqlite3_column_double(st, 2); bool is_function = false; double want = doc_row_want((const char *)sqlite3_column_text(st, 3), fmt, score, &is_function); *bad += want <= 0.0 || p != want || (forbidden_target && target && strstr(target, forbidden_target)); n++; } sqlite3_finalize(st); return n; } /* Candidates never repeat an exact link: a section whose code span names a * function (a MENTIONS edge) does not also list that function as a * candidate. And each format carries its own judged curve: reST stores only * from tfidf 0.30, with reST's p. */ TEST(pipeline_doc_candidates_skip_exact_links_and_use_format_curves) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_docfmt_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); static const char *const subjects[] = {"mail", "cache", "queue", "user", "server", "client"}; for (size_t i = 0; i < sizeof(subjects) / sizeof(subjects[0]); i++) { write_py_family(tmp, subjects[i]); } char docs[512]; snprintf(docs, sizeof(docs), "%s/docs", tmp); ASSERT_TRUE(cbm_mkdir_p(docs, 0755)); write_temp_file(tmp, "docs/mail.md", "# Mail settings\n\n" "The mail config file is parsed by `mail.parse_mail_config`, which loads " "the mail config from disk and validates every mail field before the " "parsed mail settings are returned to the caller for use.\n"); write_temp_file(tmp, "docs/mail.rst", "Mail loading\n============\n\n" "Load the mail config from disk and validate every mail field: missing " "mail fields fail the validation, and the loaded mail config is written " "back to disk after validating it.\n"); char db[512]; snprintf(db, sizeof(db), "%s/docfmt.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); /* the exact link exists ... */ sqlite3_stmt *st = NULL; ASSERT_EQ(sqlite3_prepare_v2(cbm_store_get_db(s), "SELECT count(*) FROM edges e JOIN nodes a ON a.id = e.source_id " "JOIN nodes b ON b.id = e.target_id WHERE e.type = 'MENTIONS' " "AND a.label = 'Section' AND a.file_path = 'docs/mail.md' " "AND b.name = 'parse_mail_config'", -1, &st, NULL), SQLITE_OK); ASSERT_EQ(sqlite3_step(st), SQLITE_ROW); int linked = sqlite3_column_int(st, 0); sqlite3_finalize(st); /* ... and is not repeated as a candidate, while the rest of the section's * candidates are stored on the Markdown curve */ int md_bad = 0; int rst_bad = 0; int md = doc_rows_of(s, "Mail-settings", ".md", CBM_SEM_DOC_FMT_MARKDOWN, ".parse_mail_config", &md_bad); int rst = doc_rows_of(s, "Mail-loading", ".rst", CBM_SEM_DOC_FMT_RST, NULL, &rst_bad); cbm_store_close(s); th_rmtree(tmp); cbm_pipeline_incremental_test_reset_faults(); ASSERT_EQ(linked, 1); ASSERT_GT(md, 0); ASSERT_EQ(md_bad, 0); ASSERT_GT(rst, 0); ASSERT_EQ(rst_bad, 0); PASS(); } /* The rows of a section (its QN contains heading): their count; *bad the * rows off the curve of the section's diagram family fam; *own the rows * whose p differs from a section without a diagram. */ static int doc_family_rows(cbm_store_t *s, const char *heading, cbm_sem_diag_family_t fam, int *bad, int *own) { sqlite3_stmt *st = NULL; *bad = 0; *own = 0; if (sqlite3_prepare_v2(cbm_store_get_db(s), "SELECT score, p, evidence FROM doc_link_candidates " "WHERE instr(section_qn, ?1) > 0", -1, &st, NULL) != SQLITE_OK) { return -1; } sqlite3_bind_text(st, 1, heading, -1, SQLITE_TRANSIENT); int n = 0; while (sqlite3_step(st) == SQLITE_ROW) { float score = (float)sqlite3_column_double(st, 0); double p = sqlite3_column_double(st, 1); const char *ev = (const char *)sqlite3_column_text(st, 2); bool is_function = false; double want = doc_row_want_family(ev, CBM_SEM_DOC_FMT_MARKDOWN, fam, score, &is_function); *bad += want <= 0.0 || p != want; *own += want != doc_row_want(ev, CBM_SEM_DOC_FMT_MARKDOWN, score, &is_function); n++; } sqlite3_finalize(st); return n; } /* The diagram_kind of the Diagram a section (its QN contains heading) * DEFINES, "" without one. */ static void doc_section_diagram_kind(cbm_store_t *s, const char *heading, char *out, size_t out_sz) { sqlite3_stmt *st = NULL; out[0] = '\0'; if (sqlite3_prepare_v2(cbm_store_get_db(s), "SELECT json_extract(d.properties, '$.diagram_kind') FROM edges e " "JOIN nodes a ON a.id = e.source_id JOIN nodes d ON d.id = e.target_id " "WHERE e.type = 'DEFINES' AND a.label = 'Section' " "AND d.label = 'Diagram' AND instr(a.qualified_name, ?1) > 0", -1, &st, NULL) != SQLITE_OK) { return; } sqlite3_bind_text(st, 1, heading, -1, SQLITE_TRANSIENT); if (sqlite3_step(st) == SQLITE_ROW && sqlite3_column_text(st, 0)) { snprintf(out, out_sz, "%s", (const char *)sqlite3_column_text(st, 0)); } sqlite3_finalize(st); } /* A Markdown section that embeds a diagram takes its diagram family's judged * curve (DIAG-C): a sequence diagram is structural, a flowchart is prose; a * section without a diagram keeps the Markdown curve. Some rows of each * diagram section must carry a p the Markdown curve would not give. */ TEST(pipeline_doc_candidates_diagram_family_curves) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_docdiag_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); static const char *const subjects[] = {"mail", "cache", "queue", "user", "server", "client"}; for (size_t i = 0; i < sizeof(subjects) / sizeof(subjects[0]); i++) { write_py_family(tmp, subjects[i]); } char docs[512]; snprintf(docs, sizeof(docs), "%s/docs", tmp); ASSERT_TRUE(cbm_mkdir_p(docs, 0755)); write_temp_file( tmp, "docs/mail.md", "# Mail loading\n\n" "Load the mail config from disk and validate every mail field before the " "parsed mail settings are returned; missing mail fields fail the validation.\n\n" "```mermaid\nsequenceDiagram\n" " participant Loader\n participant Validator\n" " Loader->>Validator: validate mail config\n```\n\n" "# Mail writing\n\n" "Write the mail config back to disk after validating it, so the written " "mail config file always carries every mail field it needs.\n\n" "```mermaid\nflowchart TD\n" " validate[validate mail config] --> write[write mail config]\n```\n\n" "# Mail parsing\n\n" "Parse the mail config file and return the parsed mail settings; the mail " "config is loaded from disk and every mail field is validated first.\n"); char db[512]; snprintf(db, sizeof(db), "%s/docdiag.db", tmp); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); char seq_kind[32]; char flow_kind[32]; char none_kind[32]; doc_section_diagram_kind(s, "Mail-loading", seq_kind, sizeof(seq_kind)); doc_section_diagram_kind(s, "Mail-writing", flow_kind, sizeof(flow_kind)); doc_section_diagram_kind(s, "Mail-parsing", none_kind, sizeof(none_kind)); int seq_bad = 0; int seq_own = 0; int flow_bad = 0; int flow_own = 0; int none_bad = 0; int none_own = 0; int seq = doc_family_rows(s, "Mail-loading", CBM_SEM_DIAG_STRUCTURAL, &seq_bad, &seq_own); int flow = doc_family_rows(s, "Mail-writing", CBM_SEM_DIAG_PROSE, &flow_bad, &flow_own); int none = doc_family_rows(s, "Mail-parsing", CBM_SEM_DIAG_NONE, &none_bad, &none_own); cbm_store_close(s); th_rmtree(tmp); cbm_pipeline_incremental_test_reset_faults(); ASSERT_STR_EQ(seq_kind, "sequence"); ASSERT_STR_EQ(flow_kind, "flowchart"); ASSERT_STR_EQ(none_kind, ""); ASSERT_EQ(cbm_sem_diag_family("mermaid", seq_kind), CBM_SEM_DIAG_STRUCTURAL); ASSERT_EQ(cbm_sem_diag_family("mermaid", flow_kind), CBM_SEM_DIAG_PROSE); ASSERT_GT(seq, 0); ASSERT_EQ(seq_bad, 0); ASSERT_GT(seq_own, 0); ASSERT_GT(flow, 0); ASSERT_EQ(flow_bad, 0); ASSERT_GT(flow_own, 0); ASSERT_GT(none, 0); ASSERT_EQ(none_bad, 0); ASSERT_EQ(none_own, 0); PASS(); } /* Lines of a CBM_SEM_DOC_SIGNALS dump whose section file, kind, position, * home and target file equal the given values (NULL: any). */ static int doc_signal_count(const char *dump, const char *sec_file, const char *kind, const char *pos, const char *home, const char *target_file) { FILE *f = cbm_fopen(dump, "r"); if (!f) { return -1; } char line[4096]; int n = 0; while (fgets(line, sizeof(line), f)) { line[strcspn(line, "\r\n")] = '\0'; char *col[12]; int nc = 0; for (char *p = line; nc < 12; nc++) { col[nc] = p; char *tab = strchr(p, '\t'); if (!tab) { nc++; break; } *tab = '\0'; p = tab + 1; } if (nc != 12) { continue; } char *colon = strrchr(col[5], ':'); if (colon) { *colon = '\0'; } colon = strrchr(col[6], ':'); if (colon) { *colon = '\0'; } n += (!sec_file || strcmp(col[5], sec_file) == 0) && (!kind || strcmp(col[9], kind) == 0) && (!pos || strcmp(col[10], pos) == 0) && (!home || strcmp(col[11], home) == 0) && (!target_file || strcmp(col[6], target_file) == 0); } fclose(f); return n; } /* Document position: a doc's home is its folder (a docs folder documents its * parent; a folder without code climbs to one with code; the root = the * whole project); a candidate inside the home is local, elsewhere outside; * a doc with a home below the root gets its home folder as a candidate; * whole files are candidates; local functions below the top 5 are added. */ TEST(pipeline_doc_candidates_home_position_and_kinds) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_docpos_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); char dir[512]; static const char *const pkgs[] = {"pkg/mail", "pkg/mail/sub", "pkg/cache", "pkg/queue", "pkg/mail/docs", "docs", "guides"}; for (size_t i = 0; i < sizeof(pkgs) / sizeof(pkgs[0]); i++) { snprintf(dir, sizeof(dir), "%s/%s", tmp, pkgs[i]); ASSERT_TRUE(cbm_mkdir_p(dir, 0755)); } snprintf(dir, sizeof(dir), "%s/pkg/mail", tmp); write_py_family(dir, "mail"); snprintf(dir, sizeof(dir), "%s/pkg/mail/sub", tmp); write_py_family(dir, "mail"); snprintf(dir, sizeof(dir), "%s/pkg/cache", tmp); write_py_family(dir, "cache"); snprintf(dir, sizeof(dir), "%s/pkg/queue", tmp); write_py_family(dir, "queue"); static const char body[] = "The mail config file is parsed, loaded from disk and validated: every mail " "field is checked, missing mail fields fail the validation, and the parsed mail " "settings are written back to disk after validating the mail config.\n"; static const char *const doc_files[] = {"pkg/mail/README.md", "pkg/mail/docs/guide.md", "docs/overview.md", "guides/notes.md"}; for (size_t i = 0; i < sizeof(doc_files) / sizeof(doc_files[0]); i++) { char text[1024]; snprintf(text, sizeof(text), "# Mail config %d\n\n%s", (int)i, body); write_temp_file(tmp, doc_files[i], text); } char db[512]; char dump[512]; snprintf(db, sizeof(db), "%s/docpos.db", tmp); snprintf(dump, sizeof(dump), "%s/signals.tsv", tmp); cbm_setenv("CBM_SEM_DOC_SIGNALS", dump, 1); cbm_pipeline_incremental_test_reset_faults(); cbm_pipeline_t *p = cbm_pipeline_new(tmp, db, CBM_MODE_FULL); ASSERT_NOT_NULL(p); int rc = cbm_pipeline_run(p); cbm_pipeline_free(p); cbm_unsetenv("CBM_SEM_DOC_SIGNALS"); ASSERT_EQ(rc, 0); const char *readme = "pkg/mail/README.md"; const char *guide = "pkg/mail/docs/guide.md"; /* homes: the package; its docs folder documents the package; top-level * docs and a folder without code document the whole project */ int readme_lines = doc_signal_count(dump, readme, NULL, NULL, NULL, NULL); int readme_home = doc_signal_count(dump, readme, NULL, NULL, "pkg/mail", NULL); int guide_lines = doc_signal_count(dump, guide, NULL, NULL, NULL, NULL); int guide_home = doc_signal_count(dump, guide, NULL, NULL, "pkg/mail", NULL); int overview_lines = doc_signal_count(dump, "docs/overview.md", NULL, NULL, NULL, NULL); int overview_root = doc_signal_count(dump, "docs/overview.md", NULL, "global", ".", NULL); int notes_lines = doc_signal_count(dump, "guides/notes.md", NULL, NULL, NULL, NULL); int notes_root = doc_signal_count(dump, "guides/notes.md", NULL, "global", ".", NULL); /* positions */ int local_fn = doc_signal_count(dump, readme, "function", "local", "pkg/mail", "pkg/mail/mail.py"); int local_sub = doc_signal_count(dump, readme, NULL, "local", "pkg/mail", "pkg/mail/sub/mail.py"); int outside_fn = doc_signal_count(dump, readme, "function", "outside", NULL, NULL); int wrong_local = doc_signal_count(dump, readme, NULL, "local", NULL, "pkg/cache/cache.py"); /* kinds */ int folders = doc_signal_count(dump, NULL, "folder", "local", NULL, "pkg/mail"); int folder_readme = doc_signal_count(dump, readme, "folder", "local", "pkg/mail", "pkg/mail"); int root_folders = doc_signal_count(dump, "docs/overview.md", "folder", NULL, NULL, NULL) + doc_signal_count(dump, "guides/notes.md", "folder", NULL, NULL, NULL); int files = doc_signal_count(dump, readme, "file", "local", NULL, NULL); int extras = doc_signal_count(dump, readme, "local", "local", "pkg/mail", NULL); /* stored: the README's home folder (a README: p 0.87), not the guide's * (another doc), no function outside the home, every row on its curve */ cbm_store_t *s = cbm_store_open_path(db); ASSERT_NOT_NULL(s); sqlite3_stmt *st = NULL; ASSERT_EQ(sqlite3_prepare_v2(cbm_store_get_db(s), "SELECT c.score, c.p, c.evidence, n.file_path FROM " "doc_link_candidates c JOIN nodes n ON n.qualified_name = " "c.section_qn", -1, &st, NULL), SQLITE_OK); int stored_folders = 0; int stored_outside = 0; int off_curve = 0; double folder_p = 0.0; while (sqlite3_step(st) == SQLITE_ROW) { const char *ev = (const char *)sqlite3_column_text(st, 2); const char *path = (const char *)sqlite3_column_text(st, 3); double p = sqlite3_column_double(st, 1); if (ev && strstr(ev, "\"kind\":\"folder\"")) { stored_folders++; folder_p = p; off_curve += strcmp(path, readme) != 0; continue; } stored_outside += ev && strstr(ev, "\"position\":\"outside\"") && strstr(ev, "\"kind\":\"function\""); bool is_function = false; off_curve += p != doc_row_want(ev, CBM_SEM_DOC_FMT_MARKDOWN, (float)sqlite3_column_double(st, 0), &is_function); } sqlite3_finalize(st); cbm_store_close(s); th_rmtree(tmp); cbm_pipeline_incremental_test_reset_faults(); ASSERT_GT(readme_lines, 0); ASSERT_EQ(readme_home, readme_lines); ASSERT_GT(guide_lines, 0); ASSERT_EQ(guide_home, guide_lines); ASSERT_GT(overview_lines, 0); ASSERT_EQ(overview_root, overview_lines); ASSERT_GT(notes_lines, 0); ASSERT_EQ(notes_root, notes_lines); ASSERT_GT(local_fn, 0); ASSERT_GT(local_sub, 0); ASSERT_GT(outside_fn, 0); ASSERT_EQ(wrong_local, 0); ASSERT_EQ(folders, 2); /* README and the docs guide; one per doc, never per section */ ASSERT_EQ(folder_readme, 1); ASSERT_EQ(root_folders, 0); ASSERT_GT(files, 0); ASSERT_GT(extras, 0); ASSERT_EQ(stored_folders, 1); ASSERT_FLOAT_EQ(folder_p, 0.87, 1e-9); ASSERT_EQ(stored_outside, 0); ASSERT_EQ(off_curve, 0); PASS(); } TEST(pipeline_semantic_edges_no_functions) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_nofunc_XXXXXX"); ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); char path[512]; char dbpath[512]; snprintf(dbpath, sizeof(dbpath), "%s/nofunc.db", tmp); snprintf(path, sizeof(path), "%s/main.go", tmp); ASSERT_EQ(th_write_file(path, "package main\n\ntype Widget struct{}\n"), 0); cbm_pipeline_t *p = cbm_pipeline_new(tmp, dbpath, CBM_MODE_FULL); ASSERT_NOT_NULL(p); ASSERT_EQ(cbm_pipeline_run(p), 0); cbm_pipeline_free(p); cbm_store_t *s = cbm_store_open_path(dbpath); ASSERT_NOT_NULL(s); sqlite3_stmt *st = NULL; ASSERT_EQ(sqlite3_prepare_v2(cbm_store_get_db(s), "SELECT COUNT(*) FROM nodes WHERE label = ?1", -1, &st, NULL), SQLITE_OK); const char *labels[] = {"Struct", "Function", "Method"}; int counts[3] = {0, 0, 0}; for (size_t i = 0; i < sizeof(labels) / sizeof(labels[0]); i++) { sqlite3_reset(st); sqlite3_bind_text(st, 1, labels[i], -1, SQLITE_TRANSIENT); ASSERT_EQ(sqlite3_step(st), SQLITE_ROW); counts[i] = sqlite3_column_int(st, 0); } sqlite3_finalize(st); cbm_store_close(s); ASSERT_GT(counts[0], 0); ASSERT_EQ(counts[1], 0); ASSERT_EQ(counts[2], 0); th_rmtree(tmp); 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), ®ions); 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), ®ions); 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), ®ions); 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), ®ions); 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), ®ions); 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), ®ions); ASSERT_STR_EQ("1-2,+7", joined); PASS(); } #endif /* #1153: a C++ method called on the object a static factory returns * (`SecdManager::getInstance()->m()`, `Registry::instance().m()`, * `auto w = Widget::create(); w->m()`) got no CALLS edge when the class lives * in another file. The class is declared in a header, its methods are defined * out of line in a .cpp, and the call is in a third file -- the reporter's * layout. The cross-file C++ LSP resolved `Class::member` only under the * caller's own module QN or as a bare QN, so the static call missed, its return * type stayed unknown, and every receiver built from it dropped the method * call. In one file the same code already resolved. * * `Other` declares the same method names in the same header: a correct * resolution must never bind them. `Twin` is declared by two libraries, so * `Twin::get()` names no single class and must stay unresolved. `Mystery` is * never defined, so `m->paint()` has no receiver type to dispatch on. 52 * fillers + CBM_WORKERS=4 force the parallel path; each test runs both. */ /* write_temp_file creates one directory level; this layout nests two. */ static void cpp_recv_write(const char *tmp, const char *name, const char *content) { char dir[512]; snprintf(dir, sizeof(dir), "%s/%s", tmp, name); char *slash = strrchr(dir, '/'); if (slash) { *slash = '\0'; cbm_mkdir_p(dir, 0755); } write_temp_file(tmp, name, content); } static void cpp_recv_write_fixture(const char *tmp, bool parallel) { cpp_recv_write(tmp, "app/src/SecdManager.h", "#pragma once\n" "#include \n\n" "class SecdManager {\n" "public:\n" " static SecdManager *getInstance();\n" " bool secd_retrieve_pic_fpc_i2c_id(int ifdh, int &fpc_id, int &pic_id);\n" "};\n\n" "class Registry {\n" "public:\n" " static Registry &instance();\n" " void registerItem(int id);\n" "};\n\n" "class Widget {\n" "public:\n" " static std::unique_ptr create();\n" " void paint();\n" "};\n\n" "class Session {\n" "public:\n" " static std::shared_ptr open();\n" " void close();\n" "};\n\n" "class Other {\n" "public:\n" " bool secd_retrieve_pic_fpc_i2c_id(int ifdh, int &fpc_id, int &pic_id);\n" " void registerItem(int id);\n" " void paint();\n" " void close();\n" "};\n"); cpp_recv_write( tmp, "app/src/SecdManager.cpp", "#include \"SecdManager.h\"\n\n" "SecdManager *SecdManager::getInstance() {\n" " static SecdManager inst;\n" " return &inst;\n" "}\n\n" "bool SecdManager::secd_retrieve_pic_fpc_i2c_id(int ifdh, int &fpc_id, " "int &pic_id) {\n" " fpc_id = ifdh;\n" " pic_id = ifdh;\n" " return true;\n" "}\n\n" "Registry &Registry::instance() {\n" " static Registry r;\n" " return r;\n" "}\n\n" "void Registry::registerItem(int id) { (void)id; }\n\n" "std::unique_ptr Widget::create() { return std::make_unique(); }\n\n" "void Widget::paint() {}\n\n" "std::shared_ptr Session::open() { return std::make_shared(); }\n\n" "void Session::close() {}\n\n" "bool Other::secd_retrieve_pic_fpc_i2c_id(int ifdh, int &fpc_id, int &pic_id) {\n" " fpc_id = pic_id = ifdh;\n" " return false;\n" "}\n" "void Other::registerItem(int id) { (void)id; }\n" "void Other::paint() {}\n" "void Other::close() {}\n"); for (int lib = 0; lib < 2; lib++) { char path[64]; snprintf(path, sizeof(path), "lib_%c/Twin.h", 'a' + lib); cpp_recv_write(tmp, path, "#pragma once\n" "class Twin {\n" "public:\n" " static Twin *get();\n" " void run();\n" "};\n"); snprintf(path, sizeof(path), "lib_%c/Twin.cpp", 'a' + lib); cpp_recv_write(tmp, path, "#include \"Twin.h\"\n\n" "Twin *Twin::get() {\n" " static Twin t;\n" " return &t;\n" "}\n\n" "void Twin::run() {}\n"); } cpp_recv_write( tmp, "pil/src/SecdCfgHandlerThread.cpp", "#include \"SecdManager.h\"\n" "#include \"Twin.h\"\n\n" "int GetExternalPhyPresent(int ifdh) {\n" " int fpc_id = 0, pic_id = 0;\n" " if (!SecdManager::getInstance()->secd_retrieve_pic_fpc_i2c_id(ifdh, fpc_id, " "pic_id)) {\n" " return -1;\n" " }\n" " return fpc_id + pic_id;\n" "}\n\n" "void UseRegistry() {\n" " Registry::instance().registerItem(7);\n" "}\n\n" "void UseWidget() {\n" " auto w = Widget::create();\n" " w->paint();\n" "}\n\n" "void UseSession() {\n" " auto s = Session::open();\n" " s->close();\n" "}\n\n" "void UseUnknown(struct Mystery *m) {\n" " m->paint();\n" "}\n\n" "void UseTwin() {\n" " Twin::get()->run();\n" "}\n"); for (int i = 0; parallel && i < 52; i++) { char name[64]; char pad[128]; snprintf(name, sizeof(name), "pad/cpp_pad_%02d.cpp", i); snprintf(pad, sizeof(pad), "int cpp_pad_%02d() { return %d; }\n", i, i); write_temp_file(tmp, name, pad); } } typedef struct { char tmp[256]; char db[512]; char *saved_workers; cbm_pipeline_t *p; cbm_store_t *s; const char *project; } CppRecvRun; static void cpp_recv_close(CppRecvRun *r) { if (r->s) { cbm_store_close(r->s); } if (r->p) { cbm_pipeline_free(r->p); } if (r->saved_workers) { cbm_setenv("CBM_WORKERS", r->saved_workers, 1); free(r->saved_workers); } else { cbm_unsetenv("CBM_WORKERS"); } if (r->tmp[0]) { th_rmtree(r->tmp); } memset(r, 0, sizeof(*r)); } /* Every node an assertion names must exist, or "no CALLS edge" would also * pass for a fixture that never got indexed. */ static bool cpp_recv_fixture_indexed(const CppRecvRun *r) { static const char *const required[] = { "pil.src.SecdCfgHandlerThread.GetExternalPhyPresent", "pil.src.SecdCfgHandlerThread.UseRegistry", "pil.src.SecdCfgHandlerThread.UseWidget", "pil.src.SecdCfgHandlerThread.UseSession", "pil.src.SecdCfgHandlerThread.UseUnknown", "pil.src.SecdCfgHandlerThread.UseTwin", "app.src.SecdManager.SecdManager.getInstance", "app.src.SecdManager.SecdManager.secd_retrieve_pic_fpc_i2c_id", "app.src.SecdManager.Registry.registerItem", "app.src.SecdManager.Widget.paint", "app.src.SecdManager.Session.close", "app.src.SecdManager.Other.secd_retrieve_pic_fpc_i2c_id", "app.src.SecdManager.Other.registerItem", "app.src.SecdManager.Other.paint", "app.src.SecdManager.Other.close", "lib_a.Twin.Twin.get", "lib_a.Twin.Twin.run", "lib_b.Twin.Twin.get", "lib_b.Twin.Twin.run", }; bool all = true; for (size_t i = 0; i < sizeof(required) / sizeof(required[0]); i++) { char qn[512]; snprintf(qn, sizeof(qn), "%s.%s", r->project, required[i]); cbm_node_t n; memset(&n, 0, sizeof(n)); if (cbm_store_find_node_by_qn(r->s, r->project, qn, &n) != CBM_STORE_OK) { printf(" fixture node missing: %s\n", qn); all = false; } cbm_node_free_fields(&n); } return all; } /* Index the fixture; false (after cleanup) when the run itself fails. */ static bool cpp_recv_open(CppRecvRun *r, bool parallel) { memset(r, 0, sizeof(*r)); snprintf(r->tmp, sizeof(r->tmp), "/tmp/cbm_cpp_recv_XXXXXX"); if (!cbm_mkdtemp(r->tmp)) { r->tmp[0] = '\0'; return false; } cpp_recv_write_fixture(r->tmp, parallel); const char *old_workers = getenv("CBM_WORKERS"); r->saved_workers = old_workers ? strdup(old_workers) : NULL; cbm_setenv("CBM_WORKERS", parallel ? "4" : "1", 1); snprintf(r->db, sizeof(r->db), "%s/cpp_recv.db", r->tmp); r->p = cbm_pipeline_new(r->tmp, r->db, CBM_MODE_FULL); if (!r->p || cbm_pipeline_run(r->p) != 0) { cpp_recv_close(r); return false; } r->s = cbm_store_open_path(r->db); r->project = cbm_pipeline_project_name(r->p); if (!r->s || !r->project || !cpp_recv_fixture_indexed(r)) { cpp_recv_close(r); return false; } return true; } /* Properties of the CALLS edge caller -> target (QNs below the project), or * NULL when there is no such edge. The caller frees the result. */ static char *cpp_recv_calls(const CppRecvRun *r, const char *caller, const char *target) { char src_qn[512]; char tgt_qn[512]; snprintf(src_qn, sizeof(src_qn), "%s.%s", r->project, caller); snprintf(tgt_qn, sizeof(tgt_qn), "%s.%s", r->project, target); cbm_node_t src; cbm_node_t tgt; memset(&src, 0, sizeof(src)); memset(&tgt, 0, sizeof(tgt)); char *props = NULL; if (cbm_store_find_node_by_qn(r->s, r->project, src_qn, &src) == CBM_STORE_OK && cbm_store_find_node_by_qn(r->s, r->project, tgt_qn, &tgt) == CBM_STORE_OK) { cbm_edge_t *edges = NULL; int ec = 0; cbm_store_find_edges_by_source_type(r->s, src.id, "CALLS", &edges, &ec); for (int i = 0; i < ec && !props; i++) { if (edges[i].target_id == tgt.id) { props = strdup(edges[i].properties_json ? edges[i].properties_json : "{}"); } } if (edges) { cbm_store_free_edges(edges, ec); } } cbm_node_free_fields(&src); cbm_node_free_fields(&tgt); return props; } #define CPP_RECV_CALLER "pil.src.SecdCfgHandlerThread." #define CPP_RECV_CLASS "app.src.SecdManager." /* 1 = an lsp_* CALLS edge, 0 = no edge, -1 = an edge from a name heuristic. */ static int cpp_recv_edge_kind(const CppRecvRun *r, bool parallel, const char *caller, const char *target) { char *props = cpp_recv_calls(r, caller, target); int kind = props ? (strstr(props, "\"strategy\":\"lsp_") ? 1 : -1) : 0; if (kind != 1) { printf(" [%s] %s -> %s: %s\n", parallel ? "parallel" : "sequential", caller, target, props ? props : "(no CALLS edge)"); } free(props); return kind; } /* Reported shape: a raw pointer from a singleton accessor. */ TEST(pipeline_cpp_static_factory_pointer_receiver_issue1153) { for (int mode = 0; mode < 2; mode++) { bool parallel = mode == 1; CppRecvRun r; ASSERT_TRUE(cpp_recv_open(&r, parallel)); int factory = cpp_recv_edge_kind(&r, parallel, CPP_RECV_CALLER "GetExternalPhyPresent", CPP_RECV_CLASS "SecdManager.getInstance"); int method = cpp_recv_edge_kind(&r, parallel, CPP_RECV_CALLER "GetExternalPhyPresent", CPP_RECV_CLASS "SecdManager.secd_retrieve_pic_fpc_i2c_id"); char *wrong = cpp_recv_calls(&r, CPP_RECV_CALLER "GetExternalPhyPresent", CPP_RECV_CLASS "Other.secd_retrieve_pic_fpc_i2c_id"); cpp_recv_close(&r); bool no_wrong = wrong == NULL; free(wrong); ASSERT_EQ(factory, 1); /* RED: only a unique_name guess */ ASSERT_EQ(method, 1); /* RED: the call through the returned pointer dropped */ ASSERT_TRUE(no_wrong); /* control: never the same-named Other method */ } PASS(); } /* `Foo::instance().bar()`: a reference-returning accessor. */ TEST(pipeline_cpp_static_factory_reference_receiver_issue1153) { for (int mode = 0; mode < 2; mode++) { bool parallel = mode == 1; CppRecvRun r; ASSERT_TRUE(cpp_recv_open(&r, parallel)); int method = cpp_recv_edge_kind(&r, parallel, CPP_RECV_CALLER "UseRegistry", CPP_RECV_CLASS "Registry.registerItem"); char *wrong = cpp_recv_calls(&r, CPP_RECV_CALLER "UseRegistry", CPP_RECV_CLASS "Other.registerItem"); cpp_recv_close(&r); bool no_wrong = wrong == NULL; free(wrong); ASSERT_EQ(method, 1); /* RED: dropped */ ASSERT_TRUE(no_wrong); /* control: never the same-named Other method */ } PASS(); } /* `auto w = Widget::create(); w->paint();` with std::unique_ptr. */ TEST(pipeline_cpp_static_factory_unique_ptr_receiver_issue1153) { for (int mode = 0; mode < 2; mode++) { bool parallel = mode == 1; CppRecvRun r; ASSERT_TRUE(cpp_recv_open(&r, parallel)); int method = cpp_recv_edge_kind(&r, parallel, CPP_RECV_CALLER "UseWidget", CPP_RECV_CLASS "Widget.paint"); char *wrong = cpp_recv_calls(&r, CPP_RECV_CALLER "UseWidget", CPP_RECV_CLASS "Other.paint"); cpp_recv_close(&r); bool no_wrong = wrong == NULL; free(wrong); ASSERT_EQ(method, 1); ASSERT_TRUE(no_wrong); } PASS(); } /* `auto s = Session::open(); s->close();` with std::shared_ptr. */ TEST(pipeline_cpp_static_factory_shared_ptr_receiver_issue1153) { for (int mode = 0; mode < 2; mode++) { bool parallel = mode == 1; CppRecvRun r; ASSERT_TRUE(cpp_recv_open(&r, parallel)); int method = cpp_recv_edge_kind(&r, parallel, CPP_RECV_CALLER "UseSession", CPP_RECV_CLASS "Session.close"); char *wrong = cpp_recv_calls(&r, CPP_RECV_CALLER "UseSession", CPP_RECV_CLASS "Other.close"); cpp_recv_close(&r); bool no_wrong = wrong == NULL; free(wrong); ASSERT_EQ(method, 1); ASSERT_TRUE(no_wrong); } PASS(); } /* Controls: no receiver type, or a class name two libraries declare, gives * the LSP nothing to dispatch on -- neither may gain an edge. */ TEST(pipeline_cpp_unresolvable_receiver_stays_unbound_issue1153) { static const char *const twin_targets[] = {"lib_a.Twin.Twin.get", "lib_a.Twin.Twin.run", "lib_b.Twin.Twin.get", "lib_b.Twin.Twin.run"}; for (int mode = 0; mode < 2; mode++) { bool parallel = mode == 1; CppRecvRun r; ASSERT_TRUE(cpp_recv_open(&r, parallel)); char *widget = cpp_recv_calls(&r, CPP_RECV_CALLER "UseUnknown", CPP_RECV_CLASS "Widget.paint"); char *other = cpp_recv_calls(&r, CPP_RECV_CALLER "UseUnknown", CPP_RECV_CLASS "Other.paint"); int twin_lsp = 0; for (int t = 0; t < 4; t++) { char *props = cpp_recv_calls(&r, CPP_RECV_CALLER "UseTwin", twin_targets[t]); if (props && strstr(props, "\"strategy\":\"lsp_")) { printf(" [%s] UseTwin -> %s: %s\n", parallel ? "parallel" : "sequential", twin_targets[t], props); twin_lsp++; } free(props); } cpp_recv_close(&r); bool unknown_unbound = widget == NULL && other == NULL; free(widget); free(other); ASSERT_TRUE(unknown_unbound); ASSERT_EQ(twin_lsp, 0); } PASS(); } /* ── calls_append_args: an argument longer than its formatting buffer ────── */ #if !defined(_WIN32) #include /* fork/waitpid: isolate a copy that runs past a local buffer */ #endif /* The sequential CALLS pass formats each argument into a 512-byte local before * copying it into the props buffer (calls_append_args, pass_calls.c). snprintf * reports the untruncated length, so an argument the local cannot hold must end * the array exactly like one the props buffer cannot take; copying that length * out of the local reads past it and plants the local's terminator inside the * props text. Two 250-byte strings make the formatted argument about 520 * bytes, and the props buffer is large enough to take it, so only the local's * own bound can stop the copy. 0 when the props came out right, 1 otherwise. */ static int calls_args_overlong_probe(void) { enum { CAP = 4 * CBM_SZ_1K, LONG_LEN = 250 }; static char expr[LONG_LEN + 1]; static char value[LONG_LEN + 1]; static char props[CAP]; memset(expr, 'a', LONG_LEN); expr[LONG_LEN] = '\0'; memset(value, 'b', LONG_LEN); value[LONG_LEN] = '\0'; CBMCallArg args[2] = {{"x", NULL, NULL, 0, NULL}, {expr, value, NULL, 1, NULL}}; CBMCall call = {0}; call.callee_name = "sink"; call.args = args; call.arg_count = 2; const char *base = "{\"line\":7}"; memset(props, 0x7f, CAP); /* a sentinel the formatter never writes */ memcpy(props, base, strlen(base) + 1); cbm_pipeline_calls_append_args_for_tests(props, CAP, &call); /* The short argument is kept; the overlong one ends the array cleanly. */ const char *expected = "{\"line\":7,\"args\":[{\"i\":0,\"e\":\"x\"}]}"; size_t expected_len = strlen(expected); if (strncmp(props, expected, expected_len + 1) != 0) { printf(" props: %.80s\n expected: %s\n", props, expected); return 1; } for (size_t i = expected_len + 1; i < CAP; i++) { if ((unsigned char)props[i] != 0x7f) { printf(" byte %zu past the terminator was written\n", i); return 1; } } return 0; } TEST(pipeline_calls_args_overlong_argument_stops_cleanly) { #if defined(_WIN32) ASSERT_EQ(calls_args_overlong_probe(), 0); #else /* A forked child, so a copy that runs past the local surfaces as the * child's killing signal (the sanitizer build aborts on it) rather than * taking the runner down. */ fflush(NULL); pid_t pid = fork(); ASSERT_TRUE(pid >= 0); if (pid == 0) { int rc = calls_args_overlong_probe(); fflush(NULL); _exit(rc); } int status = 0; (void)waitpid(pid, &status, 0); if (WIFSIGNALED(status)) { char m[96]; snprintf(m, sizeof(m), "formatter killed by signal %d: a copy ran past its local buffer", WTERMSIG(status)); FAIL(m); } ASSERT_TRUE(WIFEXITED(status)); ASSERT_EQ(WEXITSTATUS(status), 0); #endif PASS(); } SUITE(pipeline) { RUN_TEST(pipeline_python_module_callers_sequential); RUN_TEST(pipeline_python_module_callers_parallel); RUN_TEST(pipeline_python_class_body_calls_sequential); RUN_TEST(pipeline_python_class_body_calls_parallel); RUN_TEST(pipeline_python_init_scope_sequential); RUN_TEST(pipeline_python_init_scope_parallel); RUN_TEST(pipeline_python_init_raw_scope_failure_sequential); RUN_TEST(pipeline_python_init_raw_scope_failure_parallel); RUN_TEST(pipeline_python_init_cross_scope_failure_sequential); RUN_TEST(pipeline_python_init_cross_scope_failure_parallel); RUN_TEST(pipeline_python_init_register_scope_failure_sequential); RUN_TEST(pipeline_python_init_register_scope_failure_parallel); RUN_TEST(pipeline_python_init_import_filter_precedence); RUN_TEST(pipeline_python_init_import_probe_failure_falls_back); RUN_TEST(pipeline_issue1527_python_raw_memo_failure_sequential); RUN_TEST(pipeline_issue1527_python_raw_memo_failure_parallel); RUN_TEST(pipeline_issue1527_python_cross_memo_failure_sequential); RUN_TEST(pipeline_issue1527_python_cross_memo_failure_parallel); RUN_TEST(pipeline_issue1527_c_raw_depth_failure_sequential); RUN_TEST(pipeline_issue1527_c_cross_depth_failure_sequential); RUN_TEST(pipeline_issue1527_c_raw_depth_failure_parallel); RUN_TEST(pipeline_issue1527_c_cross_depth_failure_parallel); RUN_TEST(pipeline_issue1527_python_raw_depth_failure_sequential); RUN_TEST(pipeline_issue1527_python_cross_depth_failure_sequential); RUN_TEST(pipeline_issue1527_python_raw_depth_failure_parallel); RUN_TEST(pipeline_issue1527_python_cross_depth_failure_parallel); RUN_TEST(pipeline_issue1527_cross_memo_failure_sequential); RUN_TEST(pipeline_issue1527_cross_memo_failure_parallel); RUN_TEST(pipeline_issue1527_raw_memo_failure_sequential); RUN_TEST(pipeline_issue1527_raw_memo_failure_parallel); RUN_TEST(pipeline_issue1527_cross_memo_failure_fallback); RUN_TEST(pipeline_nested_fixture_files_are_written); RUN_TEST(pipeline_fixture_file_parent_is_preserved); RUN_TEST(pipeline_fixture_node_count_requires_exact_source_nodes); RUN_TEST(pipeline_nested_fixture_graph_has_endpoints); RUN_TEST(pipeline_python_namespace_admission_upgrade_then_noop); RUN_TEST(pipeline_python_namespace_admission_body_alias_and_all); RUN_TEST(pipeline_python_namespace_admission_deleted_and_empty); RUN_TEST(pipeline_python_namespace_admission_invalid_equal_hash); RUN_TEST(pipeline_python_namespace_change_admission); RUN_TEST(pipeline_python_namespace_codec_owns_ordered_facts); RUN_TEST(pipeline_python_namespace_codec_rejects_non_authoritative_rows); RUN_TEST(pipeline_lsp_surface_large_object_array_decode); RUN_TEST(pipeline_lsp_surface_persisted_and_body_edit_invariant); /* Index lock */ RUN_TEST(pipeline_lock_try_acquire); RUN_TEST(pipeline_lock_blocking); RUN_TEST(pipeline_lock_contention); RUN_TEST(pipeline_lock_release_allows_contender); /* Lifecycle */ RUN_TEST(pipeline_create_free); RUN_TEST(pipeline_null_repo); RUN_TEST(pipeline_free_null); RUN_TEST(pipeline_cancel); RUN_TEST(pipeline_cancel_null); RUN_TEST(pipeline_run_null); RUN_TEST(pipeline_discovery_limit_returns_exact_violation_without_publishing); RUN_TEST(pipeline_late_source_limit_preserves_previous_generation); /* Extraction back-pressure */ RUN_TEST(pipeline_backpressure_futile_nap_disengages); RUN_TEST(pipeline_over_budget_after_futility_fails_whole_and_preserves_db); /* Sequential cross-LSP shared registry (ms-typescript quadratic) */ RUN_TEST(pipeline_seq_ts_cross_uses_shared_registry); /* File persistence */ RUN_TEST(store_file_persistence); RUN_TEST(store_bulk_persistence); /* Integration: structure pass */ RUN_TEST(pipeline_grpc_routes_cover_every_service_past_the_old_cap); RUN_TEST(pipeline_doclinks_edge_lands_in_store); RUN_TEST(pipeline_sql_dump_graph_matches_the_full_parse_issue1735); RUN_TEST(pipeline_spill_resolves_namespace_imports_like_memory); RUN_TEST(pipeline_structure_nodes); RUN_TEST(pipeline_stem_folder_collision_keeps_both_nodes); RUN_TEST(pipeline_kustomize_entry_beside_same_stem_file); RUN_TEST(pipeline_committed_counts_match_persisted); RUN_TEST(pipeline_adr_survives_full_reindex); RUN_TEST(pipeline_export_error_snapshot_on_artifact_failure); RUN_TEST(pipeline_structure_edges); RUN_TEST(pipeline_branch_root_structure); RUN_TEST(pipeline_project_name_derived); RUN_TEST(pipeline_fast_mode); /* Definitions pass */ RUN_TEST(pipeline_definitions_function_nodes); RUN_TEST(pipeline_definitions_defines_edges); RUN_TEST(pipeline_definitions_properties); RUN_TEST(pipeline_def_props_valid_json_when_oversized); RUN_TEST(pipeline_edge_props_valid_json); /* Complexity propagation pass (Tier B) */ RUN_TEST(pipeline_complexity_transitive_loop_depth); /* Calls pass */ RUN_TEST(pipeline_calls_resolution); RUN_TEST(pipeline_nix_scoped_binding_calls_resolve); RUN_TEST(pipeline_hcl_block_reference_resolves_to_its_block); RUN_TEST(pipeline_incremental_preserves_cross_file_calls); RUN_TEST(pipeline_incremental_drops_terraform_reads_of_a_moved_module); RUN_TEST(pipeline_incremental_keeps_ansible_includes_of_an_edited_file); RUN_TEST(pipeline_incremental_keeps_ansible_notifies_across_files); RUN_TEST(pipeline_incremental_keeps_ansible_deploys_across_files); RUN_TEST(pipeline_field_named_like_method_keeps_its_reads_and_writes); RUN_TEST(pipeline_objectscript_export_preserves_calls_sequential_parallel); RUN_TEST(pipeline_objectscript_export_incremental_matches_full_relationships); RUN_TEST(pipeline_js_route_not_bound_to_python_builtin_sequential); RUN_TEST(pipeline_js_route_not_bound_to_python_builtin_parallel); RUN_TEST(pipeline_js_route_not_bound_to_python_builtin_incremental); RUN_TEST(pipeline_cross_file_method_defines_method_sequential); RUN_TEST(pipeline_cross_file_method_defines_method_venue_parity); 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_complexity_props_independent_of_worker_order); RUN_TEST(pipeline_incremental_cross_file_call_reference_matches_fresh_full); RUN_TEST(pipeline_incremental_changed_target_invalidates_stale_inbound_call_reference); RUN_TEST(pipeline_incremental_parallel_registry_nodes_advance_shared_ids); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API RUN_TEST(pipeline_incremental_parallel_result_cache_alloc_failure_preserves_db_and_retries); #endif RUN_TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge); RUN_TEST(pipeline_axios_wrapper_baseurl_composes_http_calls_issue1916); RUN_TEST(pipeline_ts_crossfile_new_instance_method_call_issue1354); RUN_TEST(pipeline_ts_param_property_injection_issue514); RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge); RUN_TEST(pipeline_php_receiver_suppresses_weak_method_edge); RUN_TEST(pipeline_php_receiver_parallel_suppresses_weak_method_edge); RUN_TEST(pipeline_python_receiver_keeps_specific_unique_name_member_call); RUN_TEST(pipeline_html_embedded_member_call_stays_unbound); RUN_TEST(pipeline_go_method_caller_keeps_lsp_join); RUN_TEST(pipeline_go_receiver_suppresses_weak_method_edge); RUN_TEST(pipeline_go_receiver_parallel_suppresses_weak_method_edge); RUN_TEST(pipeline_go_multi_return_receiver_is_typed); RUN_TEST(pipeline_go_multi_return_receiver_is_typed_parallel); RUN_TEST(pipeline_go_rw_usage_never_cross_into_c); RUN_TEST(pipeline_go_rw_usage_never_cross_into_c_parallel); RUN_TEST(pipeline_rust_call_never_binds_another_language); RUN_TEST(pipeline_rust_call_never_binds_another_language_parallel); RUN_TEST(pipeline_rust_assoc_call_never_binds_free_namesake); RUN_TEST(pipeline_rust_assoc_call_never_binds_free_namesake_parallel); RUN_TEST(pipeline_rust_generic_impl_method_sources_its_calls); RUN_TEST(pipeline_rust_generic_impl_method_sources_its_calls_parallel); RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method); RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method_parallel); RUN_TEST(pipeline_go_bare_ref_never_binds_field); RUN_TEST(pipeline_go_bare_ref_never_binds_field_parallel); RUN_TEST(pipeline_c_call_reaches_header_function_beside_field); RUN_TEST(pipeline_c_call_reaches_header_function_beside_field_parallel); RUN_TEST(pipeline_c_member_access_binds_field_by_object_type); RUN_TEST(pipeline_c_member_access_binds_field_by_object_type_parallel); RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges); RUN_TEST(pipeline_capitalized_axios_import_is_http_client); RUN_TEST(pipeline_capitalized_axios_import_is_http_client_sequential); RUN_TEST(pipeline_python_websocket_decorators_register_routes); RUN_TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges); RUN_TEST(pipeline_python_bare_local_binding_suppresses_weak_edge); RUN_TEST(pipeline_python_bare_local_binding_parallel_suppresses_weak_edge); RUN_TEST(pipeline_python_package_relative_imports); RUN_TEST(pipeline_python_package_relative_imports_parallel); RUN_TEST(pipeline_python_from_import_bare_call_issue1237); RUN_TEST(pipeline_python_from_import_bare_call_issue1237_parallel); RUN_TEST(pipeline_parallel_python_cross_only_dunder_gets_synthetic_carrier); RUN_TEST(pipeline_parallel_rust_cross_only_macro_hidden_gets_synthetic_carrier); RUN_TEST(pipeline_arg_url_rejects_non_http_slash_arguments); RUN_TEST(pipeline_arg_url_skips_client_navigation); RUN_TEST(pipeline_ts_config_object_url_http_calls_issue2235); RUN_TEST(pipeline_arg_url_rejects_document_file_paths); RUN_TEST(pipeline_native_fetch_classified_as_http_calls); RUN_TEST(pipeline_swift_nested_url_makes_route_issue1892); RUN_TEST(pipeline_swift_http_call_makes_route_issue1892); RUN_TEST(pipeline_route_prefix_bridge_is_order_free_and_root_bridges_nothing); RUN_TEST(pipeline_native_fetch_parallel_classified_as_http_calls); RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http); RUN_TEST(pipeline_typed_http_call_has_no_arg_url_twin); RUN_TEST(pipeline_typed_http_call_has_no_arg_url_twin_sequential); RUN_TEST(pipeline_url_constant_folding_http_calls_issue706_1147); RUN_TEST(pipeline_folded_topics_preserve_identity_sequential_parallel); RUN_TEST(pipeline_relative_folded_wrapper_url_candidates); RUN_TEST(pipeline_external_import_binding_controls_sequential_parallel); /* Git history pass */ RUN_TEST(githistory_is_trackable); RUN_TEST(githistory_compute_coupling); RUN_TEST(githistory_coupling_carries_last_co_change); RUN_TEST(githistory_skip_large_commits); RUN_TEST(githistory_limits_to_max); /* Test detection */ RUN_TEST(testdetect_is_test_file); RUN_TEST(testdetect_is_test_function); /* Implements pass (graph buffer based) */ RUN_TEST(implements_creates_override); RUN_TEST(implements_no_match); /* Usages pass (full pipeline integration) */ RUN_TEST(override_explicit_walks_to_nearest_declaring_ancestor); RUN_TEST(override_python_through_intermediate_class); RUN_TEST(python_crossfile_typed_field_calls_issue1277); RUN_TEST(python_crossfile_typed_field_calls_issue1277_parallel); RUN_TEST(python_type_checking_typed_field_issue1277); RUN_TEST(python_type_checking_typed_field_issue1277_parallel); RUN_TEST(python_dotted_base_inheritance); RUN_TEST(python_dotted_base_inheritance_parallel); RUN_TEST(pipeline_python_package_reexports_resolve_by_lsp); RUN_TEST(python_typed_field_external_import_stays_unresolved); RUN_TEST(python_typed_field_project_class_resolves); RUN_TEST(usages_creates_edges); RUN_TEST(usages_no_duplicate_calls); RUN_TEST(calls_edge_carries_call_site_line); RUN_TEST(usages_kotlin_creates_edges); RUN_TEST(usages_kotlin_no_duplicate_calls); /* Language integration tests */ RUN_TEST(pipeline_python_project); RUN_TEST(pipeline_c_definitions_own_their_qn_c1); RUN_TEST(pipeline_c_call_targets_definition_before_macro_c1); RUN_TEST(pipeline_cpp_enum_reference_reaches_flat_enumerator_c1); RUN_TEST(pipeline_c_enum_inside_struct_reference_resolves_c1); RUN_TEST(pipeline_c_variants_on_sequential_and_parallel_paths_c1); RUN_TEST(pipeline_cpp_overloads_are_listed_as_variants_c1); RUN_TEST(pipeline_semantic_version_3_index_is_rebuilt_in_full_c1); RUN_TEST(pipeline_python_app_factory_routes_issue1548); RUN_TEST(pipeline_python_nested_def_is_function_local_issue1548); RUN_TEST(pipeline_python_local_classes_stay_in_their_def_issue1548); RUN_TEST(pipeline_python_import_never_binds_a_local_def_issue1548); RUN_TEST(pipeline_python_nested_defs_parallel_issue1548); RUN_TEST(pipeline_python_nested_def_local_after_incremental_issue1548); RUN_TEST(pipeline_python_local_def_shadows_module_def_issue1548); RUN_TEST(pipeline_python_local_def_shadows_module_def_parallel_issue1548); RUN_TEST(pipeline_python_super_follows_c3_mro); RUN_TEST(pipeline_python_super_follows_c3_mro_parallel); RUN_TEST(pipeline_python_attribute_follows_c3_mro); RUN_TEST(pipeline_python_attribute_follows_c3_mro_parallel); RUN_TEST(pipeline_python_init_function_shares_submodule_qn_issue1548); RUN_TEST(pipeline_python_init_function_shares_submodule_qn_parallel_issue1548); RUN_TEST(pipeline_python_class_body_sibling_base); RUN_TEST(pipeline_python_class_body_sibling_base_parallel); RUN_TEST(pipeline_python_nested_helper_keeps_tests_issue1548); RUN_TEST(pipeline_python_nested_helper_keeps_tests_parallel_issue1548); RUN_TEST(pipeline_python_decorated_nested_class_extracted); RUN_TEST(pipeline_python_decorated_nested_class_extracted_parallel); RUN_TEST(pipeline_python_nested_class_methods_resolve); RUN_TEST(pipeline_python_nested_class_methods_resolve_parallel); RUN_TEST(pipeline_header_include_target_is_independent_of_registration_order); RUN_TEST(pipeline_imports_multi_symbol_edges); RUN_TEST(pipeline_go_cross_package_call); RUN_TEST(pipeline_swift_cross_package_import); RUN_TEST(pipeline_python_cross_module_call); RUN_TEST(pipeline_cross_language_same_name_does_not_share_calls_issue725); RUN_TEST(pipeline_go_type_classification); RUN_TEST(pipeline_go_grouped_types); RUN_TEST(pipeline_kotlin_project); RUN_TEST(pipeline_lua_anonymous_functions); RUN_TEST(pipeline_csharp_modern); RUN_TEST(pipeline_bom_stripping); RUN_TEST(pipeline_form_call_resolution); RUN_TEST(pipeline_python_type_inference); /* Docstring integration (port of TestDocstringIntegration) */ RUN_TEST(pipeline_docstring_go_function); RUN_TEST(pipeline_docstring_python_function); RUN_TEST(pipeline_docstring_java_method); RUN_TEST(pipeline_docstring_kotlin_function); RUN_TEST(pipeline_docstring_go_class); RUN_TEST(pipeline_doc_go_package_comment_on_file_node); RUN_TEST(pipeline_doc_rust_inner_doc_on_file_node); RUN_TEST(pipeline_doc_long_docstring_stored_whole); /* Project name */ RUN_TEST(project_name_from_path); RUN_TEST(project_name_drive_letter_case_insensitive_issue394); RUN_TEST(git_context_non_git_path); RUN_TEST(git_context_linked_worktree); RUN_TEST(project_name_uniqueness); /* Git diff helpers */ RUN_TEST(gitdiff_parse_range_with_count); RUN_TEST(gitdiff_parse_range_no_count); RUN_TEST(gitdiff_parse_range_zero_count); RUN_TEST(gitdiff_parse_range_large); RUN_TEST(gitdiff_parse_name_status); RUN_TEST(gitdiff_parse_name_status_filters_untrackable); RUN_TEST(gitdiff_parse_hunks); RUN_TEST(gitdiff_parse_hunks_no_newline_marker); RUN_TEST(gitdiff_parse_hunks_mode_change); RUN_TEST(gitdiff_parse_hunks_deletion); /* Config helpers */ RUN_TEST(configures_is_env_var_name); RUN_TEST(configures_normalize_config_key); RUN_TEST(configures_has_config_extension); /* Config integration tests */ RUN_TEST(configures_env_var_in_config); RUN_TEST(configures_lowercase_key_skipped); RUN_TEST(configures_non_config_file_skipped); RUN_TEST(configures_full_pipeline_integration); /* HTTP link pipeline integration */ /* Enrichment helpers */ RUN_TEST(enrichment_split_camel_case); RUN_TEST(enrichment_tokenize_decorator); /* Decorator tags integration */ RUN_TEST(decorator_tags_python_auto_discovery); RUN_TEST(decorator_tags_java_class_methods); /* Compile commands helpers */ RUN_TEST(compile_commands_split_command); RUN_TEST(compile_commands_extract_flags); RUN_TEST(compile_commands_parse_json); RUN_TEST(compile_commands_parse_empty); RUN_TEST(compile_commands_parse_invalid); /* Infrascan helpers */ RUN_TEST(infra_is_kustomize_file); RUN_TEST(infra_is_k8s_manifest); RUN_TEST(infra_secret_detection); RUN_TEST(helm_parse_chart_dependencies_issue338); RUN_TEST(helm_parse_chart_no_deps_issue338); RUN_TEST(helm_parse_chart_dependency_entries); RUN_TEST(helm_parse_chart_many_dependencies); /* Infrascan: QN helper */ RUN_TEST(infra_qn_helper); /* K8s / Kustomize extraction */ RUN_TEST(k8s_extract_kustomize); RUN_TEST(k8s_extract_manifest); RUN_TEST(k8s_extract_manifest_no_name); RUN_TEST(k8s_extract_manifest_multidoc); /* SwiftPM Package.swift manifest resolution (issue #551 item 1) */ RUN_TEST(pkgmap_swift_targets_registers_module); RUN_TEST(pkgmap_swift_products_do_not_register_alias); RUN_TEST(pkgmap_swift_target_name_immediately_before_close_paren); RUN_TEST(pkgmap_swift_target_honors_literal_path); RUN_TEST(pkgmap_swift_target_computed_path_fails_closed); RUN_TEST(pkgmap_swift_target_in_comment_or_string_not_registered); RUN_TEST(pkgmap_swift_dependencies_do_not_leak_entries); RUN_TEST(pkgmap_swift_target_name_dependency_does_not_leak_entry); RUN_TEST(pkgmap_swift_ambiguous_target_name_fails_closed); RUN_TEST(pkgmap_swift_scan_repo_finds_nested_manifest); /* Discovery-exclusion plumbing in auxiliary repo walks (#792) */ RUN_TEST(pipeline_relpath_excluded_boundary); RUN_TEST(pkgmap_scan_repo_honors_discovery_exclusions); /* Function registry / resolver */ RUN_TEST(registry_resolve_single_candidate); RUN_TEST(registry_fuzzy_nonexistent); RUN_TEST(registry_fuzzy_multiple_best_by_distance); RUN_TEST(registry_fuzzy_simple_name_extraction); RUN_TEST(registry_fuzzy_empty); RUN_TEST(registry_exists); RUN_TEST(registry_confidence_import_map); RUN_TEST(registry_confidence_import_map_suffix); RUN_TEST(registry_confidence_same_module); RUN_TEST(registry_confidence_unique_name); RUN_TEST(registry_confidence_suffix_match); RUN_TEST(registry_receiver_chain_refuses_library_unique_name_issue1893); RUN_TEST(registry_receiver_chain_refuses_library_suffix_match_issue1893); RUN_TEST(registry_receiver_chain_keeps_project_extension_issue1893); RUN_TEST(registry_receiver_chain_admits_factory_chain_into_nested_type); RUN_TEST(registry_receiver_chain_still_refuses_foreign_root_with_ancestry_rule); RUN_TEST(registry_receiver_chain_ignores_lowercase_root_issue1893); RUN_TEST(registry_receiver_chain_ignores_bare_name_issue1893); RUN_TEST(registry_fuzzy_confidence_single); RUN_TEST(registry_fuzzy_confidence_distance); RUN_TEST(registry_negative_import_rejects); RUN_TEST(registry_fuzzy_import_penalty); RUN_TEST(registry_fuzzy_no_import_map_passthrough); RUN_TEST(registry_find_by_name); RUN_TEST(registry_confidence_band); RUN_TEST(registry_is_import_reachable); RUN_TEST(registry_find_ending_with); /* Git history */ RUN_TEST(githistory_is_trackable_file); RUN_TEST(githistory_compute_change_coupling); RUN_TEST(githistory_coupling_skips_large_commits); RUN_TEST(githistory_coupling_limits_output); /* Incremental reindex */ /* FastAPI Depends edge tracking (PR #66 port) */ RUN_TEST(pipeline_fastapi_depends_edges); /* Incremental */ RUN_TEST(incremental_full_then_noop); RUN_TEST(incremental_detects_changed_file); RUN_TEST(full_reindex_recovers_when_previous_coverage_is_unreadable); RUN_TEST(incremental_detects_deleted_file); RUN_TEST(incremental_new_file_added); RUN_TEST(cancelled_full_reindex_preserves_committed_db); RUN_TEST(cancelled_incremental_reindex_preserves_committed_db); RUN_TEST(backup_failed_publish_failure_preserves_final_sidecars); RUN_TEST(backup_failed_rename_failure_preserves_corrupt_main); #ifdef __linux__ RUN_TEST(full_reindex_preserves_exact_long_db_path); #endif RUN_TEST(incremental_fast_preserves_mode_skipped_tools_dir); RUN_TEST(incremental_k8s_manifest_indexed); RUN_TEST(incremental_kustomize_entries_indexed); /* Resource management & internal helper tests */ RUN_TEST(pipeline_empty_path); RUN_TEST(pipeline_project_name_content); RUN_TEST(pipeline_cancel_sets_flag); RUN_TEST(pipeline_double_cancel); RUN_TEST(pipeline_double_free_prevention); /* Trackable file tests */ RUN_TEST(trackable_source_files); RUN_TEST(trackable_rejects_images); RUN_TEST(trackable_rejects_minified); RUN_TEST(trackable_rejects_vendor_dirs); RUN_TEST(trackable_rejects_lock_files); /* Test path detection */ RUN_TEST(test_path_directory_patterns); RUN_TEST(test_path_suffix_patterns); /* Test function name detection */ RUN_TEST(test_func_name_go_patterns); RUN_TEST(test_func_name_js_helpers); /* Env var name detection */ RUN_TEST(env_var_name_valid); RUN_TEST(env_var_name_invalid); /* Config extension detection */ RUN_TEST(config_ext_positive); RUN_TEST(config_ext_negative); /* Camel case splitting */ RUN_TEST(split_camel_basic); RUN_TEST(split_camel_single_word); RUN_TEST(split_camel_empty); /* Decorator tokenization */ RUN_TEST(tokenize_decorator_login_required); RUN_TEST(tokenize_decorator_single); /* Command splitting */ RUN_TEST(split_command_basic); RUN_TEST(split_command_quoted); RUN_TEST(split_command_empty); /* Range parsing */ RUN_TEST(parse_range_with_count); RUN_TEST(parse_range_single); RUN_TEST(parse_range_zero_count); /* Name status parsing */ RUN_TEST(parse_name_status_basic); RUN_TEST(parse_name_status_empty); /* Hunk parsing */ RUN_TEST(parse_hunks_empty); /* Change coupling */ RUN_TEST(coupling_empty_commits); RUN_TEST(coupling_single_file_commit); /* JSON bracket cleaning */ /* FQN computation */ RUN_TEST(fqn_compute_basic); RUN_TEST(fqn_compute_strips_ext); RUN_TEST(fqn_module_basic); RUN_TEST(fqn_folder_basic); /* Project name edge cases */ RUN_TEST(project_name_special_chars); RUN_TEST(project_name_trailing_slash); /* Ensemble routing pass */ RUN_TEST(pipeline_ensemble_routing_edges); RUN_TEST(pipeline_ensemble_routing_method_scoping); RUN_TEST(pipeline_python_iris_cls_class_aware_calls); RUN_TEST(pipeline_ensemble_routing_attr_does_not_leak_across_items); RUN_TEST(pipeline_ensemble_routing_settings_targets); RUN_TEST(pipeline_ensemble_routing_unterminated_item_is_safe); RUN_TEST(pipeline_delta_patch_indexes_docstring_into_fts_body); RUN_TEST(pipeline_markdown_and_config_prose_reaches_fts_body); RUN_TEST(pipeline_semantic_edges_no_functions); RUN_TEST(pipeline_semantic_batched_matches_unbatched); RUN_TEST(pipeline_semantic_pair_signals_never_change_the_graph); RUN_TEST(pipeline_semantic_edges_carry_p); RUN_TEST(pipeline_doc_candidates_published_and_kept_by_delta); RUN_TEST(pipeline_doc_candidates_off_stores_none_and_keeps_graph); RUN_TEST(pipeline_doc_candidates_off_clears_rows_on_delta); RUN_TEST(pipeline_doc_candidates_default_follows_mode); RUN_TEST(pipeline_doc_candidates_explicit_wins_over_mode); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API RUN_TEST(pipeline_doc_candidates_fast_on_legacy_incremental); #endif RUN_TEST(pipeline_doc_candidates_skip_exact_links_and_use_format_curves); RUN_TEST(pipeline_doc_candidates_home_position_and_kinds); RUN_TEST(pipeline_doc_candidates_diagram_family_curves); RUN_TEST(pipeline_cpp_static_factory_pointer_receiver_issue1153); RUN_TEST(pipeline_cpp_static_factory_reference_receiver_issue1153); RUN_TEST(pipeline_cpp_static_factory_unique_ptr_receiver_issue1153); RUN_TEST(pipeline_cpp_static_factory_shared_ptr_receiver_issue1153); RUN_TEST(pipeline_cpp_unresolvable_receiver_stays_unbound_issue1153); RUN_TEST(pipeline_calls_args_overlong_argument_stops_cleanly); } /* Focused semantic-manifest and publication contracts. Kept separate from the * broad pipeline suite so RED/GREEN iterations exercise only this boundary; * the default all-suite run still executes it. */ SUITE(pipeline_semantic_manifest_repro) { RUN_TEST(incremental_downgrade_preserves_scope_and_artifact_across_change_noop_delete); RUN_TEST(pipeline_incremental_repoints_call_reference_without_stale_edge); RUN_TEST(pipeline_staged_publish_commits_below_full_sync_issue1419); RUN_TEST(incremental_k8s_edits_reach_other_files); RUN_TEST(incremental_shell_deploy_edit_keeps_its_edges); RUN_TEST(incremental_deleted_spawner_drops_its_process); RUN_TEST(incremental_deleted_caller_drops_its_route); RUN_TEST(incremental_script_request_to_compose_service_keeps_host); RUN_TEST(incremental_deleted_dockerfile_drops_its_image); RUN_TEST(incremental_dockerfile_edit_keeps_build_edges); RUN_TEST(incremental_compose_edit_replaces_build_edges); RUN_TEST(incremental_env_edit_replaces_scoped_envvars); RUN_TEST(incremental_image_split_change_rebuilds); RUN_TEST(incremental_helm_values_edit_keeps_chart_edge); RUN_TEST(incremental_werf_values_edit_keeps_chart_edge); RUN_TEST(incremental_release_values_edit_rebuilds); RUN_TEST(helm_values_follow_helm_semantics); RUN_TEST(helm_render_range_budget_ends_unknown); RUN_TEST(helm_render_indent_budget_ends_unknown); RUN_TEST(helm_render_define_fanout_ends_unknown); RUN_TEST(helm_values_alias_expansion_limit); RUN_TEST(helm_values_set_index_cap_and_long_key); RUN_TEST(helm_render_long_key_mark_whole); RUN_TEST(helm_unknown_branch_spoils_what_it_assigns); RUN_TEST(helm_mutator_spoils_the_map_it_changes); RUN_TEST(helm_unknown_break_leaves_the_loop); RUN_TEST(helm_change_of_a_new_value_spoils_after); RUN_TEST(helm_render_captures_grow_linearly); RUN_TEST(helm_render_memory_budget_ends_unknown); RUN_TEST(helm_skipped_set_of_a_call_result); RUN_TEST(helm_own_map_given_a_shared_one); RUN_TEST(helm_set_of_an_unknown_call_result); RUN_TEST(helm_unknown_break_spoils_the_rest_of_the_loop); RUN_TEST(helm_skipped_set_after_redefinition); RUN_TEST(helm_many_changed_maps_keep_reads_right); RUN_TEST(helm_define_memo_under_mutual_recursion); RUN_TEST(helm_include_with_poisoned_argument); RUN_TEST(helm_capture_keys_cost_linear_memory); RUN_TEST(helm_render_keeps_only_its_results); RUN_TEST(helm_render_budget_logs_its_reason); RUN_TEST(helm_merge_into_values_root); RUN_TEST(helm_skipped_set_beside_a_nested_scope); RUN_TEST(helm_unknown_break_spoils_later_elements); RUN_TEST(helm_skipped_range_sets_its_element); RUN_TEST(helm_deep_merge_reaches_a_held_values_map); RUN_TEST(helm_checksum_include_keeps_the_image); RUN_TEST(helm_include_of_a_changing_template_file); RUN_TEST(helm_subchart_base_path_names_its_own_file); RUN_TEST(helm_include_of_a_missing_template_file); RUN_TEST(helm_template_file_including_itself); RUN_TEST(helm_deep_merge_over_a_change_below); RUN_TEST(helm_and_or_with_an_unknown_operand); RUN_TEST(helm_later_condition_of_a_skipped_if); RUN_TEST(helm_sort_alpha_changes_its_list); RUN_TEST(helm_parse_error_spoils); RUN_TEST(helm_captures_after_a_huge_capture); RUN_TEST(helm_includes_of_one_name_in_a_loop); RUN_TEST(incremental_compose_dotenv_change_rebuilds); RUN_TEST(incremental_dockerfile_edit_keeps_docker_build_edges); RUN_TEST(incremental_second_build_declarer_keeps_the_edge); RUN_TEST(pipeline_sql_lineage_and_relation_isolation); RUN_TEST(pipeline_incremental_sql_table_rename_drops_stale_lineage); RUN_TEST(pipeline_dbt_jinja_lineage); RUN_TEST(pipeline_parallel_manifest_is_byte_stable_above_threshold); RUN_TEST(pipeline_closure_repair_body_edit_converges_with_fresh_full); RUN_TEST(pipeline_closure_repair_removed_def_drops_dependent_edge); RUN_TEST(pipeline_closure_repair_added_name_declines_to_full); RUN_TEST(pipeline_closure_repair_new_file_declines_to_full); RUN_TEST(incremental_stem_mark_deletion_rebuilds); RUN_TEST(incremental_env_join_repair_keeps_unchanged_pairs); RUN_TEST(pipeline_closure_repair_budget_declines_to_full); RUN_TEST(pipeline_incremental_tsconfig_alias_change_matches_fresh_full); #if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API RUN_TEST(pipeline_publication_stamps_full_and_delta_generations); RUN_TEST(pipeline_git_context_change_forces_full_and_refreshes_branch); RUN_TEST(pipeline_global_extension_config_change_forces_full); RUN_TEST(pipeline_publication_never_uses_a_predictable_staging_path); RUN_TEST(pipeline_stage_names_never_nest); RUN_TEST(pipeline_minted_stage_is_owned_until_released); RUN_TEST(pipeline_stale_zero_byte_stage_beside_valid_db_routes_incremental_and_is_swept); RUN_TEST(pipeline_sweep_reclaims_orphans_of_other_projects_and_keeps_their_live_stages); RUN_TEST(pipeline_sweep_removes_dead_writer_stage_keeps_locked_stage); RUN_TEST(pipeline_concurrent_sweep_must_not_remove_inflight_stage); RUN_TEST(pipeline_unchanged_reindex_leaves_live_generation_untouched); RUN_TEST(pipeline_live_noop_proof_never_hides_a_change); RUN_TEST(pipeline_fresh_index_never_reports_invalid_existing_db); RUN_TEST(pipeline_source_mutation_before_publication_preserves_previous_generation); RUN_TEST(pipeline_source_addition_before_publication_preserves_previous_generation); RUN_TEST(pipeline_tsconfig_mutation_before_publication_preserves_previous_generation); RUN_TEST(pipeline_exact_inputs_migrate_coverage_metadata_and_index_mode); RUN_TEST(pipeline_existing_artifact_refreshes_after_default_forced_full_reindex); RUN_TEST(pipeline_full_cancel_after_predump_preserves_previous_generation); RUN_TEST(pipeline_full_cancel_after_destination_prepare_preserves_previous_generation); RUN_TEST(pipeline_full_persist_failure_after_stage_dump_preserves_previous_generation); RUN_TEST(pipeline_incremental_persist_failure_preserves_previous_generation_and_retries); RUN_TEST(pipeline_incremental_successful_publication_preserves_adr); RUN_TEST(pipeline_full_adr_capture_failure_preserves_previous_generation); RUN_TEST(pipeline_semantic_manifest_rejects_non_directory_root); RUN_TEST(pipeline_full_reindex_quarantines_corrupt_destination_without_overwrite); RUN_TEST(pipeline_full_reindex_replaces_legacy_schema_without_quarantine); #endif }