1529 lines
54 KiB
C
1529 lines
54 KiB
C
/*
|
|
* test_store_search.c — Tests for search and traversal operations.
|
|
*
|
|
* Ported from internal/store/store_test.go (TestSearch, TestBFS, etc.)
|
|
*/
|
|
#include "../src/foundation/compat.h"
|
|
#include "test_framework.h"
|
|
#include "test_helpers.h"
|
|
#include <store/store.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
|
|
/* Helper: create a typical graph for search/traversal tests.
|
|
*
|
|
* Nodes: SubmitOrder (Function), ProcessOrder (Function), OrderService (Class)
|
|
* Edges: SubmitOrder → ProcessOrder (CALLS)
|
|
*
|
|
* Returns store handle. Fills ids[3].
|
|
*/
|
|
static cbm_store_t *setup_search_store(int64_t *ids) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_node_t n1 = {.project = "test",
|
|
.label = "Function",
|
|
.name = "SubmitOrder",
|
|
.qualified_name = "test.main.SubmitOrder",
|
|
.file_path = "main.go"};
|
|
cbm_node_t n2 = {.project = "test",
|
|
.label = "Function",
|
|
.name = "ProcessOrder",
|
|
.qualified_name = "test.service.ProcessOrder",
|
|
.file_path = "service.go"};
|
|
cbm_node_t n3 = {.project = "test",
|
|
.label = "Class",
|
|
.name = "OrderService",
|
|
.qualified_name = "test.service.OrderService",
|
|
.file_path = "service.go"};
|
|
|
|
ids[0] = cbm_store_upsert_node(s, &n1);
|
|
ids[1] = cbm_store_upsert_node(s, &n2);
|
|
ids[2] = cbm_store_upsert_node(s, &n3);
|
|
|
|
cbm_edge_t e = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
|
cbm_store_insert_edge(s, &e);
|
|
|
|
return s;
|
|
}
|
|
|
|
/* ── Search by label ────────────────────────────────────────────── */
|
|
|
|
TEST(store_search_by_label) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = setup_search_store(ids);
|
|
|
|
cbm_search_params_t params = {
|
|
.project = "test", .label = "Function", .min_degree = -1, .max_degree = -1};
|
|
cbm_search_output_t out = {0};
|
|
int rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(out.count, 2);
|
|
ASSERT_EQ(out.total, 2);
|
|
cbm_store_search_free(&out);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Search by name pattern ─────────────────────────────────────── */
|
|
|
|
TEST(store_search_by_name_pattern) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = setup_search_store(ids);
|
|
|
|
cbm_search_params_t params = {
|
|
.project = "test", .name_pattern = ".*Submit.*", .min_degree = -1, .max_degree = -1};
|
|
cbm_search_output_t out = {0};
|
|
int rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(out.count, 1);
|
|
ASSERT_STR_EQ(out.results[0].node.name, "SubmitOrder");
|
|
cbm_store_search_free(&out);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Empty-string label is ignored (issue #481) ────────────────── */
|
|
|
|
/* An empty-string label must behave like an omitted label (no filter), not be
|
|
* applied as a literal `n.label = ''` that matches nothing. Previously a
|
|
* name_pattern/qn_pattern search passing label="" returned zero results, while
|
|
* the BM25 query path ignored the empty label — an inconsistency that made
|
|
* structural class/service discovery silently fail. */
|
|
TEST(store_search_empty_label_ignored) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = setup_search_store(ids);
|
|
|
|
/* name_pattern + label="" must match the same as name_pattern alone. */
|
|
cbm_search_params_t empty_label = {.project = "test",
|
|
.name_pattern = ".*Submit.*",
|
|
.label = "",
|
|
.min_degree = -1,
|
|
.max_degree = -1};
|
|
cbm_search_output_t out = {0};
|
|
int rc = cbm_store_search(s, &empty_label, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(out.count, 1);
|
|
ASSERT_STR_EQ(out.results[0].node.name, "SubmitOrder");
|
|
cbm_store_search_free(&out);
|
|
|
|
/* A non-empty label still filters: ".*Order.*" matches three names but only
|
|
* OrderService is a Class. */
|
|
cbm_search_params_t cls = {.project = "test",
|
|
.name_pattern = ".*Order.*",
|
|
.label = "Class",
|
|
.min_degree = -1,
|
|
.max_degree = -1};
|
|
cbm_search_output_t out2 = {0};
|
|
rc = cbm_store_search(s, &cls, &out2);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(out2.count, 1);
|
|
ASSERT_STR_EQ(out2.results[0].node.name, "OrderService");
|
|
cbm_store_search_free(&out2);
|
|
|
|
/* qn_pattern shares the same WHERE builder, so empty label must be ignored
|
|
* there too. */
|
|
cbm_search_params_t qn = {.project = "test",
|
|
.qn_pattern = ".*SubmitOrder",
|
|
.label = "",
|
|
.min_degree = -1,
|
|
.max_degree = -1};
|
|
cbm_search_output_t out3 = {0};
|
|
rc = cbm_store_search(s, &qn, &out3);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(out3.count, 1);
|
|
ASSERT_STR_EQ(out3.results[0].node.name, "SubmitOrder");
|
|
cbm_store_search_free(&out3);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Search by file pattern ─────────────────────────────────────── */
|
|
|
|
TEST(store_search_by_file_pattern) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = setup_search_store(ids);
|
|
|
|
cbm_search_params_t params = {
|
|
.project = "test", .file_pattern = "service*", .min_degree = -1, .max_degree = -1};
|
|
cbm_search_output_t out = {0};
|
|
int rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(out.count, 2);
|
|
cbm_store_search_free(&out);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* Issue #200: a wildcard-free file_pattern must match as a path substring,
|
|
* not require the path to equal it. "offer-server" should match
|
|
* "src/offer-server/decision.js". */
|
|
TEST(store_search_file_pattern_substring_issue200) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
cbm_node_t n = {.project = "test",
|
|
.label = "Function",
|
|
.name = "evaluate",
|
|
.qualified_name = "test.offer.evaluate",
|
|
.file_path = "src/offer-server/decision.js"};
|
|
cbm_store_upsert_node(s, &n);
|
|
|
|
cbm_search_params_t params = {
|
|
.project = "test", .file_pattern = "offer-server", .min_degree = -1, .max_degree = -1};
|
|
cbm_search_output_t out = {0};
|
|
int rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(out.count, 1);
|
|
ASSERT_STR_EQ(out.results[0].node.name, "evaluate");
|
|
cbm_store_search_free(&out);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Search pagination ──────────────────────────────────────────── */
|
|
|
|
TEST(store_search_pagination) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = setup_search_store(ids);
|
|
|
|
/* limit=1 */
|
|
cbm_search_params_t params = {
|
|
.project = "test", .limit = 1, .min_degree = -1, .max_degree = -1};
|
|
cbm_search_output_t out = {0};
|
|
int rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(out.count, 1);
|
|
ASSERT_EQ(out.total, 3);
|
|
cbm_store_search_free(&out);
|
|
|
|
/* limit=1, offset=1 */
|
|
params.offset = 1;
|
|
rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(out.count, 1);
|
|
ASSERT_EQ(out.total, 3);
|
|
cbm_store_search_free(&out);
|
|
|
|
/* offset past end */
|
|
params.offset = 100;
|
|
rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(out.count, 0);
|
|
ASSERT_EQ(out.total, 3);
|
|
cbm_store_search_free(&out);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Search with degree filter ──────────────────────────────────── */
|
|
|
|
static int search_result_index_by_name(const cbm_search_output_t *out, const char *name) {
|
|
for (int i = 0; i < out->count; i++) {
|
|
if (out->results[i].node.name && strcmp(out->results[i].node.name, name) == 0) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
TEST(store_search_degree_filter) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = setup_search_store(ids);
|
|
|
|
/* SubmitOrder has out_degree=1, ProcessOrder has in_degree=1.
|
|
* Degree filters: -1 = no filter, 0+ = active. */
|
|
cbm_search_params_t params = {
|
|
.project = "test", .label = "Function", .min_degree = 1, .max_degree = -1};
|
|
cbm_search_output_t out = {0};
|
|
int rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
/* Both functions have degree >= 1 */
|
|
ASSERT_EQ(out.count, 2);
|
|
cbm_store_search_free(&out);
|
|
|
|
/* max_degree = 0 should find nodes with no counted degree edges */
|
|
params.min_degree = -1; /* no min */
|
|
params.max_degree = 0; /* only zero-degree nodes */
|
|
params.label = "Function";
|
|
rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
/* Neither function has degree 0, so 0 results */
|
|
ASSERT_EQ(out.count, 0);
|
|
cbm_store_search_free(&out);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_search_degree_counts_inherits) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_node_t parent = {.project = "test",
|
|
.label = "Class",
|
|
.name = "AbstractAttachmentDto",
|
|
.qualified_name = "test.AbstractAttachmentDto"};
|
|
int64_t parent_id = cbm_store_upsert_node(s, &parent);
|
|
ASSERT_GT(parent_id, 0);
|
|
|
|
const char *child_names[] = {"AttachmentDtoA", "AttachmentDtoB", "AttachmentDtoC"};
|
|
const char *child_qns[] = {"test.AttachmentDtoA", "test.AttachmentDtoB", "test.AttachmentDtoC"};
|
|
for (int i = 0; i < 3; i++) {
|
|
cbm_node_t child = {.project = "test",
|
|
.label = "Class",
|
|
.name = child_names[i],
|
|
.qualified_name = child_qns[i]};
|
|
int64_t child_id = cbm_store_upsert_node(s, &child);
|
|
ASSERT_GT(child_id, 0);
|
|
cbm_edge_t edge = {
|
|
.project = "test", .source_id = child_id, .target_id = parent_id, .type = "INHERITS"};
|
|
ASSERT_GT(cbm_store_insert_edge(s, &edge), 0);
|
|
}
|
|
|
|
int in_deg = 0;
|
|
int out_deg = 0;
|
|
cbm_store_node_degree(s, parent_id, &in_deg, &out_deg);
|
|
ASSERT_EQ(in_deg, 0);
|
|
ASSERT_EQ(out_deg, 0);
|
|
|
|
cbm_search_params_t params = {
|
|
.project = "test", .label = "Class", .min_degree = -1, .max_degree = -1};
|
|
cbm_search_output_t out = {0};
|
|
int rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
int parent_idx = search_result_index_by_name(&out, "AbstractAttachmentDto");
|
|
ASSERT_GTE(parent_idx, 0);
|
|
ASSERT_EQ(out.results[parent_idx].in_degree, 3);
|
|
ASSERT_EQ(out.results[parent_idx].out_degree, 0);
|
|
cbm_store_search_free(&out);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_search_degree_calls_plus_inherits_no_double_count) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_node_t child = {.project = "test",
|
|
.label = "Class",
|
|
.name = "AttachmentDto",
|
|
.qualified_name = "test.AttachmentDto"};
|
|
cbm_node_t parent = {.project = "test",
|
|
.label = "Class",
|
|
.name = "BaseAttachmentDto",
|
|
.qualified_name = "test.BaseAttachmentDto"};
|
|
cbm_node_t fn = {.project = "test",
|
|
.label = "Function",
|
|
.name = "normalizeAttachment",
|
|
.qualified_name = "test.normalizeAttachment"};
|
|
int64_t child_id = cbm_store_upsert_node(s, &child);
|
|
int64_t parent_id = cbm_store_upsert_node(s, &parent);
|
|
int64_t fn_id = cbm_store_upsert_node(s, &fn);
|
|
ASSERT_GT(child_id, 0);
|
|
ASSERT_GT(parent_id, 0);
|
|
ASSERT_GT(fn_id, 0);
|
|
|
|
cbm_edge_t calls = {
|
|
.project = "test", .source_id = child_id, .target_id = fn_id, .type = "CALLS"};
|
|
cbm_edge_t inherits = {
|
|
.project = "test", .source_id = child_id, .target_id = parent_id, .type = "INHERITS"};
|
|
ASSERT_GT(cbm_store_insert_edge(s, &calls), 0);
|
|
ASSERT_GT(cbm_store_insert_edge(s, &inherits), 0);
|
|
|
|
int in_deg = 0;
|
|
int out_deg = 0;
|
|
cbm_store_node_degree(s, child_id, &in_deg, &out_deg);
|
|
ASSERT_EQ(in_deg, 0);
|
|
ASSERT_EQ(out_deg, 1);
|
|
|
|
cbm_search_params_t params = {
|
|
.project = "test", .label = "Class", .min_degree = -1, .max_degree = -1};
|
|
cbm_search_output_t out = {0};
|
|
int rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
int child_idx = search_result_index_by_name(&out, "AttachmentDto");
|
|
ASSERT_GTE(child_idx, 0);
|
|
ASSERT_EQ(out.results[child_idx].in_degree, 0);
|
|
ASSERT_EQ(out.results[child_idx].out_degree, 2);
|
|
cbm_store_search_free(&out);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_search_min_degree_includes_inherits_only) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_node_t child = {.project = "test",
|
|
.label = "Class",
|
|
.name = "InheritanceOnlyChild",
|
|
.qualified_name = "test.InheritanceOnlyChild"};
|
|
cbm_node_t parent = {.project = "test",
|
|
.label = "Class",
|
|
.name = "InheritanceOnlyParent",
|
|
.qualified_name = "test.InheritanceOnlyParent"};
|
|
int64_t child_id = cbm_store_upsert_node(s, &child);
|
|
int64_t parent_id = cbm_store_upsert_node(s, &parent);
|
|
ASSERT_GT(child_id, 0);
|
|
ASSERT_GT(parent_id, 0);
|
|
|
|
cbm_edge_t edge = {
|
|
.project = "test", .source_id = child_id, .target_id = parent_id, .type = "INHERITS"};
|
|
ASSERT_GT(cbm_store_insert_edge(s, &edge), 0);
|
|
|
|
cbm_search_params_t params = {
|
|
.project = "test", .label = "Class", .min_degree = 1, .max_degree = -1};
|
|
cbm_search_output_t out = {0};
|
|
int rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_GTE(search_result_index_by_name(&out, "InheritanceOnlyParent"), 0);
|
|
cbm_store_search_free(&out);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_search_isolated_node_zero_degree) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_node_t node = {.project = "test",
|
|
.label = "Class",
|
|
.name = "LonelyClass",
|
|
.qualified_name = "test.LonelyClass"};
|
|
int64_t node_id = cbm_store_upsert_node(s, &node);
|
|
ASSERT_GT(node_id, 0);
|
|
|
|
int in_deg = 0;
|
|
int out_deg = 0;
|
|
cbm_store_node_degree(s, node_id, &in_deg, &out_deg);
|
|
ASSERT_EQ(in_deg, 0);
|
|
ASSERT_EQ(out_deg, 0);
|
|
|
|
cbm_search_params_t params = {
|
|
.project = "test", .label = "Class", .min_degree = -1, .max_degree = -1};
|
|
cbm_search_output_t out = {0};
|
|
int rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
int idx = search_result_index_by_name(&out, "LonelyClass");
|
|
ASSERT_GTE(idx, 0);
|
|
ASSERT_EQ(out.results[idx].in_degree, 0);
|
|
ASSERT_EQ(out.results[idx].out_degree, 0);
|
|
cbm_store_search_free(&out);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Search all (no filters) ────────────────────────────────────── */
|
|
|
|
TEST(store_search_all) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = setup_search_store(ids);
|
|
|
|
cbm_search_params_t params = {.project = "test", .min_degree = -1, .max_degree = -1};
|
|
cbm_search_output_t out = {0};
|
|
int rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(out.count, 3);
|
|
ASSERT_EQ(out.total, 3);
|
|
cbm_store_search_free(&out);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── BFS traversal ──────────────────────────────────────────────── */
|
|
|
|
TEST(store_bfs_outbound) {
|
|
int64_t ids[4];
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
/* A → B → C → D chain */
|
|
cbm_node_t na = {
|
|
.project = "test", .label = "Function", .name = "A", .qualified_name = "test.A"};
|
|
cbm_node_t nb = {
|
|
.project = "test", .label = "Function", .name = "B", .qualified_name = "test.B"};
|
|
cbm_node_t nc = {
|
|
.project = "test", .label = "Function", .name = "C", .qualified_name = "test.C"};
|
|
cbm_node_t nd = {
|
|
.project = "test", .label = "Function", .name = "D", .qualified_name = "test.D"};
|
|
ids[0] = cbm_store_upsert_node(s, &na);
|
|
ids[1] = cbm_store_upsert_node(s, &nb);
|
|
ids[2] = cbm_store_upsert_node(s, &nc);
|
|
ids[3] = cbm_store_upsert_node(s, &nd);
|
|
|
|
cbm_edge_t e1 = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
|
cbm_edge_t e2 = {.project = "test", .source_id = ids[1], .target_id = ids[2], .type = "CALLS"};
|
|
cbm_edge_t e3 = {.project = "test", .source_id = ids[2], .target_id = ids[3], .type = "CALLS"};
|
|
cbm_store_insert_edge(s, &e1);
|
|
cbm_store_insert_edge(s, &e2);
|
|
cbm_store_insert_edge(s, &e3);
|
|
|
|
/* BFS from A, outbound, depth 3 */
|
|
const char *types[] = {"CALLS"};
|
|
cbm_traverse_result_t result = {0};
|
|
int rc = cbm_store_bfs(s, ids[0], "outbound", types, 1, 3, 100, &result);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_STR_EQ(result.root.name, "A");
|
|
ASSERT_GTE(result.visited_count, 3); /* B, C, D */
|
|
cbm_store_traverse_free(&result);
|
|
|
|
/* BFS with depth=1 */
|
|
rc = cbm_store_bfs(s, ids[0], "outbound", types, 1, 1, 100, &result);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(result.visited_count, 1); /* only B */
|
|
cbm_store_traverse_free(&result);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_bfs_inbound) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_node_t na = {
|
|
.project = "test", .label = "Function", .name = "A", .qualified_name = "test.A"};
|
|
cbm_node_t nb = {
|
|
.project = "test", .label = "Function", .name = "B", .qualified_name = "test.B"};
|
|
cbm_node_t nc = {
|
|
.project = "test", .label = "Function", .name = "C", .qualified_name = "test.C"};
|
|
ids[0] = cbm_store_upsert_node(s, &na);
|
|
ids[1] = cbm_store_upsert_node(s, &nb);
|
|
ids[2] = cbm_store_upsert_node(s, &nc);
|
|
|
|
/* A → C, B → C */
|
|
cbm_edge_t e1 = {.project = "test", .source_id = ids[0], .target_id = ids[2], .type = "CALLS"};
|
|
cbm_edge_t e2 = {.project = "test", .source_id = ids[1], .target_id = ids[2], .type = "CALLS"};
|
|
cbm_store_insert_edge(s, &e1);
|
|
cbm_store_insert_edge(s, &e2);
|
|
|
|
/* BFS from C, inbound → should find A and B */
|
|
const char *types[] = {"CALLS"};
|
|
cbm_traverse_result_t result = {0};
|
|
int rc = cbm_store_bfs(s, ids[2], "inbound", types, 1, 3, 100, &result);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(result.visited_count, 2); /* A and B */
|
|
cbm_store_traverse_free(&result);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Transaction ────────────────────────────────────────────────── */
|
|
|
|
TEST(store_transaction_commit) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_store_begin(s);
|
|
cbm_node_t n = {
|
|
.project = "test", .label = "Function", .name = "TxTest", .qualified_name = "test.TxTest"};
|
|
cbm_store_upsert_node(s, &n);
|
|
cbm_store_commit(s);
|
|
|
|
int cnt = cbm_store_count_nodes(s, "test");
|
|
ASSERT_EQ(cnt, 1);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_transaction_rollback) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_store_begin(s);
|
|
cbm_node_t n = {
|
|
.project = "test", .label = "Function", .name = "TxTest", .qualified_name = "test.TxTest"};
|
|
cbm_store_upsert_node(s, &n);
|
|
cbm_store_rollback(s);
|
|
|
|
int cnt = cbm_store_count_nodes(s, "test");
|
|
ASSERT_EQ(cnt, 0);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Bulk write mode ────────────────────────────────────────────── */
|
|
|
|
TEST(store_bulk_write_mode) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_store_begin_bulk(s);
|
|
cbm_store_drop_indexes(s);
|
|
|
|
/* Insert many nodes in bulk */
|
|
for (int i = 0; i < 50; i++) {
|
|
char name[16], qn[32];
|
|
snprintf(name, sizeof(name), "f%d", i);
|
|
snprintf(qn, sizeof(qn), "test.f%d", i);
|
|
cbm_node_t n = {.project = "test", .label = "Function", .name = name, .qualified_name = qn};
|
|
cbm_store_upsert_node(s, &n);
|
|
}
|
|
|
|
cbm_store_create_indexes(s);
|
|
cbm_store_end_bulk(s);
|
|
|
|
int cnt = cbm_store_count_nodes(s, "test");
|
|
ASSERT_EQ(cnt, 50);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Schema introspection ───────────────────────────────────────── */
|
|
|
|
TEST(store_schema_info) {
|
|
int64_t ids[3];
|
|
cbm_store_t *s = setup_search_store(ids);
|
|
|
|
cbm_schema_info_t schema = {0};
|
|
int rc = cbm_store_get_schema(s, "test", &schema);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
|
|
/* Should have labels: Function, Class */
|
|
ASSERT_GTE(schema.node_label_count, 2);
|
|
|
|
/* Should have edge type: CALLS */
|
|
ASSERT_GTE(schema.edge_type_count, 1);
|
|
|
|
cbm_store_schema_free(&schema);
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Search with exclude_labels ─────────────────────────────────── */
|
|
|
|
TEST(store_search_exclude_labels) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
/* Create nodes with different labels */
|
|
cbm_node_t n1 = {.project = "test",
|
|
.label = "Function",
|
|
.name = "node_Function",
|
|
.qualified_name = "test.Function.node_0",
|
|
.file_path = "test.go"};
|
|
cbm_node_t n2 = {.project = "test",
|
|
.label = "Route",
|
|
.name = "node_Route",
|
|
.qualified_name = "test.Route.node_1",
|
|
.file_path = "test.go"};
|
|
cbm_node_t n3 = {.project = "test",
|
|
.label = "Method",
|
|
.name = "node_Method",
|
|
.qualified_name = "test.Method.node_2",
|
|
.file_path = "test.go"};
|
|
cbm_node_t n4 = {.project = "test",
|
|
.label = "Route",
|
|
.name = "node_Route2",
|
|
.qualified_name = "test.Route.node_3",
|
|
.file_path = "test.go"};
|
|
cbm_store_upsert_node(s, &n1);
|
|
cbm_store_upsert_node(s, &n2);
|
|
cbm_store_upsert_node(s, &n3);
|
|
cbm_store_upsert_node(s, &n4);
|
|
|
|
/* Search without exclusion */
|
|
cbm_search_params_t params = {
|
|
.project = "test", .limit = 100, .min_degree = -1, .max_degree = -1};
|
|
cbm_search_output_t out = {0};
|
|
int rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
int total = out.total;
|
|
ASSERT_EQ(total, 4);
|
|
cbm_store_search_free(&out);
|
|
|
|
/* Search with Route excluded */
|
|
const char *excl[] = {"Route", NULL};
|
|
cbm_search_params_t params2 = {.project = "test",
|
|
.limit = 100,
|
|
.min_degree = -1,
|
|
.max_degree = -1,
|
|
.exclude_labels = excl};
|
|
cbm_search_output_t out2 = {0};
|
|
rc = cbm_store_search(s, ¶ms2, &out2);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_TRUE(out2.total < total);
|
|
|
|
/* Verify no Route nodes in results */
|
|
for (int i = 0; i < out2.count; i++) {
|
|
ASSERT_FALSE(strcmp(out2.results[i].node.label, "Route") == 0);
|
|
}
|
|
cbm_store_search_free(&out2);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Dump to file ──────────────────────────────────────────────── */
|
|
|
|
TEST(store_dump_to_file) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_node_t n = {.project = "test",
|
|
.label = "Function",
|
|
.name = "Hello",
|
|
.qualified_name = "test.main.Hello",
|
|
.file_path = "main.go",
|
|
.start_line = 1,
|
|
.end_line = 5,
|
|
.properties_json = "{\"sig\":\"func Hello()\"}"};
|
|
int64_t id = cbm_store_upsert_node(s, &n);
|
|
ASSERT_TRUE(id > 0);
|
|
|
|
/* Dump to temp file */
|
|
char *td = th_mktempdir("cbm_dump");
|
|
char path[256];
|
|
snprintf(path, sizeof(path), "%s/test.db", td);
|
|
|
|
int rc = cbm_store_dump_to_file(s, path);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
cbm_store_close(s);
|
|
|
|
/* Open dumped file and verify data */
|
|
cbm_store_t *disk = cbm_store_open_path(path);
|
|
ASSERT_NOT_NULL(disk);
|
|
|
|
cbm_node_t found = {0};
|
|
rc = cbm_store_find_node_by_qn(disk, "test", "test.main.Hello", &found);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_STR_EQ(found.name, "Hello");
|
|
cbm_node_free_fields(&found);
|
|
|
|
cbm_store_close(disk);
|
|
unlink(path);
|
|
PASS();
|
|
}
|
|
|
|
/* ── BFS with cross-service (HTTP_CALLS) edges ─────────────────── */
|
|
|
|
TEST(store_bfs_cross_service) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_node_t na = {
|
|
.project = "test", .label = "Function", .name = "A", .qualified_name = "test.A"};
|
|
cbm_node_t nb = {
|
|
.project = "test", .label = "Function", .name = "B", .qualified_name = "test.B"};
|
|
int64_t idA = cbm_store_upsert_node(s, &na);
|
|
int64_t idB = cbm_store_upsert_node(s, &nb);
|
|
|
|
cbm_edge_t e = {.project = "test", .source_id = idA, .target_id = idB, .type = "HTTP_CALLS"};
|
|
cbm_store_insert_edge(s, &e);
|
|
|
|
/* BFS from A with both CALLS and HTTP_CALLS */
|
|
const char *types[] = {"CALLS", "HTTP_CALLS"};
|
|
cbm_traverse_result_t result = {0};
|
|
int rc = cbm_store_bfs(s, idA, "outbound", types, 2, 1, 200, &result);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_GTE(result.visited_count, 1); /* B */
|
|
|
|
/* Verify that we found B via HTTP_CALLS */
|
|
int found_b = 0;
|
|
for (int i = 0; i < result.visited_count; i++) {
|
|
if (strcmp(result.visited[i].node.name, "B") == 0)
|
|
found_b = 1;
|
|
}
|
|
ASSERT_TRUE(found_b);
|
|
|
|
/* Check edges contain HTTP_CALLS type */
|
|
int found_http = 0;
|
|
for (int i = 0; i < result.edge_count; i++) {
|
|
if (strcmp(result.edges[i].type, "HTTP_CALLS") == 0)
|
|
found_http = 1;
|
|
}
|
|
ASSERT_TRUE(found_http);
|
|
|
|
cbm_store_traverse_free(&result);
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── BFS depth-limited chain ───────────────────────────────────── */
|
|
|
|
TEST(store_bfs_depth_chain) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
/* Build chain: A → B → C → D */
|
|
cbm_node_t na = {
|
|
.project = "test", .label = "Function", .name = "A", .qualified_name = "test.A"};
|
|
cbm_node_t nb = {
|
|
.project = "test", .label = "Function", .name = "B", .qualified_name = "test.B"};
|
|
cbm_node_t nc = {
|
|
.project = "test", .label = "Function", .name = "C", .qualified_name = "test.C"};
|
|
cbm_node_t nd = {
|
|
.project = "test", .label = "Function", .name = "D", .qualified_name = "test.D"};
|
|
int64_t idA = cbm_store_upsert_node(s, &na);
|
|
int64_t idB = cbm_store_upsert_node(s, &nb);
|
|
int64_t idC = cbm_store_upsert_node(s, &nc);
|
|
int64_t idD = cbm_store_upsert_node(s, &nd);
|
|
|
|
cbm_edge_t e1 = {.project = "test", .source_id = idA, .target_id = idB, .type = "CALLS"};
|
|
cbm_edge_t e2 = {.project = "test", .source_id = idB, .target_id = idC, .type = "CALLS"};
|
|
cbm_edge_t e3 = {.project = "test", .source_id = idC, .target_id = idD, .type = "CALLS"};
|
|
cbm_store_insert_edge(s, &e1);
|
|
cbm_store_insert_edge(s, &e2);
|
|
cbm_store_insert_edge(s, &e3);
|
|
|
|
/* BFS from A, depth=3 should find B(hop1), C(hop2), D(hop3) */
|
|
const char *types[] = {"CALLS"};
|
|
cbm_traverse_result_t result = {0};
|
|
int rc = cbm_store_bfs(s, idA, "outbound", types, 1, 3, 100, &result);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(result.visited_count, 3);
|
|
|
|
/* Verify hop distances */
|
|
for (int i = 0; i < result.visited_count; i++) {
|
|
if (strcmp(result.visited[i].node.name, "B") == 0)
|
|
ASSERT_EQ(result.visited[i].hop, 1);
|
|
if (strcmp(result.visited[i].node.name, "C") == 0)
|
|
ASSERT_EQ(result.visited[i].hop, 2);
|
|
if (strcmp(result.visited[i].node.name, "D") == 0)
|
|
ASSERT_EQ(result.visited[i].hop, 3);
|
|
}
|
|
|
|
cbm_store_traverse_free(&result);
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Search case insensitive ───────────────────────────────────── */
|
|
|
|
TEST(store_search_case_insensitive) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_node_t n = {.project = "test",
|
|
.label = "Function",
|
|
.name = "HandleRequest",
|
|
.qualified_name = "test.HandleRequest"};
|
|
cbm_store_upsert_node(s, &n);
|
|
|
|
/* Case-insensitive search (default) */
|
|
cbm_search_params_t params = {.project = "test",
|
|
.name_pattern = ".*handlerequest.*",
|
|
.min_degree = -1,
|
|
.max_degree = -1,
|
|
.case_sensitive = false};
|
|
cbm_search_output_t out = {0};
|
|
int rc = cbm_store_search(s, ¶ms, &out);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(out.count, 1);
|
|
cbm_store_search_free(&out);
|
|
|
|
/* Case-sensitive search — should NOT match */
|
|
cbm_search_params_t params2 = {.project = "test",
|
|
.name_pattern = ".*handlerequest.*",
|
|
.min_degree = -1,
|
|
.max_degree = -1,
|
|
.case_sensitive = true};
|
|
cbm_search_output_t out2 = {0};
|
|
rc = cbm_store_search(s, ¶ms2, &out2);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(out2.count, 0);
|
|
cbm_store_search_free(&out2);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Impact: HopToRisk ─────────────────────────────────────────── */
|
|
|
|
TEST(store_hop_to_risk) {
|
|
ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(1)), "CRITICAL");
|
|
ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(2)), "HIGH");
|
|
ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(3)), "MEDIUM");
|
|
ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(4)), "LOW");
|
|
ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(5)), "LOW");
|
|
ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(10)), "LOW");
|
|
PASS();
|
|
}
|
|
|
|
/* ── Impact: BuildImpactSummary ────────────────────────────────── */
|
|
|
|
TEST(store_build_impact_summary) {
|
|
cbm_node_hop_t hops[5] = {
|
|
{.node = {.id = 1}, .hop = 1}, {.node = {.id = 2}, .hop = 1}, {.node = {.id = 3}, .hop = 2},
|
|
{.node = {.id = 4}, .hop = 3}, {.node = {.id = 5}, .hop = 4},
|
|
};
|
|
cbm_edge_info_t edges[1] = {
|
|
{.from_name = "A", .to_name = "B", .type = "CALLS"},
|
|
};
|
|
|
|
cbm_impact_summary_t s = cbm_build_impact_summary(hops, 5, edges, 1);
|
|
ASSERT_EQ(s.critical, 2);
|
|
ASSERT_EQ(s.high, 1);
|
|
ASSERT_EQ(s.medium, 1);
|
|
ASSERT_EQ(s.low, 1);
|
|
ASSERT_EQ(s.total, 5);
|
|
ASSERT_FALSE(s.has_cross_service);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Impact: cross-service detection ──────────────────────────── */
|
|
|
|
TEST(store_cross_service_detection) {
|
|
cbm_node_hop_t hops[1] = {{.node = {.id = 1}, .hop = 1}};
|
|
|
|
cbm_edge_info_t edges_http[1] = {
|
|
{.from_name = "A", .to_name = "B", .type = "HTTP_CALLS"},
|
|
};
|
|
cbm_impact_summary_t s1 = cbm_build_impact_summary(hops, 1, edges_http, 1);
|
|
ASSERT_TRUE(s1.has_cross_service);
|
|
|
|
cbm_edge_info_t edges_async[1] = {
|
|
{.from_name = "A", .to_name = "B", .type = "ASYNC_CALLS"},
|
|
};
|
|
cbm_impact_summary_t s2 = cbm_build_impact_summary(hops, 1, edges_async, 1);
|
|
ASSERT_TRUE(s2.has_cross_service);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Impact: DeduplicateHops ──────────────────────────────────── */
|
|
|
|
TEST(store_deduplicate_hops) {
|
|
cbm_node_hop_t hops[4] = {
|
|
{.node = {.id = 1, .name = "A"}, .hop = 2},
|
|
{.node = {.id = 1, .name = "A"}, .hop = 3}, /* duplicate at higher hop */
|
|
{.node = {.id = 2, .name = "B"}, .hop = 1},
|
|
{.node = {.id = 3, .name = "C"}, .hop = 3},
|
|
};
|
|
|
|
cbm_node_hop_t *result = NULL;
|
|
int count = 0;
|
|
int rc = cbm_deduplicate_hops(hops, 4, &result, &count);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
ASSERT_EQ(count, 3);
|
|
|
|
/* Find node 1 — should have minimum hop = 2 */
|
|
int found1 = 0;
|
|
for (int i = 0; i < count; i++) {
|
|
if (result[i].node.id == 1) {
|
|
ASSERT_EQ(result[i].hop, 2);
|
|
found1 = 1;
|
|
}
|
|
}
|
|
ASSERT_TRUE(found1);
|
|
|
|
free(result);
|
|
PASS();
|
|
}
|
|
|
|
/* ── BFS with risk labels (from store_test.go) ─────────────────── */
|
|
|
|
TEST(store_bfs_with_risk_labels) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
/* Build chain: A → B → C → D */
|
|
cbm_node_t na = {
|
|
.project = "test", .label = "Function", .name = "A", .qualified_name = "test.A"};
|
|
cbm_node_t nb = {
|
|
.project = "test", .label = "Function", .name = "B", .qualified_name = "test.B"};
|
|
cbm_node_t nc = {
|
|
.project = "test", .label = "Function", .name = "C", .qualified_name = "test.C"};
|
|
cbm_node_t nd = {
|
|
.project = "test", .label = "Function", .name = "D", .qualified_name = "test.D"};
|
|
int64_t idA = cbm_store_upsert_node(s, &na);
|
|
(void)cbm_store_upsert_node(s, &nb);
|
|
(void)cbm_store_upsert_node(s, &nc);
|
|
(void)cbm_store_upsert_node(s, &nd);
|
|
|
|
cbm_edge_t e1 = {.project = "test", .source_id = idA, .target_id = idA + 1, .type = "CALLS"};
|
|
cbm_edge_t e2 = {
|
|
.project = "test", .source_id = idA + 1, .target_id = idA + 2, .type = "CALLS"};
|
|
cbm_edge_t e3 = {
|
|
.project = "test", .source_id = idA + 2, .target_id = idA + 3, .type = "CALLS"};
|
|
cbm_store_insert_edge(s, &e1);
|
|
cbm_store_insert_edge(s, &e2);
|
|
cbm_store_insert_edge(s, &e3);
|
|
|
|
const char *types[] = {"CALLS"};
|
|
cbm_traverse_result_t result = {0};
|
|
int rc = cbm_store_bfs(s, idA, "outbound", types, 1, 3, 200, &result);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
|
|
/* Deduplicate */
|
|
cbm_node_hop_t *deduped = NULL;
|
|
int dcount = 0;
|
|
cbm_deduplicate_hops(result.visited, result.visited_count, &deduped, &dcount);
|
|
ASSERT_EQ(dcount, 3);
|
|
|
|
/* Verify risk labels */
|
|
for (int i = 0; i < dcount; i++) {
|
|
if (strcmp(deduped[i].node.name, "B") == 0)
|
|
ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(deduped[i].hop)), "CRITICAL");
|
|
if (strcmp(deduped[i].node.name, "C") == 0)
|
|
ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(deduped[i].hop)), "HIGH");
|
|
if (strcmp(deduped[i].node.name, "D") == 0)
|
|
ASSERT_STR_EQ(cbm_risk_label(cbm_hop_to_risk(deduped[i].hop)), "MEDIUM");
|
|
}
|
|
|
|
/* Build summary */
|
|
cbm_impact_summary_t summary =
|
|
cbm_build_impact_summary(deduped, dcount, result.edges, result.edge_count);
|
|
ASSERT_EQ(summary.critical, 1);
|
|
ASSERT_EQ(summary.high, 1);
|
|
ASSERT_EQ(summary.medium, 1);
|
|
ASSERT_EQ(summary.total, 3);
|
|
|
|
free(deduped);
|
|
cbm_store_traverse_free(&result);
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── BFS cross-service summary ─────────────────────────────────── */
|
|
|
|
TEST(store_bfs_cross_service_summary) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
cbm_node_t na = {
|
|
.project = "test", .label = "Function", .name = "A", .qualified_name = "test.A"};
|
|
cbm_node_t nb = {
|
|
.project = "test", .label = "Function", .name = "B", .qualified_name = "test.B"};
|
|
int64_t idA = cbm_store_upsert_node(s, &na);
|
|
int64_t idB = cbm_store_upsert_node(s, &nb);
|
|
|
|
cbm_edge_t e = {.project = "test", .source_id = idA, .target_id = idB, .type = "HTTP_CALLS"};
|
|
cbm_store_insert_edge(s, &e);
|
|
|
|
const char *types[] = {"CALLS", "HTTP_CALLS"};
|
|
cbm_traverse_result_t result = {0};
|
|
int rc = cbm_store_bfs(s, idA, "outbound", types, 2, 1, 200, &result);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
|
|
cbm_impact_summary_t summary = cbm_build_impact_summary(result.visited, result.visited_count,
|
|
result.edges, result.edge_count);
|
|
ASSERT_TRUE(summary.has_cross_service);
|
|
|
|
cbm_store_traverse_free(&result);
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── GlobToLike ─────────────────────────────────────────────────── */
|
|
|
|
TEST(store_glob_to_like) {
|
|
struct {
|
|
const char *pattern;
|
|
const char *want;
|
|
} tests[] = {
|
|
{"**/*.py", "%%.py"},
|
|
{"**/dir/**", "%dir%"},
|
|
{"*.go", "%.go"},
|
|
{"src/**", "src%"},
|
|
{"**/test_*.py", "%test_%.py"},
|
|
{"file?.txt", "file_.txt"},
|
|
{"exact.go", "exact.go"},
|
|
{"**/custom-pip-package/**", "%custom-pip-package%"},
|
|
};
|
|
|
|
for (int i = 0; i < 8; i++) {
|
|
char *got = cbm_glob_to_like(tests[i].pattern);
|
|
ASSERT_NOT_NULL(got);
|
|
ASSERT_STR_EQ(got, tests[i].want);
|
|
free(got);
|
|
}
|
|
|
|
/* NULL returns NULL */
|
|
ASSERT_NULL(cbm_glob_to_like(NULL));
|
|
|
|
PASS();
|
|
}
|
|
|
|
/* ── ExtractLikeHints ────────────────────────────────────────────── */
|
|
|
|
TEST(store_extract_like_hints) {
|
|
char *hints[16];
|
|
int n;
|
|
|
|
/* Basic: .*handler.* → ["handler"] */
|
|
n = cbm_extract_like_hints(".*handler.*", hints, 16);
|
|
ASSERT_EQ(n, 1);
|
|
ASSERT_STR_EQ(hints[0], "handler");
|
|
free(hints[0]);
|
|
|
|
/* Multiple segments: .*Order.*Handler.* → ["Order", "Handler"] */
|
|
n = cbm_extract_like_hints(".*Order.*Handler.*", hints, 16);
|
|
ASSERT_EQ(n, 2);
|
|
ASSERT_STR_EQ(hints[0], "Order");
|
|
ASSERT_STR_EQ(hints[1], "Handler");
|
|
free(hints[0]);
|
|
free(hints[1]);
|
|
|
|
/* Plain literal: "handler" → ["handler"] */
|
|
n = cbm_extract_like_hints("handler", hints, 16);
|
|
ASSERT_EQ(n, 1);
|
|
ASSERT_STR_EQ(hints[0], "handler");
|
|
free(hints[0]);
|
|
|
|
/* Anchored: ^handleRequest$ → ["handleRequest"] */
|
|
n = cbm_extract_like_hints("^handleRequest$", hints, 16);
|
|
ASSERT_EQ(n, 1);
|
|
ASSERT_STR_EQ(hints[0], "handleRequest");
|
|
free(hints[0]);
|
|
|
|
/* Too generic: .* → no hints */
|
|
n = cbm_extract_like_hints(".*", hints, 16);
|
|
ASSERT_EQ(n, 0);
|
|
|
|
/* Short literal: .*ab.* → "ab" is only 2 chars, below threshold */
|
|
n = cbm_extract_like_hints(".*ab.*", hints, 16);
|
|
ASSERT_EQ(n, 0);
|
|
|
|
/* Exactly 3 chars: .*abc.* → ["abc"] */
|
|
n = cbm_extract_like_hints(".*abc.*", hints, 16);
|
|
ASSERT_EQ(n, 1);
|
|
ASSERT_STR_EQ(hints[0], "abc");
|
|
free(hints[0]);
|
|
|
|
/* Alternation: bail out */
|
|
n = cbm_extract_like_hints(".*foo|.*bar", hints, 16);
|
|
ASSERT_EQ(n, 0);
|
|
|
|
n = cbm_extract_like_hints(".*Order.*|.*Handler.*", hints, 16);
|
|
ASSERT_EQ(n, 0);
|
|
|
|
/* Escaped dot: \\. is ".", only 1 char */
|
|
n = cbm_extract_like_hints("\\.", hints, 16);
|
|
ASSERT_EQ(n, 0);
|
|
|
|
/* Multi-segment with underscore: .*test_.*helper.* → ["test_", "helper"] */
|
|
n = cbm_extract_like_hints(".*test_.*helper.*", hints, 16);
|
|
ASSERT_EQ(n, 2);
|
|
ASSERT_STR_EQ(hints[0], "test_");
|
|
ASSERT_STR_EQ(hints[1], "helper");
|
|
free(hints[0]);
|
|
free(hints[1]);
|
|
|
|
/* NULL safety */
|
|
n = cbm_extract_like_hints(NULL, hints, 16);
|
|
ASSERT_EQ(n, 0);
|
|
|
|
PASS();
|
|
}
|
|
|
|
/* ── EnsureCaseInsensitive ──────────────────────────────────────── */
|
|
|
|
TEST(store_ensure_case_insensitive) {
|
|
ASSERT_STR_EQ(cbm_ensure_case_insensitive("handler"), "(?i)handler");
|
|
ASSERT_STR_EQ(cbm_ensure_case_insensitive("(?i)handler"), "(?i)handler");
|
|
ASSERT_STR_EQ(cbm_ensure_case_insensitive(".*Order.*"), "(?i).*Order.*");
|
|
ASSERT_STR_EQ(cbm_ensure_case_insensitive(""), "(?i)");
|
|
PASS();
|
|
}
|
|
|
|
/* ── StripCaseFlag ──────────────────────────────────────────────── */
|
|
|
|
TEST(store_strip_case_flag) {
|
|
ASSERT_STR_EQ(cbm_strip_case_flag("(?i)handler"), "handler");
|
|
ASSERT_STR_EQ(cbm_strip_case_flag("handler"), "handler");
|
|
ASSERT_STR_EQ(cbm_strip_case_flag("(?i)(?i)double"), "(?i)double");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_batch_count_degrees) {
|
|
cbm_store_t *s = cbm_store_open_memory();
|
|
ASSERT_NOT_NULL(s);
|
|
cbm_store_upsert_project(s, "test", "/tmp/test");
|
|
|
|
/* A -> B, A -> C, B -> C (CALLS), A -> C (USAGE) */
|
|
cbm_node_t na = {
|
|
.project = "test", .label = "Function", .name = "A", .qualified_name = "test.A"};
|
|
cbm_node_t nb = {
|
|
.project = "test", .label = "Function", .name = "B", .qualified_name = "test.B"};
|
|
cbm_node_t nc = {
|
|
.project = "test", .label = "Function", .name = "C", .qualified_name = "test.C"};
|
|
int64_t idA = cbm_store_upsert_node(s, &na);
|
|
int64_t idB = cbm_store_upsert_node(s, &nb);
|
|
int64_t idC = cbm_store_upsert_node(s, &nc);
|
|
|
|
cbm_edge_t e1 = {.project = "test", .source_id = idA, .target_id = idB, .type = "CALLS"};
|
|
cbm_edge_t e2 = {.project = "test", .source_id = idA, .target_id = idC, .type = "CALLS"};
|
|
cbm_edge_t e3 = {.project = "test", .source_id = idB, .target_id = idC, .type = "CALLS"};
|
|
cbm_edge_t e4 = {.project = "test", .source_id = idA, .target_id = idC, .type = "USAGE"};
|
|
cbm_store_insert_edge(s, &e1);
|
|
cbm_store_insert_edge(s, &e2);
|
|
cbm_store_insert_edge(s, &e3);
|
|
cbm_store_insert_edge(s, &e4);
|
|
|
|
/* All edge types */
|
|
int64_t ids3[] = {idA, idB, idC};
|
|
int in3[3], out3[3];
|
|
int rc = cbm_store_batch_count_degrees(s, ids3, 3, NULL, in3, out3);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
/* A: in=0, out=3 (2 CALLS + 1 USAGE) */
|
|
ASSERT_EQ(in3[0], 0);
|
|
ASSERT_EQ(out3[0], 3);
|
|
/* B: in=1, out=1 */
|
|
ASSERT_EQ(in3[1], 1);
|
|
ASSERT_EQ(out3[1], 1);
|
|
/* C: in=3, out=0 */
|
|
ASSERT_EQ(in3[2], 3);
|
|
ASSERT_EQ(out3[2], 0);
|
|
|
|
/* Filtered by CALLS only */
|
|
int64_t ids2[] = {idA, idC};
|
|
int in2[2], out2[2];
|
|
rc = cbm_store_batch_count_degrees(s, ids2, 2, "CALLS", in2, out2);
|
|
ASSERT_EQ(rc, CBM_STORE_OK);
|
|
/* A: in=0, out=2 (CALLS only) */
|
|
ASSERT_EQ(in2[0], 0);
|
|
ASSERT_EQ(out2[0], 2);
|
|
/* C: in=2, out=0 (CALLS only) */
|
|
ASSERT_EQ(in2[1], 2);
|
|
ASSERT_EQ(out2[1], 0);
|
|
|
|
cbm_store_close(s);
|
|
PASS();
|
|
}
|
|
|
|
/* ── GlobToLike edge cases ──────────────────────────────────────── */
|
|
|
|
TEST(store_glob_to_like_empty) {
|
|
char *got = cbm_glob_to_like("");
|
|
ASSERT_NOT_NULL(got);
|
|
ASSERT_STR_EQ(got, "");
|
|
free(got);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_glob_to_like_only_star) {
|
|
char *got = cbm_glob_to_like("*");
|
|
ASSERT_NOT_NULL(got);
|
|
ASSERT_STR_EQ(got, "%");
|
|
free(got);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_glob_to_like_consecutive_doublestar) {
|
|
/* double-star slash double-star should collapse to %% */
|
|
char *got = cbm_glob_to_like("**/**");
|
|
ASSERT_NOT_NULL(got);
|
|
ASSERT_STR_EQ(got, "%%");
|
|
free(got);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_glob_to_like_dot_and_brackets) {
|
|
/* Dots and brackets are literal in glob-to-LIKE — passed through */
|
|
char *got = cbm_glob_to_like("src/[abc]/*.ts");
|
|
ASSERT_NOT_NULL(got);
|
|
ASSERT_STR_EQ(got, "src/[abc]/%.ts");
|
|
free(got);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_glob_to_like_question_marks) {
|
|
/* Multiple ? should produce multiple _ */
|
|
char *got = cbm_glob_to_like("f???.txt");
|
|
ASSERT_NOT_NULL(got);
|
|
ASSERT_STR_EQ(got, "f___.txt");
|
|
free(got);
|
|
PASS();
|
|
}
|
|
|
|
/* ── ExtractLikeHints edge cases ───────────────────────────────── */
|
|
|
|
TEST(store_extract_like_hints_null_out) {
|
|
/* NULL out array */
|
|
int n = cbm_extract_like_hints(".*handler.*", NULL, 16);
|
|
ASSERT_EQ(n, 0);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_extract_like_hints_zero_max) {
|
|
char *hints[4];
|
|
int n = cbm_extract_like_hints(".*handler.*", hints, 0);
|
|
ASSERT_EQ(n, 0);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_extract_like_hints_alternation_complex) {
|
|
char *hints[16];
|
|
/* Alternation with multiple segments on each side */
|
|
int n = cbm_extract_like_hints("(foo|bar)baz", hints, 16);
|
|
ASSERT_EQ(n, 0);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_extract_like_hints_short_segments) {
|
|
char *hints[16];
|
|
/* All segments < 3 chars — no hints */
|
|
int n = cbm_extract_like_hints(".*ab.*cd.*", hints, 16);
|
|
ASSERT_EQ(n, 0);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_extract_like_hints_complex_multi_segment) {
|
|
char *hints[16];
|
|
/* .*Foo.*Bar.* should extract both */
|
|
int n = cbm_extract_like_hints(".*Foo.*Bar.*", hints, 16);
|
|
ASSERT_EQ(n, 2);
|
|
ASSERT_STR_EQ(hints[0], "Foo");
|
|
ASSERT_STR_EQ(hints[1], "Bar");
|
|
free(hints[0]);
|
|
free(hints[1]);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_extract_like_hints_max_out_limit) {
|
|
char *hints[2];
|
|
/* More segments than max_out — should stop at max */
|
|
int n = cbm_extract_like_hints(".*aaa.*bbb.*ccc.*ddd.*", hints, 2);
|
|
ASSERT_EQ(n, 2);
|
|
ASSERT_STR_EQ(hints[0], "aaa");
|
|
ASSERT_STR_EQ(hints[1], "bbb");
|
|
free(hints[0]);
|
|
free(hints[1]);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_extract_like_hints_escaped_chars) {
|
|
char *hints[16];
|
|
/* Backslash escaping: \. makes the dot literal, so it becomes part of the
|
|
* accumulated literal string. ".handler" is the extracted hint. */
|
|
int n = cbm_extract_like_hints("\\.handler", hints, 16);
|
|
ASSERT_EQ(n, 1);
|
|
ASSERT_STR_EQ(hints[0], ".handler");
|
|
free(hints[0]);
|
|
PASS();
|
|
}
|
|
|
|
/* ── Case helper edge cases ────────────────────────────────────── */
|
|
|
|
TEST(store_ensure_case_insensitive_null) {
|
|
const char *result = cbm_ensure_case_insensitive(NULL);
|
|
ASSERT_STR_EQ(result, "");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_ensure_case_insensitive_already_ci) {
|
|
/* Already case-insensitive — should NOT double-prefix */
|
|
ASSERT_STR_EQ(cbm_ensure_case_insensitive("(?i).*Order.*"), "(?i).*Order.*");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_ensure_case_insensitive_plain) {
|
|
ASSERT_STR_EQ(cbm_ensure_case_insensitive("FooBar"), "(?i)FooBar");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_strip_case_flag_null) {
|
|
const char *result = cbm_strip_case_flag(NULL);
|
|
ASSERT_STR_EQ(result, "");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_strip_case_flag_no_flag) {
|
|
ASSERT_STR_EQ(cbm_strip_case_flag("plain_pattern"), "plain_pattern");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_strip_case_flag_empty) {
|
|
ASSERT_STR_EQ(cbm_strip_case_flag(""), "");
|
|
PASS();
|
|
}
|
|
|
|
/* ── Architecture helper edge cases ────────────────────────────── */
|
|
|
|
TEST(store_qn_to_package_single_segment) {
|
|
/* No dots — returns empty string */
|
|
ASSERT_STR_EQ(cbm_qn_to_package("nodots"), "");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_qn_to_package_two_segments) {
|
|
/* project.name — returns segment[1] */
|
|
ASSERT_STR_EQ(cbm_qn_to_package("proj.name"), "name");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_qn_to_package_many_segments) {
|
|
/* project.dir.pkg.Func — 4+ segments returns segment[2] */
|
|
ASSERT_STR_EQ(cbm_qn_to_package("myproj.dir.pkg.Func"), "pkg");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_qn_to_package_null) {
|
|
ASSERT_STR_EQ(cbm_qn_to_package(NULL), "");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_qn_to_package_empty) {
|
|
ASSERT_STR_EQ(cbm_qn_to_package(""), "");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_qn_to_top_package_single_segment) {
|
|
ASSERT_STR_EQ(cbm_qn_to_top_package("nodots"), "");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_qn_to_top_package_two_segments) {
|
|
/* project.dir — returns "dir" */
|
|
ASSERT_STR_EQ(cbm_qn_to_top_package("proj.dir"), "dir");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_qn_to_top_package_many_segments) {
|
|
/* Always returns segment[1] regardless of depth */
|
|
ASSERT_STR_EQ(cbm_qn_to_top_package("proj.dir.sub.Func"), "dir");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_qn_to_top_package_null) {
|
|
ASSERT_STR_EQ(cbm_qn_to_top_package(NULL), "");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_is_test_file_various) {
|
|
/* Positive cases */
|
|
ASSERT_TRUE(cbm_is_test_file_path("test_handler.py"));
|
|
ASSERT_TRUE(cbm_is_test_file_path("handler_test.go"));
|
|
ASSERT_TRUE(cbm_is_test_file_path("handler.test.ts"));
|
|
ASSERT_FALSE(cbm_is_test_file_path("handler.spec.ts")); /* "spec" not "test" — no match */
|
|
ASSERT_TRUE(cbm_is_test_file_path("src/__tests__/handler.js"));
|
|
ASSERT_TRUE(cbm_is_test_file_path("tests/unit/handler.py"));
|
|
|
|
/* Negative cases */
|
|
ASSERT_FALSE(cbm_is_test_file_path("handler.go"));
|
|
ASSERT_FALSE(cbm_is_test_file_path("main.py"));
|
|
ASSERT_FALSE(cbm_is_test_file_path("service.ts"));
|
|
|
|
/* Edge: NULL and empty */
|
|
ASSERT_FALSE(cbm_is_test_file_path(NULL));
|
|
ASSERT_FALSE(cbm_is_test_file_path(""));
|
|
PASS();
|
|
}
|
|
|
|
/* ── Risk/impact edge cases ────────────────────────────────────── */
|
|
|
|
TEST(store_hop_to_risk_all_levels) {
|
|
/* hop 0 hits the default case → LOW */
|
|
ASSERT_EQ(cbm_hop_to_risk(0), CBM_RISK_LOW);
|
|
/* hop 1 → CRITICAL */
|
|
ASSERT_EQ(cbm_hop_to_risk(1), CBM_RISK_CRITICAL);
|
|
/* hop 2 → HIGH */
|
|
ASSERT_EQ(cbm_hop_to_risk(2), CBM_RISK_HIGH);
|
|
/* hop 3 → MEDIUM */
|
|
ASSERT_EQ(cbm_hop_to_risk(3), CBM_RISK_MEDIUM);
|
|
/* hop 4+ → LOW */
|
|
ASSERT_EQ(cbm_hop_to_risk(4), CBM_RISK_LOW);
|
|
ASSERT_EQ(cbm_hop_to_risk(100), CBM_RISK_LOW);
|
|
/* negative → LOW (default) */
|
|
ASSERT_EQ(cbm_hop_to_risk(-1), CBM_RISK_LOW);
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_risk_label_all_levels) {
|
|
ASSERT_STR_EQ(cbm_risk_label(CBM_RISK_CRITICAL), "CRITICAL");
|
|
ASSERT_STR_EQ(cbm_risk_label(CBM_RISK_HIGH), "HIGH");
|
|
ASSERT_STR_EQ(cbm_risk_label(CBM_RISK_MEDIUM), "MEDIUM");
|
|
ASSERT_STR_EQ(cbm_risk_label(CBM_RISK_LOW), "LOW");
|
|
/* Out-of-range enum value falls to default → LOW */
|
|
ASSERT_STR_EQ(cbm_risk_label((cbm_risk_level_t)99), "LOW");
|
|
PASS();
|
|
}
|
|
|
|
TEST(store_impact_summary_empty) {
|
|
/* Zero hops and edges */
|
|
cbm_impact_summary_t s = cbm_build_impact_summary(NULL, 0, NULL, 0);
|
|
ASSERT_EQ(s.total, 0);
|
|
ASSERT_EQ(s.critical, 0);
|
|
ASSERT_EQ(s.high, 0);
|
|
ASSERT_EQ(s.medium, 0);
|
|
ASSERT_EQ(s.low, 0);
|
|
ASSERT_FALSE(s.has_cross_service);
|
|
PASS();
|
|
}
|
|
|
|
SUITE(store_search) {
|
|
RUN_TEST(store_search_by_label);
|
|
RUN_TEST(store_search_by_name_pattern);
|
|
RUN_TEST(store_search_empty_label_ignored);
|
|
RUN_TEST(store_search_by_file_pattern);
|
|
RUN_TEST(store_search_file_pattern_substring_issue200);
|
|
RUN_TEST(store_search_pagination);
|
|
RUN_TEST(store_search_degree_filter);
|
|
RUN_TEST(store_search_degree_counts_inherits);
|
|
RUN_TEST(store_search_degree_calls_plus_inherits_no_double_count);
|
|
RUN_TEST(store_search_min_degree_includes_inherits_only);
|
|
RUN_TEST(store_search_isolated_node_zero_degree);
|
|
RUN_TEST(store_search_all);
|
|
RUN_TEST(store_search_exclude_labels);
|
|
RUN_TEST(store_search_case_insensitive);
|
|
RUN_TEST(store_bfs_outbound);
|
|
RUN_TEST(store_bfs_inbound);
|
|
RUN_TEST(store_bfs_cross_service);
|
|
RUN_TEST(store_bfs_depth_chain);
|
|
RUN_TEST(store_transaction_commit);
|
|
RUN_TEST(store_transaction_rollback);
|
|
RUN_TEST(store_bulk_write_mode);
|
|
RUN_TEST(store_schema_info);
|
|
RUN_TEST(store_dump_to_file);
|
|
RUN_TEST(store_hop_to_risk);
|
|
RUN_TEST(store_build_impact_summary);
|
|
RUN_TEST(store_cross_service_detection);
|
|
RUN_TEST(store_deduplicate_hops);
|
|
RUN_TEST(store_bfs_with_risk_labels);
|
|
RUN_TEST(store_bfs_cross_service_summary);
|
|
RUN_TEST(store_glob_to_like);
|
|
RUN_TEST(store_extract_like_hints);
|
|
RUN_TEST(store_ensure_case_insensitive);
|
|
RUN_TEST(store_strip_case_flag);
|
|
RUN_TEST(store_batch_count_degrees);
|
|
/* Edge case tests */
|
|
RUN_TEST(store_glob_to_like_empty);
|
|
RUN_TEST(store_glob_to_like_only_star);
|
|
RUN_TEST(store_glob_to_like_consecutive_doublestar);
|
|
RUN_TEST(store_glob_to_like_dot_and_brackets);
|
|
RUN_TEST(store_glob_to_like_question_marks);
|
|
RUN_TEST(store_extract_like_hints_null_out);
|
|
RUN_TEST(store_extract_like_hints_zero_max);
|
|
RUN_TEST(store_extract_like_hints_alternation_complex);
|
|
RUN_TEST(store_extract_like_hints_short_segments);
|
|
RUN_TEST(store_extract_like_hints_complex_multi_segment);
|
|
RUN_TEST(store_extract_like_hints_max_out_limit);
|
|
RUN_TEST(store_extract_like_hints_escaped_chars);
|
|
RUN_TEST(store_ensure_case_insensitive_null);
|
|
RUN_TEST(store_ensure_case_insensitive_already_ci);
|
|
RUN_TEST(store_ensure_case_insensitive_plain);
|
|
RUN_TEST(store_strip_case_flag_null);
|
|
RUN_TEST(store_strip_case_flag_no_flag);
|
|
RUN_TEST(store_strip_case_flag_empty);
|
|
RUN_TEST(store_qn_to_package_single_segment);
|
|
RUN_TEST(store_qn_to_package_two_segments);
|
|
RUN_TEST(store_qn_to_package_many_segments);
|
|
RUN_TEST(store_qn_to_package_null);
|
|
RUN_TEST(store_qn_to_package_empty);
|
|
RUN_TEST(store_qn_to_top_package_single_segment);
|
|
RUN_TEST(store_qn_to_top_package_two_segments);
|
|
RUN_TEST(store_qn_to_top_package_many_segments);
|
|
RUN_TEST(store_qn_to_top_package_null);
|
|
RUN_TEST(store_is_test_file_various);
|
|
RUN_TEST(store_hop_to_risk_all_levels);
|
|
RUN_TEST(store_risk_label_all_levels);
|
|
RUN_TEST(store_impact_summary_empty);
|
|
}
|