From 5e328814fc99ffe588daae4f0f1e8cf5066e084b Mon Sep 17 00:00:00 2001 From: Jumaga2015 Date: Tue, 1 Sep 2026 21:00:26 +0200 Subject: [PATCH 1/2] fix(store,ui): export paired free functions for FFI callers cbm_store_fetch_call_edges, cbm_leiden/cbm_louvain, and cbm_layout_to_json document caller-owned memory ("caller frees both" / "the caller frees it" / "Caller must free()") but exported no matching free function, unlike the rest of the API (cbm_store_find_nodes -> cbm_store_free_nodes, cbm_layout_compute -> cbm_layout_free, etc.). From C a bare free() works via static linking, but a caller across an FFI boundary (e.g. C# P/Invoke) has no way to reach the DLL's internal free() unless it is exported by name. Adds three paired free functions following the existing pattern: - cbm_store_free_call_edges(src, tgt) - cbm_leiden_free(result) - cbm_layout_free_json(json) Updates the one internal production caller (mcp.c SCC analysis) to use cbm_store_free_call_edges instead of a bare free(), and the existing unit tests for cbm_leiden/cbm_louvain/cbm_layout_to_json to use the new paired frees. Adds fetch_call_edges_basic, fetch_call_edges_truncated, and fetch_call_edges_empty_project to test_store_arch.c, covering cbm_store_fetch_call_edges / cbm_store_free_call_edges, which had no prior test coverage. Fixes #1762 Signed-off-by: Jumaga2015 --- src/mcp/mcp.c | 6 +-- src/store/store.c | 9 +++++ src/store/store.h | 6 +++ src/ui/layout3d.c | 4 ++ src/ui/layout3d.h | 3 ++ tests/test_store_arch.c | 81 +++++++++++++++++++++++++++++++++++++---- tests/test_ui.c | 4 +- 7 files changed, 100 insertions(+), 13 deletions(-) diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index e908211b84..1d154bb28b 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -5545,12 +5545,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 03e42b0ed0..e36c93b1be 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 165356c31e..7bd7e0bbe7 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 3176da2ffb..8a5e32aea4 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 20303bf644..758f0be74e 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 4e9f590683..aeb8b3d053 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 a403dbbbec..c1975a4cae 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); From c4b635b25f7a51670642e5100c6e73d6406be4a1 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 20 Oct 2026 19:24:56 +0200 Subject: [PATCH 2/2] fix(store,ui): route the paired-free allocations through the memory core The three exported release functions added for #1762 freed raw heap blocks. Since the memory-core ratchet landed, the allocating side has to go through the core as well, and on current main cbm_leiden already allocates its result in CBM_MEM_CLASS_OTHER, so a raw free() in cbm_leiden_free left the class counters drifting. - cbm_store_fetch_call_edges: allocate and grow the source/target arrays with cbm_alloc/cbm_realloc in CBM_MEM_CLASS_STORE. An allocation failure now finalizes the statement, releases both arrays and returns CBM_STORE_ERR instead of writing through a NULL array. - cbm_store_free_call_edges, cbm_leiden_free and cbm_layout_free_json release with cbm_free in the class the matching allocation used. - cbm_layout_to_json writes its output through cbm_json_writer_allocator, so the string is an OTHER-class core block; the UI server's five release sites now call cbm_layout_free_json. - The header comments name the paired free instead of "caller frees". - Tests: ownership checks for all three pairs measure the class counters across the allocating call and the paired free, so a raw allocation on either side fails; the call-edge check grows past the initial capacity. Refs #1762 Signed-off-by: Martin Vogel --- src/store/store.c | 39 ++++++++++++++++++++------ src/store/store.h | 5 ++-- src/ui/http_server.c | 10 +++---- src/ui/layout3d.c | 9 ++++-- src/ui/layout3d.h | 2 +- tests/test_store_arch.c | 62 ++++++++++++++++++++++++++++++++++++++++- tests/test_ui.c | 35 +++++++++++++++++++++++ 7 files changed, 141 insertions(+), 21 deletions(-) diff --git a/src/store/store.c b/src/store/store.c index 92b78a2cad..069e06b1d0 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -4123,10 +4123,18 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge bind_text(stmt, SKIP_ONE, project); sqlite3_bind_int(stmt, 2, max_edges > 0 ? max_edges + SKIP_ONE : CBM_NOT_FOUND); + /* Both arrays are STORE-class memory-core blocks: the caller releases them + * with cbm_store_free_call_edges, the only free that matches. */ int cap = ST_INIT_CAP_16; int n = 0; - int64_t *src = malloc((size_t)cap * sizeof(int64_t)); - int64_t *tgt = malloc((size_t)cap * sizeof(int64_t)); + int64_t *src = cbm_alloc(CBM_MEM_CLASS_STORE, (size_t)cap * sizeof(int64_t)); + int64_t *tgt = cbm_alloc(CBM_MEM_CLASS_STORE, (size_t)cap * sizeof(int64_t)); + if (!src || !tgt) { + sqlite3_finalize(stmt); + cbm_store_free_call_edges(src, tgt); + store_set_error(s, "fetch_call_edges: out of memory"); + return CBM_STORE_ERR; + } int scan_rc; while ((scan_rc = sqlite3_step(stmt)) == SQLITE_ROW) { if (max_edges > 0 && n >= max_edges) { @@ -4136,9 +4144,23 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge break; } if (n >= cap) { + size_t grown_bytes = (size_t)cap * ST_GROWTH * sizeof(int64_t); + int64_t *grown_src = cbm_realloc(CBM_MEM_CLASS_STORE, src, grown_bytes); + if (grown_src) { + src = grown_src; + } + int64_t *grown_tgt = + grown_src ? cbm_realloc(CBM_MEM_CLASS_STORE, tgt, grown_bytes) : NULL; + if (grown_tgt) { + tgt = grown_tgt; + } + if (!grown_src || !grown_tgt) { + sqlite3_finalize(stmt); + cbm_store_free_call_edges(src, tgt); + store_set_error(s, "fetch_call_edges: out of memory"); + return CBM_STORE_ERR; + } cap *= ST_GROWTH; - src = safe_realloc(src, (size_t)cap * sizeof(int64_t)); - tgt = safe_realloc(tgt, (size_t)cap * sizeof(int64_t)); } src[n] = sqlite3_column_int64(stmt, 0); tgt[n] = sqlite3_column_int64(stmt, 1); @@ -4146,8 +4168,7 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge } sqlite3_finalize(stmt); if (scan_rc != SQLITE_DONE && scan_rc != SQLITE_ROW) { - free(src); - free(tgt); + cbm_store_free_call_edges(src, tgt); store_set_error_sqlite(s, "fetch_call_edges scan"); return CBM_STORE_ERR; } @@ -4158,8 +4179,8 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge } void cbm_store_free_call_edges(int64_t *src, int64_t *tgt) { - free(src); - free(tgt); + cbm_free(CBM_MEM_CLASS_STORE, src); + cbm_free(CBM_MEM_CLASS_STORE, tgt); } int cbm_store_find_edges_by_source(cbm_store_t *s, int64_t source_id, cbm_edge_t **out, @@ -10499,7 +10520,7 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t * } void cbm_leiden_free(cbm_louvain_result_t *result) { - free(result); + cbm_free(CBM_MEM_CLASS_OTHER, result); /* cbm_leiden allocates OTHER */ } /* ── Architecture: community clusters via Leiden ───────────────── */ diff --git a/src/store/store.h b/src/store/store.h index 3e2f3a5468..59c281443b 100644 --- a/src/store/store.h +++ b/src/store/store.h @@ -708,7 +708,8 @@ int64_t cbm_store_insert_edge(cbm_store_t *s, const cbm_edge_t *e); int cbm_store_insert_edge_batch(cbm_store_t *s, const cbm_edge_t *edges, int count); /* Fetch all CALLS edges among Function/Method nodes for a project as parallel - * (source_id, target_id) arrays (caller frees both). For SCC / cycle analysis. + * (source_id, target_id) arrays, released together with + * cbm_store_free_call_edges. For SCC / cycle analysis. * Stops at max_edges and sets *truncated — never a silent cap. Returns * CBM_STORE_OK (or _ERR); *count is the number returned. */ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edges, @@ -1291,7 +1292,7 @@ typedef struct { * reported community is internally connected. The resolution parameter * controls granularity (higher -> more, smaller communities); 1.0 is standard. * Allocates *out (length *out_count == node_count) in CBM_MEM_CLASS_OTHER; - * the caller releases it with cbm_free(CBM_MEM_CLASS_OTHER, *out). */ + * the caller releases it with cbm_leiden_free(*out). */ int cbm_leiden(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *edges, int edge_count, double resolution, cbm_louvain_result_t **out, int *out_count); diff --git a/src/ui/http_server.c b/src/ui/http_server.c index ffc834f6d4..6e153686ed 100644 --- a/src/ui/http_server.c +++ b/src/ui/http_server.c @@ -2042,7 +2042,7 @@ static bool attach_missed_graph(yyjson_mut_doc *mdoc, yyjson_mut_val *mroot, cbm return false; } yyjson_doc *mldoc = yyjson_read(mjson, strlen(mjson), 0); - free(mjson); + cbm_layout_free_json(mjson); if (!mldoc) { return false; } @@ -2164,7 +2164,7 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) { return; } cbm_http_replyf(c, 200, g_cors_json, "%s", scoped_json); - free(scoped_json); + cbm_layout_free_json(scoped_json); return; } @@ -2200,13 +2200,13 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) { if (linked_count == 0 && missed_graph) { cbm_store_close(store); cbm_http_replyf(c, 200, g_cors_json, "%s", primary_json); - free(primary_json); + cbm_layout_free_json(primary_json); return; } /* Parse primary JSON and append missed_graph + linked_projects */ yyjson_doc *pdoc = yyjson_read(primary_json, strlen(primary_json), 0); - free(primary_json); + cbm_layout_free_json(primary_json); if (!pdoc) { cbm_store_close(store); cbm_http_replyf(c, 500, g_cors_json, "{\"error\":\"JSON parse failed\"}"); @@ -2258,7 +2258,7 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) { /* Parse linked project layout */ yyjson_doc *lpdoc = yyjson_read(lp_json, strlen(lp_json), 0); - free(lp_json); + cbm_layout_free_json(lp_json); if (!lpdoc) { cbm_store_close(lp_store); free(linked[li]); diff --git a/src/ui/layout3d.c b/src/ui/layout3d.c index 89497837a8..c9abaf773d 100644 --- a/src/ui/layout3d.c +++ b/src/ui/layout3d.c @@ -14,6 +14,7 @@ #include "ui/layout3d.h" #include "ui/layout_internal.h" #include "foundation/log.h" +#include "foundation/json_writer.h" #include "foundation/mem_core.h" #include "foundation/platform.h" @@ -878,10 +879,12 @@ char *cbm_layout_to_json(const cbm_layout_result_t *r) { yyjson_mut_obj_add_val(doc, root, "edges", ea); yyjson_mut_obj_add_int(doc, root, "total_nodes", r->total_nodes); + /* The output string is an OTHER-class memory-core block, released by + * cbm_layout_free_json; the document keeps its own allocator. */ size_t len = 0; yyjson_write_err write_err = {0}; - char *json = - yyjson_mut_write_opts(doc, YYJSON_WRITE_ALLOW_INVALID_UNICODE, NULL, &len, &write_err); + char *json = yyjson_mut_write_opts(doc, YYJSON_WRITE_ALLOW_INVALID_UNICODE, + cbm_json_writer_allocator(), &len, &write_err); yyjson_mut_doc_free(doc); if (!json) { char code[CBM_SZ_32]; @@ -893,5 +896,5 @@ char *cbm_layout_to_json(const cbm_layout_result_t *r) { } void cbm_layout_free_json(char *json) { - free(json); + cbm_free(CBM_MEM_CLASS_OTHER, json); } diff --git a/src/ui/layout3d.h b/src/ui/layout3d.h index 29b13712a4..6d4cd22edf 100644 --- a/src/ui/layout3d.h +++ b/src/ui/layout3d.h @@ -69,7 +69,7 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project, /* Release a computed layout result and its fields through their matching allocators. */ void cbm_layout_free(cbm_layout_result_t *result); -/* Serialize layout result to JSON string. Caller must free(). */ +/* Serialize layout result to JSON string. Release with cbm_layout_free_json(). */ char *cbm_layout_to_json(const cbm_layout_result_t *result); /* Free the JSON string returned by cbm_layout_to_json. */ diff --git a/tests/test_store_arch.c b/tests/test_store_arch.c index 50d4345634..92af0994e6 100644 --- a/tests/test_store_arch.c +++ b/tests/test_store_arch.c @@ -1431,6 +1431,64 @@ TEST(fetch_call_edges_empty_project) { PASS(); } +/* The two arrays cbm_store_fetch_call_edges hands out are memory-core blocks of + * the STORE class, and cbm_store_free_call_edges is what takes them back. Both + * halves are measured: the fetch adds exactly the two arrays and the paired free + * removes exactly those, so a raw allocation on either side fails. Thirty-six + * edges outgrow the initial capacity of 16, so the growth path is covered too. */ +TEST(fetch_call_edges_memory_core_ownership) { + enum { FN = 37 }; + cbm_store_t *s = cbm_store_open_memory(); + ASSERT_NOT_NULL(s); + cbm_store_upsert_project(s, "grow", "/tmp/grow"); + int64_t ids[FN]; + for (int i = 0; i < FN; i++) { + char name[32]; + char qn[64]; + snprintf(name, sizeof(name), "f%d", i); + snprintf(qn, sizeof(qn), "grow.pkg.f%d", i); + cbm_node_t n = {.project = "grow", + .label = "Function", + .name = name, + .qualified_name = qn, + .file_path = "pkg/f.go"}; + ids[i] = cbm_store_upsert_node(s, &n); + ASSERT_TRUE(ids[i] > 0); + } + for (int i = 0; i + 1 < FN; i++) { + cbm_edge_t e = { + .project = "grow", .source_id = ids[i], .target_id = ids[i + 1], .type = "CALLS"}; + ASSERT_TRUE(cbm_store_insert_edge(s, &e) > 0); + } + + size_t bytes = cbm_mem_class_live_bytes(CBM_MEM_CLASS_STORE); + size_t blocks = cbm_mem_class_live_blocks(CBM_MEM_CLASS_STORE); + int64_t *src = NULL; + int64_t *tgt = NULL; + int count = 0; + bool truncated = true; + ASSERT_EQ(cbm_store_fetch_call_edges(s, "grow", 100, &src, &tgt, &count, &truncated), + CBM_STORE_OK); + ASSERT_EQ(count, FN - 1); + ASSERT_FALSE(truncated); + ASSERT_NOT_NULL(src); + ASSERT_NOT_NULL(tgt); + /* Ordered by (source_id, target_id): growth kept every pair in place. */ + ASSERT_EQ(src[0], ids[0]); + ASSERT_EQ(tgt[0], ids[1]); + ASSERT_EQ(src[FN - 2], ids[FN - 2]); + ASSERT_EQ(tgt[FN - 2], ids[FN - 1]); + ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_STORE), blocks + 2); + ASSERT_TRUE(cbm_mem_class_live_bytes(CBM_MEM_CLASS_STORE) >= + bytes + 2 * (size_t)count * sizeof(int64_t)); + + cbm_store_free_call_edges(src, tgt); + ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_STORE), bytes); + ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_STORE), blocks); + cbm_store_close(s); + PASS(); +} + /* ── Louvain tests ──────────────────────────────────────────────── */ TEST(louvain_basic) { @@ -1564,7 +1622,8 @@ TEST(leiden_result_memory_core_ownership) { ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks + 1); ASSERT_TRUE(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER) >= bytes + 3 * sizeof(*result)); - cbm_free(CBM_MEM_CLASS_OTHER, result); + /* Released through the exported pair, the way an FFI caller must. */ + cbm_leiden_free(result); ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER), bytes); ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks); } @@ -2014,6 +2073,7 @@ SUITE(store_arch) { RUN_TEST(fetch_call_edges_basic); RUN_TEST(fetch_call_edges_truncated); RUN_TEST(fetch_call_edges_empty_project); + RUN_TEST(fetch_call_edges_memory_core_ownership); /* Louvain */ RUN_TEST(louvain_basic); diff --git a/tests/test_ui.c b/tests/test_ui.c index 4d31410d91..8c3a600060 100644 --- a/tests/test_ui.c +++ b/tests/test_ui.c @@ -562,6 +562,40 @@ TEST(layout_to_json) { PASS(); } +/* cbm_layout_to_json hands out one memory-core block of the OTHER class, and + * cbm_layout_free_json takes exactly that block back. A raw allocation on either + * side of the pair fails the count. */ +TEST(layout_json_memory_core_ownership) { + cbm_store_t *store = cbm_store_open_memory(); + ASSERT_NOT_NULL(store); + cbm_store_upsert_project(store, "test", "/tmp/test"); + cbm_node_t n = {.project = "test", + .label = "Function", + .name = "hello", + .qualified_name = "test::hello", + .file_path = "a.c", + .start_line = 1, + .end_line = 5}; + cbm_store_upsert_node(store, &n); + cbm_layout_result_t *r = cbm_layout_compute(store, "test", CBM_LAYOUT_OVERVIEW, NULL, 0, 100); + ASSERT_NOT_NULL(r); + + size_t bytes = cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER); + size_t blocks = cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER); + char *json = cbm_layout_to_json(r); + ASSERT_NOT_NULL(json); + ASSERT(strstr(json, "\"hello\"") != NULL); + ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks + 1); + ASSERT_TRUE(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER) >= bytes + strlen(json) + 1); + + cbm_layout_free_json(json); + ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER), bytes); + ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks); + cbm_layout_free(r); + cbm_store_close(store); + PASS(); +} + TEST(layout_null_inputs) { /* NULL store → NULL result */ cbm_layout_result_t *r = cbm_layout_compute(NULL, "test", CBM_LAYOUT_OVERVIEW, NULL, 0, 100); @@ -2698,6 +2732,7 @@ SUITE(ui) { RUN_TEST(layout_honors_budget_above_default); RUN_TEST(layout_deterministic); RUN_TEST(layout_to_json); + RUN_TEST(layout_json_memory_core_ownership); RUN_TEST(layout_null_inputs); RUN_TEST(layout_dead_code_classification); RUN_TEST(layout_coincident_nodes_bounded);