diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 50adb6d4e..606b8f3ef 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -5610,12 +5610,10 @@ static int arch_compute_cycles(cbm_store_t *store, const char *project, int64_t *scanned_edges = ecount; scc_graph_t g; if (!scc_build(src, tgt, ecount, &g)) { - free(src); - free(tgt); + cbm_store_free_call_edges(src, tgt); return CBM_STORE_OK; /* no edges = no cycles, not an error */ } - free(src); - free(tgt); + cbm_store_free_call_edges(src, tgt); int *comp = malloc((size_t)g.nverts * sizeof(int)); if (!comp) { diff --git a/src/store/store.c b/src/store/store.c index 03e42b0ed..e36c93b1b 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -3142,6 +3142,11 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge return CBM_STORE_OK; } +void cbm_store_free_call_edges(int64_t *src, int64_t *tgt) { + free(src); + free(tgt); +} + int cbm_store_find_edges_by_source(cbm_store_t *s, int64_t source_id, cbm_edge_t **out, int *count) { bind_id_t b = {source_id}; @@ -8234,6 +8239,10 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t * return cbm_leiden(nodes, node_count, edges, edge_count, 1.0, out, out_count); } +void cbm_leiden_free(cbm_louvain_result_t *result) { + free(result); +} + /* ── Architecture: community clusters via Leiden ───────────────── */ enum { diff --git a/src/store/store.h b/src/store/store.h index 165356c31..7bd7e0bbe 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -574,6 +574,9 @@ int cbm_store_insert_edge_batch(cbm_store_t *s, const cbm_edge_t *edges, int cou int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edges, int64_t **out_src, int64_t **out_tgt, int *count, bool *truncated); +/* Free the (source_id, target_id) arrays returned by cbm_store_fetch_call_edges. */ +void cbm_store_free_call_edges(int64_t *src, int64_t *tgt); + /* Find edges by source node. */ int cbm_store_find_edges_by_source(cbm_store_t *s, int64_t source_id, cbm_edge_t **out, int *count); @@ -996,6 +999,9 @@ int cbm_leiden(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *e int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *edges, int edge_count, cbm_louvain_result_t **out, int *out_count); +/* Free the result array returned by cbm_leiden / cbm_louvain. */ +void cbm_leiden_free(cbm_louvain_result_t *result); + /* ── Memory management helpers ──────────────────────────────────── */ /* Free heap-allocated strings in a stack-allocated node (does NOT free the node itself). */ diff --git a/src/ui/layout3d.c b/src/ui/layout3d.c index 3176da2ff..8a5e32aea 100644 --- a/src/ui/layout3d.c +++ b/src/ui/layout3d.c @@ -867,3 +867,7 @@ char *cbm_layout_to_json(const cbm_layout_result_t *r) { } return json; } + +void cbm_layout_free_json(char *json) { + free(json); +} diff --git a/src/ui/layout3d.h b/src/ui/layout3d.h index 20303bf64..758f0be74 100644 --- a/src/ui/layout3d.h +++ b/src/ui/layout3d.h @@ -71,4 +71,7 @@ void cbm_layout_free(cbm_layout_result_t *result); /* Serialize layout result to JSON string. Caller must free(). */ char *cbm_layout_to_json(const cbm_layout_result_t *result); +/* Free the JSON string returned by cbm_layout_to_json. */ +void cbm_layout_free_json(char *json); + #endif /* CBM_UI_LAYOUT3D_H */ diff --git a/tests/test_store_arch.c b/tests/test_store_arch.c index 4e9f59068..aeb8b3d05 100644 --- a/tests/test_store_arch.c +++ b/tests/test_store_arch.c @@ -1189,6 +1189,68 @@ TEST(adr_validate_keys_empty) { PASS(); } +/* ── cbm_store_fetch_call_edges / cbm_store_free_call_edges tests ─ */ + +TEST(fetch_call_edges_basic) { + cbm_store_t *s = setup_arch_test_store(); + ASSERT_NOT_NULL(s); + + int64_t *src = NULL; + int64_t *tgt = NULL; + int count = 0; + bool truncated = true; + ASSERT_EQ(cbm_store_fetch_call_edges(s, "test", 100, &src, &tgt, &count, &truncated), + CBM_STORE_OK); + /* setup_arch_test_store() wires 5 CALLS edges among Function nodes. */ + ASSERT_EQ(count, 5); + ASSERT_FALSE(truncated); + ASSERT_NOT_NULL(src); + ASSERT_NOT_NULL(tgt); + + cbm_store_free_call_edges(src, tgt); + cbm_store_close(s); + PASS(); +} + +TEST(fetch_call_edges_truncated) { + cbm_store_t *s = setup_arch_test_store(); + ASSERT_NOT_NULL(s); + + int64_t *src = NULL; + int64_t *tgt = NULL; + int count = 0; + bool truncated = false; + /* Only 5 CALLS edges exist; capping at 2 must set truncated. */ + ASSERT_EQ(cbm_store_fetch_call_edges(s, "test", 2, &src, &tgt, &count, &truncated), + CBM_STORE_OK); + ASSERT_EQ(count, 2); + ASSERT_TRUE(truncated); + + cbm_store_free_call_edges(src, tgt); + cbm_store_close(s); + PASS(); +} + +TEST(fetch_call_edges_empty_project) { + cbm_store_t *s = cbm_store_open_memory(); + ASSERT_NOT_NULL(s); + cbm_store_upsert_project(s, "empty", "/tmp/empty"); + + int64_t *src = NULL; + int64_t *tgt = NULL; + int count = 0; + bool truncated = true; + ASSERT_EQ(cbm_store_fetch_call_edges(s, "empty", 100, &src, &tgt, &count, &truncated), + CBM_STORE_OK); + ASSERT_EQ(count, 0); + ASSERT_FALSE(truncated); + + /* cbm_store_free_call_edges(NULL, NULL) must not crash. */ + cbm_store_free_call_edges(src, tgt); + cbm_store_close(s); + PASS(); +} + /* ── Louvain tests ──────────────────────────────────────────────── */ TEST(louvain_basic) { @@ -1214,7 +1276,7 @@ TEST(louvain_basic) { /* Triangle and pair different */ ASSERT_TRUE(comm[1] != comm[4]); - free(result); + cbm_leiden_free(result); PASS(); } @@ -1223,7 +1285,7 @@ TEST(louvain_empty) { int count = 0; ASSERT_EQ(cbm_louvain(NULL, 0, NULL, 0, &result, &count), CBM_STORE_OK); ASSERT_EQ(count, 0); - free(result); + cbm_leiden_free(result); PASS(); } @@ -1234,7 +1296,7 @@ TEST(louvain_single_node) { ASSERT_EQ(cbm_louvain(nodes, 1, NULL, 0, &result, &count), CBM_STORE_OK); ASSERT_EQ(count, 1); ASSERT_EQ(result[0].node_id, 42); - free(result); + cbm_leiden_free(result); PASS(); } @@ -1297,7 +1359,7 @@ TEST(louvain_converges) { } ASSERT_TRUE(same_count >= 8); - free(result); + cbm_leiden_free(result); PASS(); } @@ -1431,7 +1493,7 @@ TEST(leiden_multilevel_collapses_noise) { } ASSERT_TRUE(same >= SZ - 1); } - free(result); + cbm_leiden_free(result); PASS(); } @@ -1463,8 +1525,8 @@ TEST(leiden_resolution_controls_granularity) { ASSERT_TRUE(n_hi > n_lo); ASSERT_TRUE(leiden_all_communities_connected(lo, N, edges, ne)); ASSERT_TRUE(leiden_all_communities_connected(hi, N, edges, ne)); - free(lo); - free(hi); + cbm_leiden_free(lo); + cbm_leiden_free(hi); PASS(); } @@ -1727,6 +1789,11 @@ SUITE(store_arch) { RUN_TEST(adr_splice_refuses_unterminated_fence); RUN_TEST(adr_validate_keys_empty); + /* Call-edge fetch / free */ + RUN_TEST(fetch_call_edges_basic); + RUN_TEST(fetch_call_edges_truncated); + RUN_TEST(fetch_call_edges_empty_project); + /* Louvain */ RUN_TEST(louvain_basic); RUN_TEST(louvain_empty); diff --git a/tests/test_ui.c b/tests/test_ui.c index a403dbbbe..c1975a4ca 100644 --- a/tests/test_ui.c +++ b/tests/test_ui.c @@ -548,7 +548,7 @@ TEST(layout_to_json) { ASSERT(strstr(json, "\"hello\"") != NULL); ASSERT(strstr(json, "\"Function\"") != NULL); - free(json); + cbm_layout_free_json(json); cbm_layout_free(r); cbm_store_close(store); PASS(); @@ -719,7 +719,7 @@ TEST(layout_dead_code_classification) { ASSERT_NOT_NULL(json); ASSERT(strstr(json, "\"status\":\"dead\"") != NULL); ASSERT(strstr(json, "\"in_calls\":2") != NULL); - free(json); + cbm_layout_free_json(json); cbm_layout_free(r); cbm_store_close(store);