mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
1478 lines
74 KiB
C
1478 lines
74 KiB
C
#include "test_framework.h"
|
|
#include "test_helpers.h"
|
|
#include "foundation/compat.h"
|
|
#include "foundation/mem_core.h"
|
|
#include "mcp/architecture_jobs.h"
|
|
#include <store/architecture_projection.h>
|
|
#include <foundation/platform.h>
|
|
#include <yyjson/yyjson.h>
|
|
#include <sqlite3.h>
|
|
|
|
static int64_t projection_node(cbm_store_t *store, const char *label, const char *qn,
|
|
const char *file, bool entry) {
|
|
cbm_node_t n = {.project = "projection",
|
|
.label = label,
|
|
.name = qn,
|
|
.qualified_name = qn,
|
|
.file_path = file,
|
|
.start_line = 10,
|
|
.end_line = 20,
|
|
.properties_json = entry ? "{\"is_entry_point\":true}" : "{}"};
|
|
return cbm_store_upsert_node(store, &n);
|
|
}
|
|
|
|
static int64_t projection_edge(cbm_store_t *store, int64_t source, int64_t target,
|
|
const char *type) {
|
|
cbm_edge_t e = {
|
|
.project = "projection", .source_id = source, .target_id = target, .type = type};
|
|
return cbm_store_insert_edge(store, &e);
|
|
}
|
|
|
|
static cbm_store_t *projection_store(void) {
|
|
cbm_store_t *store = cbm_store_open_memory();
|
|
if (store)
|
|
cbm_store_upsert_project(store, "projection", "/tmp/projection");
|
|
return store;
|
|
}
|
|
|
|
static yyjson_val *projection_field(yyjson_doc *doc, const char *field) {
|
|
return yyjson_obj_get(yyjson_doc_get_root(doc), field);
|
|
}
|
|
|
|
TEST(projection_accounts_for_isolated_nodes_and_files) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
projection_node(store, "Project", "projection", "", false);
|
|
projection_node(store, "Folder", "projection.folder", "folder", false);
|
|
projection_node(store, "File", "projection.alone.c", "alone.c", false);
|
|
projection_node(store, "Module", "projection.alone", "alone.c", false);
|
|
projection_node(store, "Function", "projection.alone.work", "alone.c", false);
|
|
projection_node(store, "File", "projection.notes.md", "notes.md", false);
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", NULL, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
yyjson_val *totals = projection_field(doc, "totals");
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(totals, "nodes")), 6);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(totals, "accounted_nodes")), 4);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(totals, "structural_nodes")), 2);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(totals, "files")), 2);
|
|
ASSERT_TRUE(yyjson_get_bool(projection_field(doc, "complete")));
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "components")), 2);
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "dependencies")), 0);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_preserves_typed_edges_and_contiguous_paths) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
projection_node(store, "Package", "projection.a", "", false);
|
|
projection_node(store, "Package", "projection.b", "", false);
|
|
projection_node(store, "Package", "projection.c", "", false);
|
|
int64_t a = projection_node(store, "Function", "projection.a.start", "a.c", true);
|
|
int64_t b1 = projection_node(store, "Function", "projection.b.one", "b.c", false);
|
|
int64_t b2 = projection_node(store, "Function", "projection.b.two", "b.c", false);
|
|
int64_t c = projection_node(store, "Function", "projection.c.end", "c.c", false);
|
|
int64_t first = projection_edge(store, a, b1, "CALLS");
|
|
projection_edge(store, b2, c, "CALLS");
|
|
projection_edge(store, b1, b2, "CALL_REFERENCE");
|
|
char *json = NULL;
|
|
cbm_architecture_projection_options_t opts = {.entry_node_id = a};
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
yyjson_val *paths = projection_field(doc, "paths");
|
|
ASSERT_EQ(yyjson_arr_size(paths), 1);
|
|
yyjson_val *path = yyjson_arr_get(paths, 0);
|
|
yyjson_val *nodes = yyjson_obj_get(path, "nodes"), *edges = yyjson_obj_get(path, "edges");
|
|
ASSERT_EQ(yyjson_arr_size(nodes), 2);
|
|
ASSERT_EQ(yyjson_arr_size(edges), 1);
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(yyjson_arr_get(nodes, 1), "id")), b1);
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(yyjson_arr_get(edges, 0), "id")), first);
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "dependencies")), 2);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
/* A real binding changes the answer; projected component edges alone did not. */
|
|
projection_edge(store, b1, b2, "CALLS");
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
nodes = yyjson_obj_get(yyjson_arr_get(projection_field(doc, "paths"), 0), "nodes");
|
|
ASSERT_EQ(yyjson_arr_size(nodes), 4);
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(yyjson_arr_get(nodes, 3), "id")), c);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_reports_dependency_cycles_without_fabricated_execution) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
projection_node(store, "Package", "projection.a", "", false);
|
|
projection_node(store, "Package", "projection.b", "", false);
|
|
projection_node(store, "Package", "projection.c", "", false);
|
|
int64_t a = projection_node(store, "Function", "projection.a.run", "a.c", true);
|
|
int64_t b = projection_node(store, "Function", "projection.b.run", "b.c", false);
|
|
int64_t c = projection_node(store, "Function", "projection.c.run", "c.c", false);
|
|
projection_edge(store, a, b, "IMPORTS");
|
|
projection_edge(store, b, c, "IMPLEMENTS");
|
|
projection_edge(store, c, a, "USAGE");
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", NULL, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
yyjson_val *cycles = projection_field(doc, "cycles");
|
|
ASSERT_EQ(yyjson_arr_size(cycles), 1);
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(yyjson_arr_get(cycles, 0), "component_ids")), 3);
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "paths")), 0);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_architecture_projection_options_t opts = {.max_components = 2};
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "cycles")), 0);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(projection_field(doc, "totals"), "cycles")), 1);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(projection_field(doc, "limits"), "omitted_cycles")), 1);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_graph_limits_abstain_instead_of_sampling) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
int64_t a = projection_node(store, "Function", "projection.a", "a.c", true);
|
|
int64_t b = projection_node(store, "Function", "projection.b", "b.c", false);
|
|
projection_edge(store, a, b, "CALLS");
|
|
projection_edge(store, b, a, "CALLS");
|
|
cbm_architecture_projection_options_t opts = {.max_nodes = 1};
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
ASSERT_STR_EQ(yyjson_get_str(projection_field(doc, "status")), "limited");
|
|
ASSERT_FALSE(yyjson_get_bool(projection_field(doc, "complete")));
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "components")), 0);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(projection_field(doc, "totals"), "nodes")), 2);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
opts.max_nodes = 0;
|
|
opts.max_edges = 1;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
ASSERT_STR_EQ(yyjson_get_str(projection_field(doc, "status")), "limited");
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "components")), 0);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
static bool projection_cancel(void *context) {
|
|
(void)context;
|
|
return true;
|
|
}
|
|
|
|
TEST(projection_cancellation_deadline_and_store_reuse) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
projection_node(store, "Function", "projection.a", "a.c", true);
|
|
char *json = NULL;
|
|
cbm_architecture_projection_options_t opts = {.cancel = projection_cancel};
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json),
|
|
CBM_STORE_CANCELLED);
|
|
ASSERT_NULL(json);
|
|
opts.cancel = NULL;
|
|
opts.deadline_ms = cbm_now_ms() - 1;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json),
|
|
CBM_STORE_SCAN_LIMIT);
|
|
ASSERT_NULL(json);
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", NULL, &json), CBM_STORE_OK);
|
|
ASSERT_NOT_NULL(json);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
ASSERT_EQ(sqlite3_get_autocommit(cbm_store_get_db(store)), 1);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_display_caps_preserve_total_accounting) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
projection_node(store, "File", "projection.a.c", "a.c", false);
|
|
projection_node(store, "File", "projection.b.c", "b.c", false);
|
|
projection_node(store, "File", "projection.c.c", "c.c", false);
|
|
cbm_architecture_projection_options_t opts = {.max_components = 1};
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "components")), 1);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(projection_field(doc, "totals"), "components")), 3);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(projection_field(doc, "totals"), "accounted_nodes")),
|
|
3);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(projection_field(doc, "limits"), "omitted_components")),
|
|
2);
|
|
ASSERT_FALSE(yyjson_get_bool(projection_field(doc, "complete")));
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_unknown_project_and_empty_project) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "absent", NULL, &json), CBM_STORE_NOT_FOUND);
|
|
ASSERT_NULL(json);
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", NULL, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
ASSERT_TRUE(yyjson_get_bool(projection_field(doc, "complete")));
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "components")), 0);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
projection_node(store, "Project", "projection", "", false);
|
|
projection_node(store, "Folder", "projection.folder", "folder", false);
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", NULL, &json), CBM_STORE_OK);
|
|
doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
ASSERT_TRUE(yyjson_get_bool(projection_field(doc, "complete")));
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "components")), 0);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(projection_field(doc, "totals"), "structural_nodes")),
|
|
2);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_paths_depth_cap_is_explicit) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
int64_t nodes[5];
|
|
for (int i = 0; i < 5; i++) {
|
|
char qn[48];
|
|
snprintf(qn, sizeof(qn), "projection.n%d", i);
|
|
nodes[i] = projection_node(store, "Function", qn, "chain.c", i == 0);
|
|
if (i)
|
|
projection_edge(store, nodes[i - 1], nodes[i], "CALLS");
|
|
}
|
|
cbm_architecture_projection_options_t opts = {.max_depth = 2};
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
ASSERT_TRUE(
|
|
yyjson_get_bool(yyjson_obj_get(projection_field(doc, "limits"), "paths_truncated")));
|
|
yyjson_val *path = yyjson_arr_get(projection_field(doc, "paths"), 0);
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(path, "edges")), 2);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_channel_relationships_are_typed_not_fake_calls) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
int64_t producer = projection_node(store, "Function", "projection.producer", "send.c", true);
|
|
int64_t consumer =
|
|
projection_node(store, "Function", "projection.consumer", "receive.c", false);
|
|
int64_t channel = projection_node(store, "Channel", "projection.orders", "", false);
|
|
projection_edge(store, producer, channel, "EMITS");
|
|
projection_edge(store, consumer, channel, "LISTENS_ON");
|
|
projection_edge(store, producer, consumer, "DATA_FLOWS");
|
|
projection_edge(store, producer, consumer, "SIMILAR_TO");
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", NULL, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "dependencies")), 3);
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "paths")), 0);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(projection_field(doc, "totals"), "unmodeled_edges")),
|
|
1);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_test_callers_cannot_absorb_source_components) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
projection_node(store, "Module", "projection.store", "src/store.c", false);
|
|
int64_t production =
|
|
projection_node(store, "Function", "projection.store.read", "src/store.c", false);
|
|
for (int file = 0; file < 2; file++) {
|
|
char module[64], path[64];
|
|
snprintf(module, sizeof(module), "projection.test%d", file);
|
|
snprintf(path, sizeof(path), "tests/check%d.c", file);
|
|
projection_node(store, "Module", module, path, false);
|
|
for (int i = 0; i < 3; i++) {
|
|
char qn[80];
|
|
snprintf(qn, sizeof(qn), "%s.check%d", module, i);
|
|
int64_t test = projection_node(store, "Function", qn, path, false);
|
|
projection_edge(store, test, production, "CALLS");
|
|
}
|
|
}
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", NULL, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
yyjson_val *parts = projection_field(doc, "components");
|
|
ASSERT_EQ(yyjson_arr_size(parts), 3);
|
|
int non_test = 0, tests = 0;
|
|
for (size_t i = 0; i < yyjson_arr_size(parts); i++) {
|
|
yyjson_val *part = yyjson_arr_get(parts, i);
|
|
const char *role = yyjson_get_str(yyjson_obj_get(part, "role"));
|
|
ASSERT_NOT_NULL(role);
|
|
if (!strcmp(role, "non_test")) {
|
|
non_test++;
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(part, "member_count")), 2);
|
|
} else {
|
|
tests++;
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(part, "member_count")), 4);
|
|
}
|
|
}
|
|
ASSERT_EQ(non_test, 1);
|
|
ASSERT_EQ(tests, 2);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(projection_field(doc, "totals"), "accounted_nodes")),
|
|
10);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_indexed_embedded_test_has_separate_membership) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
int64_t type = projection_node(store, "Class", "projection.Store", "src/store.rs", false);
|
|
int64_t method =
|
|
projection_node(store, "Method", "projection.Store.read", "src/store.rs", false);
|
|
cbm_node_t test = {.project = "projection",
|
|
.label = "Method",
|
|
.name = "embedded_check",
|
|
.qualified_name = "projection.Store.embedded_check",
|
|
.file_path = "src/store.rs",
|
|
.properties_json = "{\"is_test\":true}"};
|
|
int64_t embedded = cbm_store_upsert_node(store, &test);
|
|
projection_edge(store, type, method, "DEFINES_METHOD");
|
|
projection_edge(store, type, embedded, "DEFINES_METHOD");
|
|
projection_edge(store, embedded, method, "CALLS");
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", NULL, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
yyjson_val *parts = projection_field(doc, "components");
|
|
ASSERT_EQ(yyjson_arr_size(parts), 2);
|
|
bool found = false;
|
|
for (size_t i = 0; i < yyjson_arr_size(parts); i++) {
|
|
yyjson_val *part = yyjson_arr_get(parts, i);
|
|
if (!strcmp(yyjson_get_str(yyjson_obj_get(part, "role")), "test")) {
|
|
found = true;
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(part, "member_count")), 1);
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(part, "role_basis")), "indexed_is_test");
|
|
}
|
|
}
|
|
ASSERT_TRUE(found);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_shallow_leaves_do_not_hide_deeper_behavior) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
int64_t entry = projection_node(store, "Function", "projection.main", "main.c", true);
|
|
/* These precede the meaningful branch in both node and edge order. */
|
|
for (int i = 0; i < 13; i++) {
|
|
char qn[64];
|
|
snprintf(qn, sizeof(qn), "projection.utility%d", i);
|
|
int64_t leaf = projection_node(store, "Function", qn, "utility.c", false);
|
|
projection_edge(store, entry, leaf, "CALLS");
|
|
}
|
|
int64_t a = projection_node(store, "Function", "projection.dispatch", "dispatch.c", false);
|
|
int64_t b = projection_node(store, "Function", "projection.query", "query.c", false);
|
|
int64_t end = projection_node(store, "Function", "projection.persist", "store.c", false);
|
|
projection_edge(store, entry, a, "CALLS");
|
|
projection_edge(store, a, b, "CALLS");
|
|
projection_edge(store, b, end, "CALLS");
|
|
char *json = NULL;
|
|
cbm_architecture_projection_options_t opts = {.entry_node_id = entry};
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
yyjson_val *paths = projection_field(doc, "paths");
|
|
ASSERT_EQ(yyjson_arr_size(paths), 12);
|
|
yyjson_val *first = yyjson_arr_get(paths, 0);
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(first, "edges")), 3);
|
|
yyjson_val *nodes = yyjson_obj_get(first, "nodes");
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(yyjson_arr_get(nodes, 3), "id")), end);
|
|
ASSERT_TRUE(
|
|
yyjson_get_bool(yyjson_obj_get(projection_field(doc, "limits"), "paths_truncated")));
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_behavior_preserves_first_hop_diversity) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
int64_t entry = projection_node(store, "Function", "projection.main", "main.c", true);
|
|
int64_t a = projection_node(store, "Function", "projection.a", "a.c", false);
|
|
int64_t a1 = projection_node(store, "Function", "projection.a1", "a.c", false);
|
|
int64_t a2 = projection_node(store, "Function", "projection.a2", "a.c", false);
|
|
int64_t b = projection_node(store, "Function", "projection.b", "b.c", false);
|
|
int64_t b1 = projection_node(store, "Function", "projection.b1", "b.c", false);
|
|
projection_edge(store, entry, a, "CALLS");
|
|
projection_edge(store, a, a1, "CALLS");
|
|
projection_edge(store, a, a2, "CALLS");
|
|
projection_edge(store, entry, b, "CALLS");
|
|
projection_edge(store, b, b1, "CALLS");
|
|
char *json = NULL;
|
|
cbm_architecture_projection_options_t opts = {.entry_node_id = entry, .max_paths = 2};
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
yyjson_val *paths = projection_field(doc, "paths");
|
|
ASSERT_EQ(yyjson_arr_size(paths), 2);
|
|
yyjson_val *first = yyjson_obj_get(yyjson_arr_get(paths, 0), "nodes");
|
|
yyjson_val *second = yyjson_obj_get(yyjson_arr_get(paths, 1), "nodes");
|
|
int64_t hop1 = yyjson_get_sint(yyjson_obj_get(yyjson_arr_get(first, 1), "id"));
|
|
int64_t hop2 = yyjson_get_sint(yyjson_obj_get(yyjson_arr_get(second, 1), "id"));
|
|
ASSERT_TRUE((hop1 == a && hop2 == b) || (hop1 == b && hop2 == a));
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_corridor_preserves_joins_cycles_and_callsite_evidence) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
int64_t a = projection_node(store, "Function", "projection.a", "src/api/main.c", true);
|
|
int64_t b = projection_node(store, "Function", "projection.b", "src/engine/b.c", false);
|
|
int64_t c = projection_node(store, "Function", "projection.c", "src/engine/c.c", false);
|
|
int64_t d = projection_node(store, "Function", "projection.d", "src/store/d.c", false);
|
|
int64_t ignored =
|
|
projection_node(store, "Function", "projection.callback", "src/events/e.c", false);
|
|
cbm_edge_t evidence = {
|
|
.project = "projection",
|
|
.source_id = a,
|
|
.target_id = b,
|
|
.type = "CALLS",
|
|
.properties_json =
|
|
"{\"line\":42,\"strategy\":\"lsp_direct\",\"confidence\":0.95,\"candidates\":1}"};
|
|
int64_t ab = cbm_store_insert_edge(store, &evidence);
|
|
projection_edge(store, a, c, "CALLS");
|
|
projection_edge(store, b, d, "CALLS");
|
|
projection_edge(store, c, d, "CALLS");
|
|
projection_edge(store, b, c, "CALLS");
|
|
projection_edge(store, c, b, "CALLS");
|
|
projection_edge(store, a, ignored, "CALL_REFERENCE");
|
|
projection_edge(store, ignored, d, "EMITS");
|
|
cbm_architecture_projection_options_t opts = {
|
|
.entry_node_id = a, .target_node_id = d, .max_depth = 4};
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
yyjson_val *view = projection_field(doc, "behavior"), *nodes = yyjson_obj_get(view, "nodes"),
|
|
*edges = yyjson_obj_get(view, "edges");
|
|
ASSERT_TRUE(yyjson_get_bool(yyjson_obj_get(view, "complete")));
|
|
ASSERT_TRUE(yyjson_get_bool(yyjson_obj_get(view, "reachable")));
|
|
ASSERT_EQ(yyjson_arr_size(nodes), 4);
|
|
ASSERT_EQ(yyjson_arr_size(edges), 6);
|
|
int into_d = 0;
|
|
bool found_evidence = false;
|
|
for (size_t i = 0; i < yyjson_arr_size(edges); i++) {
|
|
yyjson_val *edge = yyjson_arr_get(edges, i);
|
|
if (yyjson_get_sint(yyjson_obj_get(edge, "target_id")) == d)
|
|
into_d++;
|
|
if (yyjson_get_sint(yyjson_obj_get(edge, "id")) == ab) {
|
|
found_evidence = true;
|
|
yyjson_val *site = yyjson_obj_get(edge, "callsite"),
|
|
*resolution = yyjson_obj_get(edge, "resolution");
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(site, "line")), 42);
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(site, "file_path")), "src/api/main.c");
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(resolution, "strategy")), "lsp_direct");
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(resolution, "candidates")), 1);
|
|
}
|
|
}
|
|
ASSERT_EQ(into_d, 2);
|
|
ASSERT_TRUE(found_evidence);
|
|
yyjson_val *cycles = yyjson_obj_get(view, "cycles");
|
|
ASSERT_EQ(yyjson_arr_size(cycles), 1);
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(yyjson_arr_get(cycles, 0), "node_ids")), 2);
|
|
yyjson_val *paths = projection_field(doc, "paths");
|
|
ASSERT_TRUE(yyjson_arr_size(paths) >= 2);
|
|
for (size_t i = 0; i < yyjson_arr_size(paths); i++) {
|
|
yyjson_val *path = yyjson_arr_get(paths, i), *pn = yyjson_obj_get(path, "nodes"),
|
|
*pe = yyjson_obj_get(path, "edges");
|
|
ASSERT_EQ(yyjson_arr_size(pn), yyjson_arr_size(pe) + 1);
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(yyjson_arr_get(pn, 0), "id")), a);
|
|
ASSERT_EQ(
|
|
yyjson_get_sint(yyjson_obj_get(yyjson_arr_get(pn, yyjson_arr_size(pn) - 1), "id")), d);
|
|
ASSERT_TRUE(yyjson_arr_size(pe) <= 4);
|
|
for (size_t j = 0; j < yyjson_arr_size(pe); j++) {
|
|
yyjson_val *edge = yyjson_arr_get(pe, j);
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(edge, "source_id")),
|
|
yyjson_get_sint(yyjson_obj_get(yyjson_arr_get(pn, j), "id")));
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(edge, "target_id")),
|
|
yyjson_get_sint(yyjson_obj_get(yyjson_arr_get(pn, j + 1), "id")));
|
|
}
|
|
}
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
opts.target_node_id = ignored;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
view = projection_field(doc, "behavior");
|
|
ASSERT_FALSE(yyjson_get_bool(yyjson_obj_get(view, "reachable")));
|
|
ASSERT_TRUE(yyjson_get_bool(yyjson_obj_get(view, "corridor_complete")));
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(view, "nodes")), 0);
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "paths")), 0);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_corridor_caps_abstain_without_hiding_joins) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
int64_t a = projection_node(store, "Function", "projection.a", "a.c", true);
|
|
int64_t b = projection_node(store, "Function", "projection.b", "b.c", false);
|
|
int64_t d = projection_node(store, "Function", "projection.d", "d.c", false);
|
|
projection_edge(store, a, b, "CALLS");
|
|
projection_edge(store, b, d, "CALLS");
|
|
cbm_architecture_projection_options_t opts = {
|
|
.entry_node_id = a, .target_node_id = d, .max_corridor_nodes = 2};
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
yyjson_val *view = projection_field(doc, "behavior");
|
|
ASSERT_FALSE(yyjson_get_bool(yyjson_obj_get(view, "complete")));
|
|
ASSERT_FALSE(yyjson_get_bool(yyjson_obj_get(view, "corridor_complete")));
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(yyjson_obj_get(view, "totals"), "corridor_nodes")), 3);
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(view, "nodes")), 0);
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(view, "edges")), 0);
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "paths")), 0);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
opts.target_node_id = 0;
|
|
opts.max_targets = 1;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
view = projection_field(doc, "behavior");
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(view, "reachable_targets")), 1);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(yyjson_obj_get(view, "totals"), "omitted_targets")), 1);
|
|
ASSERT_FALSE(yyjson_get_bool(yyjson_obj_get(view, "complete")));
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_overview_accounts_for_components_beyond_legacy_cap) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
for (int i = 0; i < 300; i++) {
|
|
char qn[80], file[80];
|
|
snprintf(qn, sizeof(qn), "projection.part%d", i);
|
|
snprintf(file, sizeof(file), "packages/part%d/file.c", i);
|
|
projection_node(store, "File", qn, file, false);
|
|
}
|
|
int64_t entry = projection_node(store, "Function", "projection.entry", "src/main.c", true);
|
|
int64_t target =
|
|
projection_node(store, "Function", "projection.target", "src/service.c", false);
|
|
projection_edge(store, entry, target, "CALLS");
|
|
cbm_architecture_projection_options_t opts = {.max_components = 1, .entry_node_id = entry};
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
yyjson_val *overview = projection_field(doc, "overview"),
|
|
*groups = yyjson_obj_get(overview, "groups"),
|
|
*parts = yyjson_obj_get(overview, "components");
|
|
ASSERT_TRUE(yyjson_get_bool(yyjson_obj_get(overview, "complete")));
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "components")), 1);
|
|
ASSERT_EQ(yyjson_arr_size(parts), 302);
|
|
ASSERT_TRUE(yyjson_arr_size(groups) <= 64);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(overview, "grouping_depth")), 1);
|
|
size_t membership = 0;
|
|
int members = 0;
|
|
for (size_t i = 0; i < yyjson_arr_size(groups); i++) {
|
|
yyjson_val *group = yyjson_arr_get(groups, i);
|
|
membership += yyjson_arr_size(yyjson_obj_get(group, "component_ids"));
|
|
members += yyjson_get_int(yyjson_obj_get(group, "member_count"));
|
|
}
|
|
ASSERT_EQ(membership, 302);
|
|
ASSERT_EQ(members, 302);
|
|
yyjson_val *symbol =
|
|
yyjson_arr_get(yyjson_obj_get(projection_field(doc, "behavior"), "reachable_targets"), 0);
|
|
const char *group_id = yyjson_get_str(yyjson_obj_get(symbol, "group_id"));
|
|
ASSERT_NOT_NULL(group_id);
|
|
char stable[128];
|
|
snprintf(stable, sizeof(stable), "%s", group_id);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
opts.target_node_id = target;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
symbol =
|
|
yyjson_arr_get(yyjson_obj_get(projection_field(doc, "behavior"), "reachable_targets"), 0);
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(symbol, "group_id")), stable);
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(projection_field(doc, "overview"), "components")),
|
|
302);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_aggregate_cycle_does_not_invent_symbol_cycle) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
int64_t a = projection_node(store, "Function", "projection.a", "src/api/a.c", true);
|
|
int64_t b = projection_node(store, "Function", "projection.b", "src/store/b.c", false);
|
|
int64_t c = projection_node(store, "Function", "projection.c", "src/api/c.c", false);
|
|
projection_edge(store, a, b, "CALLS");
|
|
projection_edge(store, b, c, "CALLS");
|
|
cbm_architecture_projection_options_t opts = {.entry_node_id = a, .target_node_id = c};
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(projection_field(doc, "overview"), "groups")), 2);
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(projection_field(doc, "overview"), "connections")), 2);
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(projection_field(doc, "behavior"), "cycles")), 0);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_provenance_budget_counts_utf8_bytes) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
int64_t a = projection_node(store, "Function", "projection.a", "a.c", true);
|
|
int64_t b = projection_node(store, "Function", "projection.b", "b.c", false);
|
|
int64_t d = projection_node(store, "Function", "projection.d", "d.c", false);
|
|
char properties[19000];
|
|
size_t used = (size_t)snprintf(properties, sizeof(properties),
|
|
"{\"line\":42,\"strategy\":\"unique_name\",\"ignored\":\"");
|
|
for (int i = 0; i < 9000; i++) {
|
|
properties[used++] = (char)0xc3;
|
|
properties[used++] = (char)0xa9;
|
|
}
|
|
memcpy(properties + used, "\"}", 3);
|
|
ASSERT_TRUE(strlen(properties) > 16384);
|
|
cbm_edge_t edge = {.project = "projection",
|
|
.source_id = a,
|
|
.target_id = b,
|
|
.type = "CALLS",
|
|
.properties_json = properties};
|
|
int64_t oversized = cbm_store_insert_edge(store, &edge);
|
|
edge.source_id = b;
|
|
edge.target_id = d;
|
|
edge.properties_json = "{\"line\":24}";
|
|
int64_t normal = cbm_store_insert_edge(store, &edge);
|
|
cbm_architecture_projection_options_t opts = {.entry_node_id = a, .target_node_id = d};
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
yyjson_val *edges = yyjson_obj_get(projection_field(doc, "behavior"), "edges");
|
|
ASSERT_EQ(yyjson_arr_size(edges), 2);
|
|
for (size_t i = 0; i < yyjson_arr_size(edges); i++) {
|
|
yyjson_val *value = yyjson_arr_get(edges, i);
|
|
int64_t id = yyjson_get_sint(yyjson_obj_get(value, "id"));
|
|
if (id == oversized) {
|
|
ASSERT_NULL(yyjson_obj_get(value, "callsite"));
|
|
ASSERT_NULL(yyjson_obj_get(value, "resolution"));
|
|
} else {
|
|
ASSERT_EQ(id, normal);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(yyjson_obj_get(value, "callsite"), "line")),
|
|
24);
|
|
}
|
|
}
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_example_budgets_preserve_full_aggregate_counts) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
projection_node(store, "Package", "projection.a", "a.c", false);
|
|
projection_node(store, "Package", "projection.b", "b.c", false);
|
|
for (int i = 0; i < 5; i++) {
|
|
char source[64], target[64];
|
|
snprintf(source, sizeof(source), "projection.a.caller%d", i);
|
|
snprintf(target, sizeof(target), "projection.b.callee%d", i);
|
|
int64_t a = projection_node(store, "Function", source, "a.c", i == 0);
|
|
int64_t b = projection_node(store, "Function", target, "b.c", false);
|
|
projection_edge(store, a, b, "CALLS");
|
|
}
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", NULL, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
yyjson_val *parts = projection_field(doc, "components"),
|
|
*deps = projection_field(doc, "dependencies");
|
|
ASSERT_EQ(yyjson_arr_size(parts), 2);
|
|
ASSERT_EQ(yyjson_arr_size(deps), 1);
|
|
yyjson_val *dependency = yyjson_arr_get(deps, 0);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(dependency, "count")), 5);
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(dependency, "witnesses")), 1);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(dependency, "omitted_witnesses")), 4);
|
|
yyjson_val *overview = projection_field(doc, "overview");
|
|
ASSERT_TRUE(yyjson_get_bool(yyjson_obj_get(overview, "complete")));
|
|
yyjson_val *all = yyjson_obj_get(overview, "components"),
|
|
*groups = yyjson_obj_get(overview, "groups");
|
|
ASSERT_EQ(yyjson_arr_size(all), 2);
|
|
ASSERT_EQ(yyjson_arr_size(groups), 1);
|
|
for (size_t i = 0; i < yyjson_arr_size(all); i++) {
|
|
yyjson_val *part = yyjson_arr_get(all, i);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(part, "member_count")), 6);
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(part, "representatives")), 1);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(part, "omitted_members")), 5);
|
|
}
|
|
yyjson_val *group = yyjson_arr_get(groups, 0);
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(group, "component_ids")), 2);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(group, "member_count")), 12);
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(group, "representatives")), 1);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(group, "omitted_representatives")), 1);
|
|
yyjson_val *connection = yyjson_arr_get(yyjson_obj_get(overview, "connections"), 0);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(connection, "count")), 5);
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(connection, "witnesses")), 1);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(connection, "omitted_witnesses")), 4);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
static int64_t projection_node_properties(cbm_store_t *store, const char *qn, const char *file,
|
|
const char *properties) {
|
|
cbm_node_t node = {.project = "projection",
|
|
.label = "Function",
|
|
.name = qn,
|
|
.qualified_name = qn,
|
|
.file_path = file,
|
|
.start_line = 10,
|
|
.end_line = 20,
|
|
.properties_json = properties};
|
|
return cbm_store_upsert_node(store, &node);
|
|
}
|
|
|
|
static int projection_count_metadata_queries(unsigned kind, void *context, void *statement,
|
|
void *detail) {
|
|
(void)detail;
|
|
if (kind == SQLITE_TRACE_STMT) {
|
|
const char *sql = sqlite3_sql(statement);
|
|
if (sql && strstr(sql, "FROM nodes WHERE id=?1"))
|
|
(*(int *)context)++;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Validate every duplicated occurrence, not just the first path or witness. */
|
|
static int projection_check_contracts(yyjson_val *value, int64_t symbol_id, int64_t edge_id,
|
|
int *symbols, int *edges) {
|
|
if (yyjson_is_obj(value)) {
|
|
if (yyjson_obj_get(value, "qualified_name") &&
|
|
yyjson_get_sint(yyjson_obj_get(value, "id")) == symbol_id) {
|
|
(*symbols)++;
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(value, "signature")),
|
|
"save(value: Value) -> Result");
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(value, "return_type")), "Result");
|
|
yyjson_val *parameters = yyjson_obj_get(value, "parameters");
|
|
ASSERT_NOT_NULL(parameters);
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(parameters, "count")), 1);
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_arr_get(yyjson_obj_get(parameters, "names"), 0)),
|
|
"value");
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_arr_get(yyjson_obj_get(parameters, "types"), 0)),
|
|
"Value");
|
|
}
|
|
if ((yyjson_obj_get(value, "source_id") &&
|
|
yyjson_get_sint(yyjson_obj_get(value, "id")) == edge_id) ||
|
|
yyjson_get_sint(yyjson_obj_get(value, "edge_id")) == edge_id) {
|
|
(*edges)++;
|
|
yyjson_val *arguments = yyjson_obj_get(value, "arguments");
|
|
ASSERT_EQ(yyjson_arr_size(arguments), 1);
|
|
yyjson_val *argument = yyjson_arr_get(arguments, 0);
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(argument, "i")), 7);
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(argument, "e")), "input[\"key\"]");
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(argument, "v")), "line\nvalue");
|
|
ASSERT_NULL(yyjson_obj_get(argument, "keyword"));
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(value, "argument_limit")), 8);
|
|
ASSERT_TRUE(yyjson_is_bool(yyjson_obj_get(value, "arguments_complete")));
|
|
ASSERT_FALSE(yyjson_get_bool(yyjson_obj_get(value, "arguments_complete")));
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(yyjson_obj_get(value, "callsite"), "line")),
|
|
42);
|
|
}
|
|
size_t index, maximum;
|
|
yyjson_val *key, *child;
|
|
yyjson_obj_foreach(value, index, maximum, key,
|
|
child) if (projection_check_contracts(child, symbol_id, edge_id, symbols,
|
|
edges)) return 1;
|
|
} else if (yyjson_is_arr(value)) {
|
|
size_t index, maximum;
|
|
yyjson_val *child;
|
|
yyjson_arr_foreach(value, index, maximum,
|
|
child) if (projection_check_contracts(child, symbol_id, edge_id, symbols,
|
|
edges)) return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
TEST(projection_behavior_evidence_is_opt_in_and_shared_across_surfaces) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
projection_node(store, "Package", "projection.a", "", false);
|
|
projection_node(store, "Package", "projection.b", "", false);
|
|
int64_t a = projection_node(store, "Function", "projection.a.start", "src/api/start.c", true);
|
|
int64_t b = projection_node_properties(
|
|
store, "projection.b.save", "src/store/save.c",
|
|
"{\"signature\":\"save(value: Value) -> Result\",\"return_type\":\"Result\","
|
|
"\"param_names\":[\"value\"],\"param_types\":[\"Value\"],\"param_count\":1}");
|
|
cbm_edge_t evidence = {.project = "projection",
|
|
.source_id = a,
|
|
.target_id = b,
|
|
.type = "CALLS",
|
|
.properties_json =
|
|
"{\"line\":42,\"args\":[{\"i\":7,\"e\":\"input[\\\"key\\\"]\",\"v\":"
|
|
"\"line\\nvalue\",\"keyword\":\"value\"}]}"};
|
|
int64_t call = cbm_store_insert_edge(store, &evidence);
|
|
ASSERT_TRUE(call > 0);
|
|
int queries = 0;
|
|
sqlite3_trace_v2(cbm_store_get_db(store), SQLITE_TRACE_STMT, projection_count_metadata_queries,
|
|
&queries);
|
|
cbm_architecture_projection_options_t opts = {.entry_node_id = a, .target_node_id = b};
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
ASSERT_NULL(strstr(json, "\"signature\""));
|
|
ASSERT_NULL(strstr(json, "\"return_type\""));
|
|
ASSERT_NULL(strstr(json, "\"parameters\""));
|
|
ASSERT_NULL(strstr(json, "\"arguments\""));
|
|
ASSERT_NULL(strstr(json, "\"argument_limit\""));
|
|
ASSERT_EQ(queries, 0);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
|
|
opts.include_behavior_evidence = true;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
int symbols = 0, edges = 0;
|
|
ASSERT_EQ(projection_check_contracts(yyjson_doc_get_root(doc), b, call, &symbols, &edges), 0);
|
|
ASSERT_TRUE(symbols >= 3);
|
|
ASSERT_TRUE(edges >= 4);
|
|
ASSERT_TRUE(queries > 0);
|
|
ASSERT_TRUE(queries <= 4); /* cache once per displayed indexed node */
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
|
|
queries = 0;
|
|
opts.include_behavior_evidence = false;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
ASSERT_NULL(strstr(json, "\"return_type\""));
|
|
ASSERT_NULL(strstr(json, "\"arguments\""));
|
|
ASSERT_EQ(queries, 0);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
sqlite3_trace_v2(cbm_store_get_db(store), 0, NULL, NULL);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_argument_validation_keeps_exact_edge_and_callsite_identity) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
int64_t a = projection_node(store, "Function", "projection.a", "a.c", true);
|
|
int64_t b = projection_node(store, "Function", "projection.b", "b.c", false);
|
|
int64_t target = projection_node(store, "Function", "projection.target", "target.c", false);
|
|
projection_edge(store, a, b, "CALLS");
|
|
cbm_edge_t evidence = {
|
|
.project = "projection",
|
|
.source_id = a,
|
|
.target_id = target,
|
|
.type = "CALLS",
|
|
.properties_json =
|
|
"{\"line\":42,\"args\":[{\"i\":0,\"e\":\"first\",\"v\":\"exact\"},"
|
|
"{\"i\":-1,\"e\":\"negative\"},{\"i\":2},{\"i\":3,\"e\":42},"
|
|
"{\"i\":4,\"e\":\"keep\",\"v\":false},{\"i\":5,\"e\":\"actual\"},"
|
|
"{\"i\":6.5,\"e\":\"float\"},{\"i\":7,\"e\":\"last\"},{\"i\":8,\"e\":\"ninth\"}]}"};
|
|
int64_t first = cbm_store_insert_edge(store, &evidence);
|
|
ASSERT_TRUE(first > 0);
|
|
evidence.source_id = b;
|
|
evidence.properties_json = "{\"line\":84,\"args\":[{\"i\":0,\"e\":\"different\"}]}";
|
|
int64_t second = cbm_store_insert_edge(store, &evidence);
|
|
ASSERT_TRUE(second > 0);
|
|
cbm_architecture_projection_options_t opts = {
|
|
.entry_node_id = a, .target_node_id = target, .include_behavior_evidence = true};
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
yyjson_val *edges = yyjson_obj_get(projection_field(doc, "behavior"), "edges");
|
|
int seen = 0;
|
|
for (size_t i = 0; i < yyjson_arr_size(edges); i++) {
|
|
yyjson_val *value = yyjson_arr_get(edges, i), *args = yyjson_obj_get(value, "arguments");
|
|
int64_t id = yyjson_get_sint(yyjson_obj_get(value, "id"));
|
|
if (id == first) {
|
|
seen++;
|
|
ASSERT_EQ(yyjson_arr_size(args), 4);
|
|
int indexes[] = {0, 4, 5, 7};
|
|
for (size_t at = 0; at < 4; at++)
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(yyjson_arr_get(args, at), "i")),
|
|
indexes[at]);
|
|
ASSERT_NULL(yyjson_obj_get(yyjson_arr_get(args, 1), "v"));
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(yyjson_arr_get(args, 3), "e")), "last");
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(yyjson_obj_get(value, "callsite"), "line")),
|
|
42);
|
|
} else if (id == second) {
|
|
seen++;
|
|
ASSERT_EQ(yyjson_arr_size(args), 1);
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(yyjson_arr_get(args, 0), "e")),
|
|
"different");
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(yyjson_obj_get(value, "callsite"), "line")),
|
|
84);
|
|
ASSERT_STR_EQ(
|
|
yyjson_get_str(yyjson_obj_get(yyjson_obj_get(value, "callsite"), "file_path")),
|
|
"b.c");
|
|
}
|
|
}
|
|
ASSERT_EQ(seen, 2);
|
|
ASSERT_NULL(strstr(json, "ninth"));
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_missing_corrupt_and_noncall_arguments_remain_unknown) {
|
|
const char *properties[] = {"{}", "{\"args\":\"corrupt\"}", "{\"args\":null}", "{\"args\":{}}",
|
|
"{\"args\":[]}"};
|
|
for (size_t i = 0; i < sizeof(properties) / sizeof(properties[0]); i++) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
int64_t a = projection_node(store, "Function", "projection.a", "a.c", true);
|
|
int64_t b = projection_node_properties(
|
|
store, "projection.b", "b.c",
|
|
"{\"signature\":42,\"return_type\":\"Known\",\"param_count\":1,\"param_names\":[\"x\"],"
|
|
"\"param_types\":[false]}");
|
|
cbm_edge_t evidence = {.project = "projection",
|
|
.source_id = a,
|
|
.target_id = b,
|
|
.type = "CALLS",
|
|
.properties_json = properties[i]};
|
|
ASSERT_TRUE(cbm_store_insert_edge(store, &evidence) > 0);
|
|
cbm_architecture_projection_options_t opts = {
|
|
.entry_node_id = a, .target_node_id = b, .include_behavior_evidence = true};
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json),
|
|
CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
yyjson_val *edge =
|
|
yyjson_arr_get(yyjson_obj_get(projection_field(doc, "behavior"), "edges"), 0);
|
|
if (i == 4) {
|
|
ASSERT_TRUE(yyjson_is_arr(yyjson_obj_get(edge, "arguments")));
|
|
ASSERT_EQ(yyjson_arr_size(yyjson_obj_get(edge, "arguments")), 0);
|
|
ASSERT_FALSE(yyjson_get_bool(yyjson_obj_get(edge, "arguments_complete")));
|
|
} else
|
|
ASSERT_NULL(yyjson_obj_get(edge, "arguments"));
|
|
ASSERT_NULL(strstr(json, "\"signature\""));
|
|
ASSERT_NULL(strstr(json, "\"parameters\""));
|
|
ASSERT_NOT_NULL(strstr(json, "\"return_type\":\"Known\""));
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
}
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
int64_t a = projection_node(store, "Function", "projection.a", "a.c", true);
|
|
int64_t b = projection_node(store, "Function", "projection.b", "b.c", false);
|
|
cbm_edge_t evidence = {.project = "projection",
|
|
.source_id = a,
|
|
.target_id = b,
|
|
.type = "HTTP_CALLS",
|
|
.properties_json =
|
|
"{\"args\":[{\"i\":0,\"e\":\"not_a_CALLS_property\"}]}"};
|
|
ASSERT_TRUE(cbm_store_insert_edge(store, &evidence) > 0);
|
|
cbm_architecture_projection_options_t opts = {
|
|
.entry_node_id = a, .target_node_id = b, .include_behavior_evidence = true};
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
ASSERT_NULL(strstr(json, "\"arguments\""));
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_oversized_evidence_is_omitted_without_truncating_values) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
int64_t a = projection_node(store, "Function", "projection.a", "a.c", true);
|
|
char signature[4098], expression[1026], node_properties[4300], edge_properties[2300];
|
|
memset(signature, 's', sizeof(signature) - 1);
|
|
signature[sizeof(signature) - 1] = '\0';
|
|
memset(expression, 'e', sizeof(expression) - 1);
|
|
expression[sizeof(expression) - 1] = '\0';
|
|
snprintf(node_properties, sizeof(node_properties),
|
|
"{\"signature\":\"%s\",\"return_type\":\"Preserved\"}", signature);
|
|
int64_t b = projection_node_properties(store, "projection.b", "b.c", node_properties);
|
|
snprintf(edge_properties, sizeof(edge_properties),
|
|
"{\"args\":[{\"i\":0,\"e\":\"%s\"},{\"i\":1,\"e\":\"kept\",\"v\":\"%s\"}]}",
|
|
expression, expression);
|
|
cbm_edge_t evidence = {.project = "projection",
|
|
.source_id = a,
|
|
.target_id = b,
|
|
.type = "CALLS",
|
|
.properties_json = edge_properties};
|
|
ASSERT_TRUE(cbm_store_insert_edge(store, &evidence) > 0);
|
|
cbm_architecture_projection_options_t opts = {
|
|
.entry_node_id = a, .target_node_id = b, .include_behavior_evidence = true};
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
ASSERT_NULL(strstr(json, "\"signature\""));
|
|
ASSERT_NOT_NULL(strstr(json, "\"return_type\":\"Preserved\""));
|
|
yyjson_val *edge =
|
|
yyjson_arr_get(yyjson_obj_get(projection_field(doc, "behavior"), "edges"), 0);
|
|
yyjson_val *args = yyjson_obj_get(edge, "arguments");
|
|
ASSERT_EQ(yyjson_arr_size(args), 1);
|
|
yyjson_val *arg = yyjson_arr_get(args, 0);
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(arg, "i")), 1);
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(arg, "e")), "kept");
|
|
ASSERT_NULL(yyjson_obj_get(arg, "v"));
|
|
ASSERT_FALSE(yyjson_get_bool(yyjson_obj_get(edge, "arguments_complete")));
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_repeated_behavior_evidence_preserves_base_graph_under_budget) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
enum { components = 80 };
|
|
int64_t symbols[components];
|
|
char signature[4097], expression[1025], properties[4400], arguments[1200];
|
|
memset(signature, 's', sizeof(signature) - 1);
|
|
signature[sizeof(signature) - 1] = '\0';
|
|
memset(expression, 'e', sizeof(expression) - 1);
|
|
expression[sizeof(expression) - 1] = '\0';
|
|
snprintf(properties, sizeof(properties),
|
|
"{\"signature\":\"%s\",\"return_type\":\"Result\",\"is_entry_point\":true}",
|
|
signature);
|
|
snprintf(arguments, sizeof(arguments), "{\"line\":42,\"args\":[{\"i\":0,\"e\":\"%s\"}]}",
|
|
expression);
|
|
for (int i = 0; i < components; i++) {
|
|
char package[80], name[100], file[100];
|
|
snprintf(package, sizeof(package), "projection.package%02d", i);
|
|
snprintf(name, sizeof(name), "%s.function", package);
|
|
snprintf(file, sizeof(file), "src/package%02d/function.c", i);
|
|
ASSERT_TRUE(projection_node(store, "Package", package, "", false) > 0);
|
|
symbols[i] = projection_node_properties(store, name, file, properties);
|
|
ASSERT_TRUE(symbols[i] > 0);
|
|
}
|
|
/* Thousands of witnesses repeat a small indexed declaration set. Its
|
|
uncapped mutable JSON plus serialized copy exceeded the shared budget. */
|
|
for (int from = 0; from < components; from++)
|
|
for (int to = 0; to < components; to++) {
|
|
if (from == to)
|
|
continue;
|
|
cbm_edge_t edge = {.project = "projection",
|
|
.source_id = symbols[from],
|
|
.target_id = symbols[to],
|
|
.type = "CALLS",
|
|
.properties_json = arguments};
|
|
ASSERT_TRUE(cbm_store_insert_edge(store, &edge) > 0);
|
|
}
|
|
cbm_architecture_projection_options_t opts = {
|
|
.entry_node_id = symbols[0], .target_node_id = symbols[1], .max_depth = 1, .max_paths = 1};
|
|
char *baseline_json = NULL, *enriched_json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &baseline_json),
|
|
CBM_STORE_OK);
|
|
yyjson_doc *baseline = yyjson_read(baseline_json, strlen(baseline_json), 0);
|
|
ASSERT_NOT_NULL(baseline);
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(baseline, "components")), components);
|
|
opts.include_behavior_evidence = true;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &opts, &enriched_json),
|
|
CBM_STORE_OK);
|
|
yyjson_doc *enriched = yyjson_read(enriched_json, strlen(enriched_json), 0);
|
|
ASSERT_NOT_NULL(enriched);
|
|
const char *collections[] = {"components", "dependencies", "entrypoints", "paths"};
|
|
for (size_t i = 0; i < sizeof(collections) / sizeof(collections[0]); i++)
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(enriched, collections[i])),
|
|
yyjson_arr_size(projection_field(baseline, collections[i])));
|
|
ASSERT_EQ(
|
|
yyjson_get_sint(yyjson_obj_get(projection_field(enriched, "totals"), "accounted_nodes")),
|
|
yyjson_get_sint(yyjson_obj_get(projection_field(baseline, "totals"), "accounted_nodes")));
|
|
ASSERT_NULL(strstr(enriched_json, "The architecture response exceeded its memory budget"));
|
|
yyjson_val *path = yyjson_arr_get(projection_field(enriched, "paths"), 0);
|
|
yyjson_val *source = yyjson_arr_get(yyjson_obj_get(path, "nodes"), 0);
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(source, "signature")), signature);
|
|
yyjson_val *edge = yyjson_arr_get(yyjson_obj_get(path, "edges"), 0);
|
|
yyjson_val *argument = yyjson_arr_get(yyjson_obj_get(edge, "arguments"), 0);
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(argument, "e")), expression);
|
|
ASSERT_NOT_NULL(strstr(enriched_json, "Optional behavior evidence was omitted"));
|
|
yyjson_doc_free(baseline);
|
|
yyjson_doc_free(enriched);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, baseline_json);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, enriched_json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
/* Shaped like Django 5.2.7 (12,700 mostly single-symbol components, far below
|
|
* the node and edge budgets): one full overview entry per component used to
|
|
* exceed the 32 MB response budget, so the whole projection came back
|
|
* limited and empty. The overview now lists a bounded, ranked subset and
|
|
* still accounts for every component through its groups and totals. A group
|
|
* whose components all rank last, like Django's @no-source, still gets its
|
|
* own share of entries so that it can be opened. */
|
|
TEST(projection_many_small_components_stay_within_response_budget) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
enum { bulk = 14000, late = 20, parts = bulk + late, entries = 4096, share = 16 };
|
|
char padding[161];
|
|
memset(padding, 'x', sizeof(padding) - 1);
|
|
padding[sizeof(padding) - 1] = '\0';
|
|
ASSERT_EQ(cbm_store_begin(store), CBM_STORE_OK);
|
|
for (int i = 0; i < bulk; i++) {
|
|
char qn[256], file[256];
|
|
snprintf(qn, sizeof(qn), "projection.area%d.%s%05d.Part", i % 8, padding, i);
|
|
snprintf(file, sizeof(file), "src/area%d/%.*s%05d.py", i % 8, 120, padding, i);
|
|
ASSERT_TRUE(projection_node(store, "Class", qn, file, false) > 0);
|
|
}
|
|
/* Equal rank otherwise, so the later ids of this group rank last. */
|
|
for (int i = 0; i < late; i++) {
|
|
char qn[64], file[64];
|
|
snprintf(qn, sizeof(qn), "projection.late.Part%02d", i);
|
|
snprintf(file, sizeof(file), "late/part%02d.py", i);
|
|
ASSERT_TRUE(projection_node(store, "Class", qn, file, false) > 0);
|
|
}
|
|
ASSERT_EQ(cbm_store_commit(store), CBM_STORE_OK);
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", NULL, &json), CBM_STORE_OK);
|
|
ASSERT_NULL(strstr(json, "exceeded its memory budget"));
|
|
/* architecture_jobs.c rejects results above 8 MiB. */
|
|
ASSERT_TRUE(strlen(json) < 8 * 1024 * 1024);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
ASSERT_STR_EQ(yyjson_get_str(projection_field(doc, "status")), "ready");
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(projection_field(doc, "totals"), "components")), parts);
|
|
yyjson_val *displayed = projection_field(doc, "components");
|
|
ASSERT_EQ(yyjson_arr_size(displayed), 256);
|
|
yyjson_val *overview = projection_field(doc, "overview"),
|
|
*listed = yyjson_obj_get(overview, "components"),
|
|
*groups = yyjson_obj_get(overview, "groups");
|
|
size_t listed_count = yyjson_arr_size(listed);
|
|
ASSERT_EQ(listed_count, entries);
|
|
ASSERT_FALSE(yyjson_get_bool(yyjson_obj_get(overview, "complete")));
|
|
ASSERT_EQ(yyjson_get_int(
|
|
yyjson_obj_get(yyjson_obj_get(overview, "limits"), "omitted_component_entries")),
|
|
parts - entries);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(yyjson_obj_get(overview, "totals"), "components")),
|
|
parts);
|
|
/* Every displayed component keeps its overview entry. */
|
|
for (size_t i = 0; i < yyjson_arr_size(displayed); i++) {
|
|
const char *id = yyjson_get_str(yyjson_obj_get(yyjson_arr_get(displayed, i), "id"));
|
|
bool found = false;
|
|
for (size_t j = 0; j < listed_count && !found; j++)
|
|
found = !strcmp(id, yyjson_get_str(yyjson_obj_get(yyjson_arr_get(listed, j), "id")));
|
|
ASSERT_TRUE(found);
|
|
}
|
|
size_t membership = 0;
|
|
int members = 0;
|
|
for (size_t i = 0; i < yyjson_arr_size(groups); i++) {
|
|
yyjson_val *group = yyjson_arr_get(groups, i);
|
|
yyjson_val *ids = yyjson_obj_get(group, "component_ids");
|
|
membership += yyjson_arr_size(ids);
|
|
members += yyjson_get_int(yyjson_obj_get(group, "member_count"));
|
|
/* Equal ranks fall back to component order, so each group must list
|
|
* a prefix of its ids, and at least its share of them. */
|
|
const char *group_id = yyjson_get_str(yyjson_obj_get(group, "id"));
|
|
size_t count = yyjson_arr_size(ids), shown = 0, at, maximum;
|
|
yyjson_val *entry;
|
|
yyjson_arr_foreach(listed, at, maximum, entry) {
|
|
if (strcmp(group_id, yyjson_get_str(yyjson_obj_get(entry, "group_id"))))
|
|
continue;
|
|
ASSERT_TRUE(shown < count);
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(entry, "id")),
|
|
yyjson_get_str(yyjson_arr_get(ids, shown)));
|
|
shown++;
|
|
}
|
|
ASSERT_TRUE(shown >= (count < share ? count : (size_t)share));
|
|
if (!strcmp(yyjson_get_str(yyjson_obj_get(group, "label")), "late"))
|
|
ASSERT_EQ(shown, share);
|
|
}
|
|
ASSERT_EQ(membership, parts);
|
|
ASSERT_EQ(members, parts);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
static yyjson_val *projection_component_of(yyjson_doc *doc, const char *qn) {
|
|
yyjson_val *parts = projection_field(doc, "components");
|
|
for (size_t i = 0; i < yyjson_arr_size(parts); i++) {
|
|
yyjson_val *part = yyjson_arr_get(parts, i);
|
|
yyjson_val *rep = yyjson_arr_get(yyjson_obj_get(part, "representatives"), 0);
|
|
if (!strcmp(yyjson_get_str(yyjson_obj_get(rep, "qualified_name")), qn))
|
|
return part;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
TEST(projection_python_test_files_follow_name_conventions) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
projection_node(store, "Class", "projection.app.tests.ViewTests", "app/tests.py", false);
|
|
projection_node(store, "Function", "projection.app.models_test.check", "app/models_test.py",
|
|
false);
|
|
projection_node(store, "Function", "projection.app.conftest.fixture", "app/conftest.py", false);
|
|
projection_node(store, "Function", "projection.check", "test/check.c", false);
|
|
/* A top-level test/ is a test suite, also for helpers without a test name. */
|
|
projection_node(store, "Function", "projection.test.helpers.build", "test/helpers.py", false);
|
|
projection_node(store, "Function", "projection.app.views.index", "app/views.py", false);
|
|
/* django/test/ ships Django's testing API. The extractor flags its
|
|
* functions from the directory alone; the class carries no flag. */
|
|
int64_t client = projection_node(store, "Class", "projection.django.test.client.Client",
|
|
"django/test/client.py", false);
|
|
cbm_node_t method = {.project = "projection",
|
|
.label = "Method",
|
|
.name = "get",
|
|
.qualified_name = "projection.django.test.client.Client.get",
|
|
.file_path = "django/test/client.py",
|
|
.properties_json = "{\"is_test\":true}"};
|
|
int64_t get = cbm_store_upsert_node(store, &method);
|
|
projection_edge(store, client, get, "DEFINES_METHOD");
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", NULL, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
const char *tests[] = {"projection.app.tests.ViewTests", "projection.app.models_test.check",
|
|
"projection.app.conftest.fixture", "projection.check",
|
|
"projection.test.helpers.build"};
|
|
for (size_t i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
|
|
yyjson_val *part = projection_component_of(doc, tests[i]);
|
|
ASSERT_NOT_NULL(part);
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(part, "role")), "test");
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(part, "role_basis")), "test_path_convention");
|
|
}
|
|
yyjson_val *views = projection_component_of(doc, "projection.app.views.index");
|
|
ASSERT_NOT_NULL(views);
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(views, "role")), "non_test");
|
|
/* One role per file: the class and its flagged method stay one part. */
|
|
yyjson_val *api = projection_component_of(doc, "projection.django.test.client.Client");
|
|
ASSERT_NOT_NULL(api);
|
|
ASSERT_STR_EQ(yyjson_get_str(yyjson_obj_get(api, "role")), "non_test");
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(api, "member_count")), 2);
|
|
ASSERT_EQ(yyjson_arr_size(projection_field(doc, "components")), 7);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
/* Django views are never named main; their HANDLES edge is the entry evidence.
|
|
* Test-client calls like self.client.get("/probe/") are indexed as route
|
|
* registrations, so a route that only test code registers proves nothing. */
|
|
TEST(projection_route_handlers_are_entry_points) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
int64_t route = projection_node(store, "Route", "projection.__route__GET__/orders", "", false);
|
|
int64_t view =
|
|
projection_node(store, "Function", "projection.shop.views.orders", "shop/views.py", false);
|
|
int64_t method =
|
|
projection_node(store, "Method", "projection.shop.api.Orders.get", "shop/api.py", false);
|
|
int64_t fixture =
|
|
projection_node(store, "Function", "projection.tests.urls.orders", "tests/urls.py", false);
|
|
int64_t query =
|
|
projection_node(store, "Function", "projection.shop.query.orders", "shop/query.py", false);
|
|
projection_node(store, "Function", "projection.shop.views.unrouted", "shop/views.py", false);
|
|
int64_t cart_route =
|
|
projection_node(store, "Route", "projection.__route__GET__/cart", "", false);
|
|
int64_t urls =
|
|
projection_node(store, "Function", "projection.shop.urls.register", "shop/urls.py", false);
|
|
int64_t cart =
|
|
projection_node(store, "Function", "projection.shop.views.cart", "shop/views.py", false);
|
|
int64_t probe_route =
|
|
projection_node(store, "Route", "projection.__route__GET__/probe", "", false);
|
|
int64_t probe = projection_node(store, "Method", "projection.tests.test_views.Tests.test_probe",
|
|
"tests/test_views.py", false);
|
|
int64_t text = projection_node(store, "Method", "projection.shop.models.Order.text",
|
|
"shop/models.py", false);
|
|
/* Created last, so only the ranking can put the indexed entry first. */
|
|
int64_t launcher =
|
|
projection_node(store, "Function", "projection.manage.main", "manage.py", true);
|
|
int64_t setup =
|
|
projection_node(store, "Function", "projection.manage.setup", "manage.py", false);
|
|
projection_edge(store, view, route, "HANDLES");
|
|
projection_edge(store, method, route, "HANDLES");
|
|
projection_edge(store, fixture, route, "HANDLES");
|
|
projection_edge(store, view, query, "CALLS");
|
|
projection_edge(store, urls, cart_route, "CALLS");
|
|
projection_edge(store, cart, cart_route, "HANDLES");
|
|
projection_edge(store, probe, probe_route, "CALLS");
|
|
projection_edge(store, text, probe_route, "HANDLES");
|
|
projection_edge(store, launcher, setup, "CALLS");
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", NULL, &json), CBM_STORE_OK);
|
|
yyjson_doc *doc = yyjson_read(json, strlen(json), 0);
|
|
ASSERT_NOT_NULL(doc);
|
|
yyjson_val *entries = projection_field(doc, "entrypoints");
|
|
const int64_t expected[] = {launcher, view, method, cart};
|
|
ASSERT_EQ(yyjson_arr_size(entries), sizeof(expected) / sizeof(expected[0]));
|
|
for (size_t i = 0; i < sizeof(expected) / sizeof(expected[0]); i++)
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(yyjson_arr_get(entries, i), "id")), expected[i]);
|
|
ASSERT_EQ(yyjson_get_int(yyjson_obj_get(projection_field(doc, "totals"), "entrypoints")), 4);
|
|
yyjson_val *paths = projection_field(doc, "paths");
|
|
ASSERT_EQ(yyjson_arr_size(paths), 2);
|
|
yyjson_val *first = yyjson_arr_get(paths, 0), *second = yyjson_arr_get(paths, 1);
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(first, "entrypoint_id")), launcher);
|
|
ASSERT_EQ(yyjson_get_sint(yyjson_obj_get(second, "entrypoint_id")), view);
|
|
ASSERT_EQ(
|
|
yyjson_get_sint(yyjson_obj_get(yyjson_arr_get(yyjson_obj_get(second, "nodes"), 1), "id")),
|
|
query);
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_response_memory_returns_to_caller_baseline) {
|
|
cbm_store_t *store = projection_store();
|
|
ASSERT_NOT_NULL(store);
|
|
projection_node(store, "File", "projection.a.c", "a.c", false);
|
|
int64_t entry = projection_node(store, "Function", "projection.start", "a.c", true);
|
|
int64_t target = projection_node(store, "Function", "projection.finish", "b.c", false);
|
|
projection_edge(store, entry, target, "CALLS");
|
|
cbm_architecture_projection_options_t options = {.entry_node_id = entry,
|
|
.include_behavior_evidence = true};
|
|
size_t blocks = cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER);
|
|
size_t bytes = cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER);
|
|
for (int limited = 0; limited < 2; limited++) {
|
|
options.max_nodes = limited ? 1 : 0;
|
|
char *json = NULL;
|
|
ASSERT_EQ(cbm_store_architecture_projection(store, "projection", &options, &json),
|
|
CBM_STORE_OK);
|
|
ASSERT_NOT_NULL(json);
|
|
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks + 1);
|
|
ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER), bytes + cbm_mem_usable_size(json));
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks);
|
|
ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER), bytes);
|
|
}
|
|
cbm_store_close(store);
|
|
PASS();
|
|
}
|
|
|
|
TEST(projection_completed_job_memory_returns_to_caller_baseline) {
|
|
char temp[256] = "/tmp/cbm_projection_memory_XXXXXX";
|
|
ASSERT_NOT_NULL(cbm_mkdtemp(temp));
|
|
char path[1024];
|
|
snprintf(path, sizeof(path), "%s/index.db", temp);
|
|
cbm_store_t *store = cbm_store_open_path(path);
|
|
ASSERT_NOT_NULL(store);
|
|
ASSERT_EQ(cbm_store_upsert_project(store, "projection", temp), CBM_STORE_OK);
|
|
projection_node(store, "Function", "projection.entry", "entry.c", true);
|
|
|
|
/* A live caller allocation makes a lost worker delta observable even when
|
|
* the shared counters would otherwise saturate at zero on cache release. */
|
|
void *sentinel = cbm_alloc(CBM_MEM_CLASS_OTHER, 64);
|
|
ASSERT_NOT_NULL(sentinel);
|
|
size_t before_blocks = cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER);
|
|
size_t before_bytes = cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER);
|
|
cbm_architecture_jobs_t *jobs = cbm_architecture_jobs_new();
|
|
ASSERT_NOT_NULL(jobs);
|
|
bool ready = false, cached = false;
|
|
uint64_t deadline = cbm_now_ms() + 10000;
|
|
do {
|
|
char *json = cbm_architecture_jobs_request(jobs, store, "projection", 0);
|
|
yyjson_doc *doc = json ? yyjson_read(json, strlen(json), 0) : NULL;
|
|
const char *status = doc ? yyjson_get_str(projection_field(doc, "status")) : NULL;
|
|
ready = status && strcmp(status, "ready") == 0;
|
|
bool pending = status && strcmp(status, "pending") == 0;
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
if (!pending)
|
|
break;
|
|
cbm_usleep(1000);
|
|
} while (cbm_now_ms() < deadline);
|
|
if (ready) {
|
|
char *json = cbm_architecture_jobs_request(jobs, store, "projection", 0);
|
|
yyjson_doc *doc = json ? yyjson_read(json, strlen(json), 0) : NULL;
|
|
const char *status = doc ? yyjson_get_str(projection_field(doc, "status")) : NULL;
|
|
cached = status && strcmp(status, "ready") == 0;
|
|
yyjson_doc_free(doc);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, json);
|
|
}
|
|
cbm_architecture_jobs_free(jobs); /* joins before the caller frees retained output */
|
|
size_t after_blocks = cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER);
|
|
size_t after_bytes = cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, sentinel);
|
|
cbm_store_close(store);
|
|
th_rmtree(temp);
|
|
|
|
ASSERT_TRUE(ready);
|
|
ASSERT_TRUE(cached);
|
|
ASSERT_EQ(after_blocks, before_blocks);
|
|
ASSERT_EQ(after_bytes, before_bytes);
|
|
PASS();
|
|
}
|
|
|
|
SUITE(architecture_projection) {
|
|
RUN_TEST(projection_completed_job_memory_returns_to_caller_baseline);
|
|
RUN_TEST(projection_response_memory_returns_to_caller_baseline);
|
|
RUN_TEST(projection_accounts_for_isolated_nodes_and_files);
|
|
RUN_TEST(projection_preserves_typed_edges_and_contiguous_paths);
|
|
RUN_TEST(projection_reports_dependency_cycles_without_fabricated_execution);
|
|
RUN_TEST(projection_graph_limits_abstain_instead_of_sampling);
|
|
RUN_TEST(projection_cancellation_deadline_and_store_reuse);
|
|
RUN_TEST(projection_display_caps_preserve_total_accounting);
|
|
RUN_TEST(projection_unknown_project_and_empty_project);
|
|
RUN_TEST(projection_paths_depth_cap_is_explicit);
|
|
RUN_TEST(projection_channel_relationships_are_typed_not_fake_calls);
|
|
RUN_TEST(projection_test_callers_cannot_absorb_source_components);
|
|
RUN_TEST(projection_indexed_embedded_test_has_separate_membership);
|
|
RUN_TEST(projection_shallow_leaves_do_not_hide_deeper_behavior);
|
|
RUN_TEST(projection_behavior_preserves_first_hop_diversity);
|
|
RUN_TEST(projection_corridor_preserves_joins_cycles_and_callsite_evidence);
|
|
RUN_TEST(projection_corridor_caps_abstain_without_hiding_joins);
|
|
RUN_TEST(projection_overview_accounts_for_components_beyond_legacy_cap);
|
|
RUN_TEST(projection_aggregate_cycle_does_not_invent_symbol_cycle);
|
|
RUN_TEST(projection_provenance_budget_counts_utf8_bytes);
|
|
RUN_TEST(projection_example_budgets_preserve_full_aggregate_counts);
|
|
RUN_TEST(projection_behavior_evidence_is_opt_in_and_shared_across_surfaces);
|
|
RUN_TEST(projection_argument_validation_keeps_exact_edge_and_callsite_identity);
|
|
RUN_TEST(projection_missing_corrupt_and_noncall_arguments_remain_unknown);
|
|
RUN_TEST(projection_oversized_evidence_is_omitted_without_truncating_values);
|
|
RUN_TEST(projection_repeated_behavior_evidence_preserves_base_graph_under_budget);
|
|
RUN_TEST(projection_many_small_components_stay_within_response_budget);
|
|
RUN_TEST(projection_python_test_files_follow_name_conventions);
|
|
RUN_TEST(projection_route_handlers_are_entry_points);
|
|
}
|