mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Add 480+ tests, fix 3 bugs discovered by new test coverage
New tests (6481 lines across 23 files): - Arena: reset invariants, growth, sized init, destroy safety - Dynamic array: capacity, remove, clear, grow patterns - FQN: module/package/class name computation (592 lines) - Graph buffer: node/edge CRUD, dedup, merge, concurrent access - Hash table: collision, resize, iteration, edge cases - MCP: tool dispatch, argument parsing, error handling - Memory: allocation tracking, collection, limits - Pipeline: test detection (path + func name patterns) - Store: node/edge CRUD, search, batch ops, persistence - String: intern dedup, util edge cases, YAML parsing (1105 lines) - Traces: ingestion, dedup, query - Watcher: baseline, polling, change detection - Worker pool: task distribution, completion Bugs found and fixed: 1. Arena reset overflow: block_size not reset to block_sizes[0] after growth — subsequent allocations could overflow blocks[0] 2. Test path detection: missing __tests__/, tests/, spec/ directory patterns and _spec.rb suffix 3. Test func detection: "Testable" falsely matched as Go test — now requires uppercase or end-of-string after "Test" prefix
This commit is contained in:
1 parent
cbac7be1e0
commit
7f579f91ad
23 files changed
+6481
-5
No files matched your search
+4
-2
@@ -268,7 +268,7 @@ TEST_DISCOVER_SRCS = \
|
||||
|
||||
TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c
|
||||
|
||||
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c
|
||||
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_fqn.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c
|
||||
|
||||
TEST_WATCHER_SRCS = tests/test_watcher.c
|
||||
|
||||
@@ -294,7 +294,9 @@ TEST_UI_SRCS = tests/test_ui.c
|
||||
|
||||
TEST_SECURITY_SRCS = tests/test_security.c
|
||||
|
||||
ALL_TEST_SRCS = $(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_HTTPLINK_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_SECURITY_SRCS) $(TEST_INTEGRATION_SRCS)
|
||||
TEST_YAML_SRCS = tests/test_yaml.c
|
||||
|
||||
ALL_TEST_SRCS = $(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_HTTPLINK_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_INTEGRATION_SRCS)
|
||||
|
||||
# ── Build directories ────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -136,6 +136,11 @@ void cbm_arena_reset(CBMArena *a) {
|
||||
}
|
||||
a->used = 0;
|
||||
a->total_alloc = 0;
|
||||
/* Reset block_size to match surviving block — prevents overflow if
|
||||
* block_size grew during previous allocations (e.g., 128 → 256). */
|
||||
if (a->nblocks == 1) {
|
||||
a->block_size = a->block_sizes[0];
|
||||
}
|
||||
}
|
||||
|
||||
void cbm_arena_destroy(CBMArena *a) {
|
||||
|
||||
@@ -76,6 +76,22 @@ bool cbm_is_test_path(const char *path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Directory-based: __tests__/, tests/, test/, spec/ */
|
||||
if (strstr(path, "__tests__/") || strstr(path, "/tests/") || strstr(path, "/test/") ||
|
||||
strstr(path, "/spec/")) {
|
||||
return true;
|
||||
}
|
||||
/* Also match if path STARTS with these directories */
|
||||
if (strncmp(path, "tests/", 6) == 0 || strncmp(path, "test/", 5) == 0 ||
|
||||
strncmp(path, "spec/", 5) == 0 || strncmp(path, "__tests__/", 10) == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Ruby: _spec.rb suffix */
|
||||
if (str_ends_with(path, len, "_spec.rb")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -84,9 +100,17 @@ bool cbm_is_test_func_name(const char *name) {
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
/* Go: Test*, Benchmark*, Example* */
|
||||
if (strncmp(name, "Test", 4) == 0 || strncmp(name, "Benchmark", 9) == 0 ||
|
||||
strncmp(name, "Example", 7) == 0) {
|
||||
/* Go: Test/Benchmark/Example + uppercase letter or end-of-string.
|
||||
* "TestFoo" = test, "Testable" = not test (lowercase after prefix). */
|
||||
if (strncmp(name, "Test", 4) == 0 && (name[4] == '\0' || (name[4] >= 'A' && name[4] <= 'Z'))) {
|
||||
return true;
|
||||
}
|
||||
if (strncmp(name, "Benchmark", 9) == 0 &&
|
||||
(name[9] == '\0' || (name[9] >= 'A' && name[9] <= 'Z'))) {
|
||||
return true;
|
||||
}
|
||||
if (strncmp(name, "Example", 7) == 0 &&
|
||||
(name[7] == '\0' || (name[7] >= 'A' && name[7] <= 'Z'))) {
|
||||
return true;
|
||||
}
|
||||
/* Python/Rust/C++/Lua/Java: test_ or test prefix (lowercase) */
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* embedded_assets.c — Generated file mapping URL paths to embedded bytes.
|
||||
* DO NOT EDIT — regenerated by scripts/embed-frontend.sh
|
||||
*/
|
||||
#include "ui/embedded_assets.h"
|
||||
#include <string.h>
|
||||
|
||||
extern const unsigned char _binary_assets_index_D1_3bntm_css_data[];
|
||||
extern const unsigned int _binary_assets_index_D1_3bntm_css_size;
|
||||
extern const unsigned char _binary_assets_index_urRrcEH6_js_data[];
|
||||
extern const unsigned int _binary_assets_index_urRrcEH6_js_size;
|
||||
extern const unsigned char _binary_index_html_data[];
|
||||
extern const unsigned int _binary_index_html_size;
|
||||
|
||||
cbm_embedded_file_t CBM_EMBEDDED_FILES[] = {
|
||||
{"/assets/index-D1-3bntm.css", _binary_assets_index_D1_3bntm_css_data, 0, "text/css"},
|
||||
{"/assets/index-urRrcEH6.js", _binary_assets_index_urRrcEH6_js_data, 0, "application/javascript"},
|
||||
{"/index.html", _binary_index_html_data, 0, "text/html"},
|
||||
};
|
||||
const int CBM_EMBEDDED_FILE_COUNT = 3;
|
||||
|
||||
static void __attribute__((constructor)) init_embedded_sizes(void) {
|
||||
cbm_embedded_file_t *files = CBM_EMBEDDED_FILES;
|
||||
files[0].size = _binary_assets_index_D1_3bntm_css_size;
|
||||
files[1].size = _binary_assets_index_urRrcEH6_js_size;
|
||||
files[2].size = _binary_index_html_size;
|
||||
}
|
||||
|
||||
const cbm_embedded_file_t *cbm_embedded_lookup(const char *path) {
|
||||
for (int i = 0; i < CBM_EMBEDDED_FILE_COUNT; i++) {
|
||||
if (strcmp(CBM_EMBEDDED_FILES[i].path, path) == 0) {
|
||||
return &CBM_EMBEDDED_FILES[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -377,6 +377,37 @@ TEST(arena_total_through_reset) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(arena_reset_block_size_invariant) {
|
||||
/* BUG DETECTOR: after reset, block_size should match blocks[0]'s actual size.
|
||||
* arena_grow() updates block_size to the new (larger) block's size.
|
||||
* arena_reset() frees the grown blocks but does NOT restore block_size.
|
||||
* Result: arena thinks blocks[0] has more capacity than it actually does.
|
||||
* This is a heap-buffer-overflow waiting to happen. */
|
||||
CBMArena a;
|
||||
cbm_arena_init_sized(&a, 128);
|
||||
size_t original_block_size = a.block_size;
|
||||
ASSERT_EQ(original_block_size, 128);
|
||||
ASSERT_EQ(a.block_sizes[0], 128);
|
||||
|
||||
/* Force growth: block_size will be updated to new (larger) value */
|
||||
cbm_arena_alloc(&a, 100);
|
||||
cbm_arena_alloc(&a, 100); /* triggers grow */
|
||||
ASSERT_GTE(a.nblocks, 2);
|
||||
ASSERT_GT(a.block_size, 128); /* block_size grew */
|
||||
|
||||
/* Reset: frees grown blocks, keeps blocks[0] (which is 128 bytes) */
|
||||
cbm_arena_reset(&a);
|
||||
ASSERT_EQ(a.nblocks, 1);
|
||||
|
||||
/* CRITICAL CHECK: block_size must match blocks[0]'s actual capacity.
|
||||
* If block_size > block_sizes[0], the arena will allow allocations
|
||||
* that overflow blocks[0]. */
|
||||
ASSERT_EQ(a.block_size, a.block_sizes[0]);
|
||||
|
||||
cbm_arena_destroy(&a);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(arena_strndup_zero_len) {
|
||||
/* strndup with len=0 — should return empty string */
|
||||
CBMArena a;
|
||||
@@ -419,5 +450,6 @@ SUITE(arena) {
|
||||
RUN_TEST(arena_calloc_zero);
|
||||
RUN_TEST(arena_many_large_allocs_block_growth);
|
||||
RUN_TEST(arena_total_through_reset);
|
||||
RUN_TEST(arena_reset_block_size_invariant);
|
||||
RUN_TEST(arena_strndup_zero_len);
|
||||
}
|
||||
@@ -129,6 +129,316 @@ TEST(da_insert) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_push_ptr_struct) {
|
||||
typedef struct {
|
||||
int id;
|
||||
double value;
|
||||
const char *tag;
|
||||
} Record;
|
||||
CBM_DYN_ARRAY(Record) arr = {0};
|
||||
|
||||
Record *r = cbm_da_push_ptr(&arr);
|
||||
r->id = 42;
|
||||
r->value = 3.14;
|
||||
r->tag = "first";
|
||||
|
||||
r = cbm_da_push_ptr(&arr);
|
||||
r->id = 99;
|
||||
r->value = 2.72;
|
||||
r->tag = "second";
|
||||
|
||||
ASSERT_EQ(arr.count, 2);
|
||||
ASSERT_EQ(arr.items[0].id, 42);
|
||||
ASSERT_STR_EQ(arr.items[0].tag, "first");
|
||||
ASSERT_EQ(arr.items[1].id, 99);
|
||||
ASSERT_STR_EQ(arr.items[1].tag, "second");
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_insert_at_beginning) {
|
||||
CBM_DYN_ARRAY(int) arr = {0};
|
||||
cbm_da_push(&arr, 20);
|
||||
cbm_da_push(&arr, 30);
|
||||
cbm_da_push(&arr, 40);
|
||||
cbm_da_insert(&arr, 0, 10); /* insert at front */
|
||||
ASSERT_EQ(arr.count, 4);
|
||||
ASSERT_EQ(arr.items[0], 10);
|
||||
ASSERT_EQ(arr.items[1], 20);
|
||||
ASSERT_EQ(arr.items[2], 30);
|
||||
ASSERT_EQ(arr.items[3], 40);
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_insert_at_end) {
|
||||
CBM_DYN_ARRAY(int) arr = {0};
|
||||
cbm_da_push(&arr, 10);
|
||||
cbm_da_push(&arr, 20);
|
||||
cbm_da_insert(&arr, 2, 30); /* insert at count (append) */
|
||||
ASSERT_EQ(arr.count, 3);
|
||||
ASSERT_EQ(arr.items[0], 10);
|
||||
ASSERT_EQ(arr.items[1], 20);
|
||||
ASSERT_EQ(arr.items[2], 30);
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_remove_first) {
|
||||
CBM_DYN_ARRAY(int) arr = {0};
|
||||
cbm_da_push(&arr, 10);
|
||||
cbm_da_push(&arr, 20);
|
||||
cbm_da_push(&arr, 30);
|
||||
cbm_da_remove(&arr, 0); /* remove first */
|
||||
ASSERT_EQ(arr.count, 2);
|
||||
ASSERT_EQ(arr.items[0], 20);
|
||||
ASSERT_EQ(arr.items[1], 30);
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_remove_last) {
|
||||
CBM_DYN_ARRAY(int) arr = {0};
|
||||
cbm_da_push(&arr, 10);
|
||||
cbm_da_push(&arr, 20);
|
||||
cbm_da_push(&arr, 30);
|
||||
cbm_da_remove(&arr, 2); /* remove last element */
|
||||
ASSERT_EQ(arr.count, 2);
|
||||
ASSERT_EQ(arr.items[0], 10);
|
||||
ASSERT_EQ(arr.items[1], 20);
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_remove_sequence) {
|
||||
CBM_DYN_ARRAY(int) arr = {0};
|
||||
for (int i = 0; i < 5; i++)
|
||||
cbm_da_push(&arr, i * 10); /* 0, 10, 20, 30, 40 */
|
||||
|
||||
cbm_da_remove(&arr, 2); /* remove 20 -> 0,10,30,40 */
|
||||
ASSERT_EQ(arr.count, 4);
|
||||
ASSERT_EQ(arr.items[2], 30);
|
||||
|
||||
cbm_da_remove(&arr, 0); /* remove 0 -> 10,30,40 */
|
||||
ASSERT_EQ(arr.count, 3);
|
||||
ASSERT_EQ(arr.items[0], 10);
|
||||
|
||||
cbm_da_remove(&arr, 2); /* remove 40 -> 10,30 */
|
||||
ASSERT_EQ(arr.count, 2);
|
||||
ASSERT_EQ(arr.items[0], 10);
|
||||
ASSERT_EQ(arr.items[1], 30);
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_reserve_then_push) {
|
||||
CBM_DYN_ARRAY(int) arr = {0};
|
||||
cbm_da_reserve(&arr, 50);
|
||||
int cap_after_reserve = arr.cap;
|
||||
ASSERT_GTE(cap_after_reserve, 50);
|
||||
|
||||
/* Push 50 items — capacity should not change (no realloc needed) */
|
||||
for (int i = 0; i < 50; i++)
|
||||
cbm_da_push(&arr, i);
|
||||
ASSERT_EQ(arr.count, 50);
|
||||
ASSERT_EQ(arr.cap, cap_after_reserve);
|
||||
for (int i = 0; i < 50; i++)
|
||||
ASSERT_EQ(arr.items[i], i);
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_reserve_zero) {
|
||||
CBM_DYN_ARRAY(int) arr = {0};
|
||||
cbm_da_reserve(&arr, 0);
|
||||
/* Should be a no-op: cap stays 0, items stays NULL */
|
||||
ASSERT_EQ(arr.cap, 0);
|
||||
ASSERT_NULL(arr.items);
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_reserve_less_than_cap) {
|
||||
CBM_DYN_ARRAY(int) arr = {0};
|
||||
cbm_da_reserve(&arr, 100);
|
||||
int old_cap = arr.cap;
|
||||
/* Reserve smaller than current cap — should be a no-op */
|
||||
cbm_da_reserve(&arr, 10);
|
||||
ASSERT_EQ(arr.cap, old_cap);
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_clear_then_push) {
|
||||
CBM_DYN_ARRAY(int) arr = {0};
|
||||
cbm_da_push(&arr, 1);
|
||||
cbm_da_push(&arr, 2);
|
||||
cbm_da_push(&arr, 3);
|
||||
int old_cap = arr.cap;
|
||||
|
||||
cbm_da_clear(&arr);
|
||||
ASSERT_EQ(arr.count, 0);
|
||||
/* Memory retained after clear */
|
||||
ASSERT_EQ(arr.cap, old_cap);
|
||||
ASSERT_NOT_NULL(arr.items);
|
||||
|
||||
/* Push again — should reuse existing memory */
|
||||
cbm_da_push(&arr, 100);
|
||||
cbm_da_push(&arr, 200);
|
||||
ASSERT_EQ(arr.count, 2);
|
||||
ASSERT_EQ(arr.items[0], 100);
|
||||
ASSERT_EQ(arr.items[1], 200);
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_free_then_push) {
|
||||
CBM_DYN_ARRAY(int) arr = {0};
|
||||
cbm_da_push(&arr, 1);
|
||||
cbm_da_push(&arr, 2);
|
||||
cbm_da_free(&arr);
|
||||
ASSERT_NULL(arr.items);
|
||||
ASSERT_EQ(arr.count, 0);
|
||||
ASSERT_EQ(arr.cap, 0);
|
||||
|
||||
/* Re-use after free — should work like fresh array */
|
||||
cbm_da_push(&arr, 42);
|
||||
ASSERT_EQ(arr.count, 1);
|
||||
ASSERT_EQ(arr.items[0], 42);
|
||||
ASSERT_GT(arr.cap, 0);
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_large_array_10k) {
|
||||
CBM_DYN_ARRAY(int64_t) arr = {0};
|
||||
for (int i = 0; i < 10000; i++)
|
||||
cbm_da_push(&arr, (int64_t)i * 1000);
|
||||
ASSERT_EQ(arr.count, 10000);
|
||||
ASSERT_GTE(arr.cap, 10000);
|
||||
|
||||
/* Verify contiguous memory: all values accessible and correct */
|
||||
for (int i = 0; i < 10000; i++)
|
||||
ASSERT_EQ(arr.items[i], (int64_t)i * 1000);
|
||||
|
||||
/* Verify truly contiguous: pointer arithmetic */
|
||||
int64_t *base = arr.items;
|
||||
for (int i = 0; i < 10000; i++)
|
||||
ASSERT_EQ(&arr.items[i], base + i);
|
||||
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_nested_struct_with_ptrs) {
|
||||
typedef struct {
|
||||
const char *name;
|
||||
int scores[3];
|
||||
const char *category;
|
||||
} Entry;
|
||||
CBM_DYN_ARRAY(Entry) arr = {0};
|
||||
|
||||
cbm_da_push(&arr, ((Entry){.name = "alpha", .scores = {10, 20, 30}, .category = "A"}));
|
||||
cbm_da_push(&arr, ((Entry){.name = "beta", .scores = {40, 50, 60}, .category = "B"}));
|
||||
cbm_da_push(&arr, ((Entry){.name = "gamma", .scores = {70, 80, 90}, .category = "A"}));
|
||||
|
||||
ASSERT_EQ(arr.count, 3);
|
||||
ASSERT_STR_EQ(arr.items[0].name, "alpha");
|
||||
ASSERT_STR_EQ(arr.items[1].name, "beta");
|
||||
ASSERT_STR_EQ(arr.items[2].name, "gamma");
|
||||
ASSERT_EQ(arr.items[0].scores[2], 30);
|
||||
ASSERT_EQ(arr.items[2].scores[0], 70);
|
||||
ASSERT_STR_EQ(arr.items[0].category, "A");
|
||||
ASSERT_STR_EQ(arr.items[1].category, "B");
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_push_pop_lifo) {
|
||||
CBM_DYN_ARRAY(int) arr = {0};
|
||||
/* Push 1..5 */
|
||||
for (int i = 1; i <= 5; i++)
|
||||
cbm_da_push(&arr, i);
|
||||
|
||||
/* Pop should return in LIFO order: 5,4,3,2,1 */
|
||||
ASSERT_EQ(cbm_da_pop(&arr), 5);
|
||||
ASSERT_EQ(cbm_da_pop(&arr), 4);
|
||||
ASSERT_EQ(cbm_da_pop(&arr), 3);
|
||||
ASSERT_EQ(cbm_da_pop(&arr), 2);
|
||||
ASSERT_EQ(cbm_da_pop(&arr), 1);
|
||||
ASSERT_EQ(arr.count, 0);
|
||||
|
||||
/* Push again after full pop */
|
||||
cbm_da_push(&arr, 99);
|
||||
ASSERT_EQ(arr.count, 1);
|
||||
ASSERT_EQ(arr.items[0], 99);
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_mixed_operations) {
|
||||
CBM_DYN_ARRAY(int) arr = {0};
|
||||
|
||||
/* push 10, 20, 30 */
|
||||
cbm_da_push(&arr, 10);
|
||||
cbm_da_push(&arr, 20);
|
||||
cbm_da_push(&arr, 30);
|
||||
ASSERT_EQ(arr.count, 3);
|
||||
|
||||
/* insert 15 at index 1 -> 10,15,20,30 */
|
||||
cbm_da_insert(&arr, 1, 15);
|
||||
ASSERT_EQ(arr.count, 4);
|
||||
ASSERT_EQ(arr.items[1], 15);
|
||||
ASSERT_EQ(arr.items[2], 20);
|
||||
|
||||
/* remove index 0 -> 15,20,30 */
|
||||
cbm_da_remove(&arr, 0);
|
||||
ASSERT_EQ(arr.count, 3);
|
||||
ASSERT_EQ(arr.items[0], 15);
|
||||
|
||||
/* push 40 -> 15,20,30,40 */
|
||||
cbm_da_push(&arr, 40);
|
||||
ASSERT_EQ(arr.count, 4);
|
||||
|
||||
/* pop -> 40, remaining: 15,20,30 */
|
||||
int v = cbm_da_pop(&arr);
|
||||
ASSERT_EQ(v, 40);
|
||||
ASSERT_EQ(arr.count, 3);
|
||||
|
||||
/* last should be 30 */
|
||||
ASSERT_EQ(cbm_da_last(&arr), 30);
|
||||
|
||||
/* insert at end -> 15,20,30,50 */
|
||||
cbm_da_insert(&arr, 3, 50);
|
||||
ASSERT_EQ(arr.count, 4);
|
||||
ASSERT_EQ(arr.items[3], 50);
|
||||
|
||||
/* remove middle -> 15,30,50 */
|
||||
cbm_da_remove(&arr, 1);
|
||||
ASSERT_EQ(arr.count, 3);
|
||||
ASSERT_EQ(arr.items[0], 15);
|
||||
ASSERT_EQ(arr.items[1], 30);
|
||||
ASSERT_EQ(arr.items[2], 50);
|
||||
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(da_push_ptr_growth) {
|
||||
/* Verify push_ptr triggers growth correctly */
|
||||
CBM_DYN_ARRAY(int) arr = {0};
|
||||
for (int i = 0; i < 100; i++) {
|
||||
int *p = cbm_da_push_ptr(&arr);
|
||||
*p = i * 7;
|
||||
}
|
||||
ASSERT_EQ(arr.count, 100);
|
||||
ASSERT_GTE(arr.cap, 100);
|
||||
for (int i = 0; i < 100; i++)
|
||||
ASSERT_EQ(arr.items[i], i * 7);
|
||||
cbm_da_free(&arr);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(dyn_array) {
|
||||
RUN_TEST(da_push_pop);
|
||||
RUN_TEST(da_last);
|
||||
@@ -140,4 +450,20 @@ SUITE(dyn_array) {
|
||||
RUN_TEST(da_string_ptrs);
|
||||
RUN_TEST(da_remove);
|
||||
RUN_TEST(da_insert);
|
||||
RUN_TEST(da_push_ptr_struct);
|
||||
RUN_TEST(da_insert_at_beginning);
|
||||
RUN_TEST(da_insert_at_end);
|
||||
RUN_TEST(da_remove_first);
|
||||
RUN_TEST(da_remove_last);
|
||||
RUN_TEST(da_remove_sequence);
|
||||
RUN_TEST(da_reserve_then_push);
|
||||
RUN_TEST(da_reserve_zero);
|
||||
RUN_TEST(da_reserve_less_than_cap);
|
||||
RUN_TEST(da_clear_then_push);
|
||||
RUN_TEST(da_free_then_push);
|
||||
RUN_TEST(da_large_array_10k);
|
||||
RUN_TEST(da_nested_struct_with_ptrs);
|
||||
RUN_TEST(da_push_pop_lifo);
|
||||
RUN_TEST(da_mixed_operations);
|
||||
RUN_TEST(da_push_ptr_growth);
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
/*
|
||||
* test_fqn.c -- Tests for FQN (Fully Qualified Name) computation.
|
||||
*
|
||||
* Covers: cbm_pipeline_fqn_compute, cbm_pipeline_fqn_module,
|
||||
* cbm_pipeline_fqn_folder, cbm_project_name_from_path.
|
||||
*/
|
||||
#include "test_framework.h"
|
||||
#include "../src/pipeline/pipeline.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── Helper: assert FQN result and free ────────────────────────── */
|
||||
|
||||
#define ASSERT_FQN(expr, expected) \
|
||||
do { \
|
||||
char *_r = (expr); \
|
||||
ASSERT_NOT_NULL(_r); \
|
||||
ASSERT_STR_EQ(_r, expected); \
|
||||
free(_r); \
|
||||
} while (0)
|
||||
|
||||
/* ================================================================
|
||||
* cbm_pipeline_fqn_compute
|
||||
* ================================================================ */
|
||||
|
||||
/* ── Basic: project + path + name ─────────────────────────────── */
|
||||
|
||||
TEST(fqn_compute_basic_go) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("myproj", "main.go", "main"), "myproj.main.main");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_basic_py) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "app.py", "run"), "proj.app.run");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_basic_ts) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "server.ts", "handler"), "proj.server.handler");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_basic_js) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "util.js", "parse"), "proj.util.parse");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_basic_c) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "core.c", "init"), "proj.core.init");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_basic_rs) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "lib.rs", "new"), "proj.lib.new");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Nested paths ─────────────────────────────────────────────── */
|
||||
|
||||
TEST(fqn_compute_nested_two_levels) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "src/pkg/module.go", "FuncName"),
|
||||
"proj.src.pkg.module.FuncName");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_nested_three_levels) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "a/b/c/file.py", "Class"),
|
||||
"proj.a.b.c.file.Class");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_nested_deep) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "a/b/c/d/e/f/g.ts", "fn"),
|
||||
"proj.a.b.c.d.e.f.g.fn");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Python __init__.py ───────────────────────────────────────── */
|
||||
|
||||
TEST(fqn_compute_init_py_with_name) {
|
||||
/* __init__ stripped when name is provided */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "pkg/__init__.py", "MyClass"),
|
||||
"proj.pkg.MyClass");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_init_py_without_name) {
|
||||
/* __init__ kept when no name (module QN for the file itself) */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "pkg/__init__.py", NULL),
|
||||
"proj.pkg.__init__");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_init_py_empty_name) {
|
||||
/* Empty string name also keeps __init__ */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "pkg/__init__.py", ""),
|
||||
"proj.pkg.__init__");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_init_py_nested) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "a/b/__init__.py", "Foo"),
|
||||
"proj.a.b.Foo");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_init_py_root) {
|
||||
/* __init__.py at root with name */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "__init__.py", "X"),
|
||||
"proj.X");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_init_py_root_no_name) {
|
||||
/* __init__.py at root without name -- only project + __init__ (seg_count=2 > 1) */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "__init__.py", NULL),
|
||||
"proj.__init__");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── JS/TS index files ────────────────────────────────────────── */
|
||||
|
||||
TEST(fqn_compute_index_js_with_name) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "pkg/index.js", "render"),
|
||||
"proj.pkg.render");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_index_js_without_name) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "pkg/index.js", NULL),
|
||||
"proj.pkg.index");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_index_ts_with_name) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "src/index.ts", "App"),
|
||||
"proj.src.App");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_index_ts_without_name) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "src/index.ts", NULL),
|
||||
"proj.src.index");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_index_ts_empty_name) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "lib/index.ts", ""),
|
||||
"proj.lib.index");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_index_root_with_name) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "index.js", "main"),
|
||||
"proj.main");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_index_root_no_name) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "index.js", NULL),
|
||||
"proj.index");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Empty / NULL parameters ──────────────────────────────────── */
|
||||
|
||||
TEST(fqn_compute_empty_rel_path) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "", "func"), "proj.func");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_empty_name) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "mod.go", ""), "proj.mod");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_both_empty) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "", ""), "proj");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_null_project) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute(NULL, "foo.go", "bar"), "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_null_rel_path) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", NULL, "fn"), "proj.fn");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_null_name) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "mod.go", NULL), "proj.mod");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_all_null) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute(NULL, NULL, NULL), "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_null_project_null_path) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute(NULL, NULL, "fn"), "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Backslash paths (Windows) ────────────────────────────────── */
|
||||
|
||||
TEST(fqn_compute_backslash_simple) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "src\\main.go", "run"),
|
||||
"proj.src.main.run");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_backslash_nested) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "a\\b\\c\\file.py", "X"),
|
||||
"proj.a.b.c.file.X");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_backslash_mixed) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "a/b\\c/d.ts", "fn"),
|
||||
"proj.a.b.c.d.fn");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Multiple extensions ──────────────────────────────────────── */
|
||||
|
||||
TEST(fqn_compute_double_ext) {
|
||||
/* Only last extension stripped: foo.test.ts -> foo.test */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "foo.test.ts", "bar"),
|
||||
"proj.foo.test.bar");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_spec_ext) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "util.spec.js", "it"),
|
||||
"proj.util.spec.it");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Leading / trailing slashes ───────────────────────────────── */
|
||||
|
||||
TEST(fqn_compute_leading_slash) {
|
||||
/* Leading slash produces empty segment which is skipped */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "/src/main.go", "fn"),
|
||||
"proj.src.main.fn");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_trailing_slash) {
|
||||
/* Trailing slash: path becomes empty after last /, extension strip is no-op */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "src/", "fn"),
|
||||
"proj.src.fn");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_double_slash) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "a//b.go", "fn"),
|
||||
"proj.a.b.fn");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── No extension ─────────────────────────────────────────────── */
|
||||
|
||||
TEST(fqn_compute_no_ext) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "Makefile", "target"),
|
||||
"proj.Makefile.target");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Project-only (no path, no name) ──────────────────────────── */
|
||||
|
||||
TEST(fqn_compute_project_only) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", NULL, NULL), "proj");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Non-init/index filenames that start similarly ────────────── */
|
||||
|
||||
TEST(fqn_compute_init_not_stripped) {
|
||||
/* __init_data__ is NOT __init__, should not be stripped */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "pkg/__init_data__.py", "F"),
|
||||
"proj.pkg.__init_data__.F");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_index2_not_stripped) {
|
||||
/* "indexer" is NOT "index", should not be stripped */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_compute("proj", "pkg/indexer.ts", "F"),
|
||||
"proj.pkg.indexer.F");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* cbm_pipeline_fqn_module
|
||||
* ================================================================ */
|
||||
|
||||
TEST(fqn_module_basic) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_module("proj", "src/app.py"), "proj.src.app");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_module_go) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_module("proj", "cmd/server.go"), "proj.cmd.server");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_module_init_py) {
|
||||
/* fqn_module passes NULL name -> __init__ kept */
|
||||
ASSERT_FQN(cbm_pipeline_fqn_module("proj", "pkg/__init__.py"), "proj.pkg.__init__");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_module_index_js) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_module("proj", "components/index.js"), "proj.components.index");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_module_empty_path) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_module("proj", ""), "proj");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_module_null_path) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_module("proj", NULL), "proj");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_module_null_project) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_module(NULL, "foo.go"), "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_module_deep) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_module("proj", "a/b/c/d/e.rs"), "proj.a.b.c.d.e");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* cbm_pipeline_fqn_folder
|
||||
* ================================================================ */
|
||||
|
||||
TEST(fqn_folder_basic) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_folder("proj", "src"), "proj.src");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_folder_nested) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_folder("proj", "src/pkg/util"), "proj.src.pkg.util");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_folder_empty_dir) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_folder("proj", ""), "proj");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_folder_null_dir) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_folder("proj", NULL), "proj");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_folder_null_project) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_folder(NULL, "src"), "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_folder_backslash) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_folder("proj", "src\\pkg\\util"), "proj.src.pkg.util");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_folder_backslash_mixed) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_folder("proj", "src/pkg\\util"), "proj.src.pkg.util");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_folder_trailing_slash) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_folder("proj", "src/pkg/"), "proj.src.pkg");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_folder_leading_slash) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_folder("proj", "/src/pkg"), "proj.src.pkg");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_folder_double_slash) {
|
||||
ASSERT_FQN(cbm_pipeline_fqn_folder("proj", "a//b"), "proj.a.b");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* cbm_project_name_from_path
|
||||
* ================================================================ */
|
||||
|
||||
TEST(project_name_unix_path) {
|
||||
ASSERT_FQN(cbm_project_name_from_path("/Users/dev/my-project"),
|
||||
"Users-dev-my-project");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_windows_path) {
|
||||
ASSERT_FQN(cbm_project_name_from_path("C:\\Users\\dev\\project"),
|
||||
"C-Users-dev-project");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_with_colons) {
|
||||
/* Colons replaced with dashes (e.g., C: drive) */
|
||||
ASSERT_FQN(cbm_project_name_from_path("C:/dev/proj"),
|
||||
"C-dev-proj");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_multiple_slashes) {
|
||||
/* Consecutive slashes become one dash */
|
||||
ASSERT_FQN(cbm_project_name_from_path("/home///user//code"),
|
||||
"home-user-code");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_leading_trailing_slashes) {
|
||||
/* Leading/trailing dashes trimmed */
|
||||
ASSERT_FQN(cbm_project_name_from_path("/foo/bar/"),
|
||||
"foo-bar");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_empty) {
|
||||
ASSERT_FQN(cbm_project_name_from_path(""), "root");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_null) {
|
||||
ASSERT_FQN(cbm_project_name_from_path(NULL), "root");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_all_slashes) {
|
||||
/* All separators become dashes -> all trimmed -> "root" */
|
||||
ASSERT_FQN(cbm_project_name_from_path("///"), "root");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_single_segment) {
|
||||
ASSERT_FQN(cbm_project_name_from_path("myproject"), "myproject");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_mixed_separators) {
|
||||
/* Mix of forward slash, backslash, colon */
|
||||
ASSERT_FQN(cbm_project_name_from_path("C:\\Users/dev:proj"),
|
||||
"C-Users-dev-proj");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_already_dashed) {
|
||||
/* Dashes are preserved, not collapsed unless from separator conversion */
|
||||
ASSERT_FQN(cbm_project_name_from_path("/my-great-project"),
|
||||
"my-great-project");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_deep_path) {
|
||||
ASSERT_FQN(cbm_project_name_from_path("/a/b/c/d/e/f/g"),
|
||||
"a-b-c-d-e-f-g");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_colon_only) {
|
||||
/* Single colon -> single dash -> trimmed -> root */
|
||||
ASSERT_FQN(cbm_project_name_from_path(":"), "root");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_backslash_only) {
|
||||
ASSERT_FQN(cbm_project_name_from_path("\\"), "root");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_consecutive_colons) {
|
||||
ASSERT_FQN(cbm_project_name_from_path("a::b"), "a-b");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* Suite
|
||||
* ================================================================ */
|
||||
|
||||
SUITE(fqn) {
|
||||
/* fqn_compute: basic extensions */
|
||||
RUN_TEST(fqn_compute_basic_go);
|
||||
RUN_TEST(fqn_compute_basic_py);
|
||||
RUN_TEST(fqn_compute_basic_ts);
|
||||
RUN_TEST(fqn_compute_basic_js);
|
||||
RUN_TEST(fqn_compute_basic_c);
|
||||
RUN_TEST(fqn_compute_basic_rs);
|
||||
|
||||
/* fqn_compute: nested paths */
|
||||
RUN_TEST(fqn_compute_nested_two_levels);
|
||||
RUN_TEST(fqn_compute_nested_three_levels);
|
||||
RUN_TEST(fqn_compute_nested_deep);
|
||||
|
||||
/* fqn_compute: Python __init__.py */
|
||||
RUN_TEST(fqn_compute_init_py_with_name);
|
||||
RUN_TEST(fqn_compute_init_py_without_name);
|
||||
RUN_TEST(fqn_compute_init_py_empty_name);
|
||||
RUN_TEST(fqn_compute_init_py_nested);
|
||||
RUN_TEST(fqn_compute_init_py_root);
|
||||
RUN_TEST(fqn_compute_init_py_root_no_name);
|
||||
|
||||
/* fqn_compute: JS/TS index files */
|
||||
RUN_TEST(fqn_compute_index_js_with_name);
|
||||
RUN_TEST(fqn_compute_index_js_without_name);
|
||||
RUN_TEST(fqn_compute_index_ts_with_name);
|
||||
RUN_TEST(fqn_compute_index_ts_without_name);
|
||||
RUN_TEST(fqn_compute_index_ts_empty_name);
|
||||
RUN_TEST(fqn_compute_index_root_with_name);
|
||||
RUN_TEST(fqn_compute_index_root_no_name);
|
||||
|
||||
/* fqn_compute: empty / NULL parameters */
|
||||
RUN_TEST(fqn_compute_empty_rel_path);
|
||||
RUN_TEST(fqn_compute_empty_name);
|
||||
RUN_TEST(fqn_compute_both_empty);
|
||||
RUN_TEST(fqn_compute_null_project);
|
||||
RUN_TEST(fqn_compute_null_rel_path);
|
||||
RUN_TEST(fqn_compute_null_name);
|
||||
RUN_TEST(fqn_compute_all_null);
|
||||
RUN_TEST(fqn_compute_null_project_null_path);
|
||||
|
||||
/* fqn_compute: backslash (Windows) */
|
||||
RUN_TEST(fqn_compute_backslash_simple);
|
||||
RUN_TEST(fqn_compute_backslash_nested);
|
||||
RUN_TEST(fqn_compute_backslash_mixed);
|
||||
|
||||
/* fqn_compute: multiple extensions */
|
||||
RUN_TEST(fqn_compute_double_ext);
|
||||
RUN_TEST(fqn_compute_spec_ext);
|
||||
|
||||
/* fqn_compute: leading / trailing slashes */
|
||||
RUN_TEST(fqn_compute_leading_slash);
|
||||
RUN_TEST(fqn_compute_trailing_slash);
|
||||
RUN_TEST(fqn_compute_double_slash);
|
||||
|
||||
/* fqn_compute: edge cases */
|
||||
RUN_TEST(fqn_compute_no_ext);
|
||||
RUN_TEST(fqn_compute_project_only);
|
||||
RUN_TEST(fqn_compute_init_not_stripped);
|
||||
RUN_TEST(fqn_compute_index2_not_stripped);
|
||||
|
||||
/* fqn_module */
|
||||
RUN_TEST(fqn_module_basic);
|
||||
RUN_TEST(fqn_module_go);
|
||||
RUN_TEST(fqn_module_init_py);
|
||||
RUN_TEST(fqn_module_index_js);
|
||||
RUN_TEST(fqn_module_empty_path);
|
||||
RUN_TEST(fqn_module_null_path);
|
||||
RUN_TEST(fqn_module_null_project);
|
||||
RUN_TEST(fqn_module_deep);
|
||||
|
||||
/* fqn_folder */
|
||||
RUN_TEST(fqn_folder_basic);
|
||||
RUN_TEST(fqn_folder_nested);
|
||||
RUN_TEST(fqn_folder_empty_dir);
|
||||
RUN_TEST(fqn_folder_null_dir);
|
||||
RUN_TEST(fqn_folder_null_project);
|
||||
RUN_TEST(fqn_folder_backslash);
|
||||
RUN_TEST(fqn_folder_backslash_mixed);
|
||||
RUN_TEST(fqn_folder_trailing_slash);
|
||||
RUN_TEST(fqn_folder_leading_slash);
|
||||
RUN_TEST(fqn_folder_double_slash);
|
||||
|
||||
/* project_name_from_path */
|
||||
RUN_TEST(project_name_unix_path);
|
||||
RUN_TEST(project_name_windows_path);
|
||||
RUN_TEST(project_name_with_colons);
|
||||
RUN_TEST(project_name_multiple_slashes);
|
||||
RUN_TEST(project_name_leading_trailing_slashes);
|
||||
RUN_TEST(project_name_empty);
|
||||
RUN_TEST(project_name_null);
|
||||
RUN_TEST(project_name_all_slashes);
|
||||
RUN_TEST(project_name_single_segment);
|
||||
RUN_TEST(project_name_mixed_separators);
|
||||
RUN_TEST(project_name_already_dashed);
|
||||
RUN_TEST(project_name_deep_path);
|
||||
RUN_TEST(project_name_colon_only);
|
||||
RUN_TEST(project_name_backslash_only);
|
||||
RUN_TEST(project_name_consecutive_colons);
|
||||
}
|
||||
@@ -337,9 +337,600 @@ TEST(gbuf_many_nodes) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Node edge cases ───────────────────────────────────────────── */
|
||||
|
||||
TEST(gbuf_upsert_null_qn) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
/* NULL qualified_name → returns 0 (error), no node inserted */
|
||||
int64_t id = cbm_gbuf_upsert_node(gb, "Function", "foo", NULL, "f.go", 1, 5, "{}");
|
||||
ASSERT_EQ(id, 0);
|
||||
ASSERT_EQ(cbm_gbuf_node_count(gb), 0);
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_upsert_empty_qn) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
/* Empty string QN is valid — it's a non-NULL key */
|
||||
int64_t id = cbm_gbuf_upsert_node(gb, "Function", "anon", "", "f.go", 1, 5, "{}");
|
||||
ASSERT_GT(id, 0);
|
||||
ASSERT_EQ(cbm_gbuf_node_count(gb), 1);
|
||||
|
||||
const cbm_gbuf_node_t *n = cbm_gbuf_find_by_qn(gb, "");
|
||||
ASSERT_NOT_NULL(n);
|
||||
ASSERT_STR_EQ(n->qualified_name, "");
|
||||
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_upsert_same_qn_updates_all_fields) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
int64_t id1 = cbm_gbuf_upsert_node(gb, "Function", "old_name", "pkg.fn", "old.go", 1, 10,
|
||||
"{\"k\":\"v1\"}");
|
||||
int64_t id2 = cbm_gbuf_upsert_node(gb, "Method", "new_name", "pkg.fn", "new.go", 20, 30,
|
||||
"{\"k\":\"v2\"}");
|
||||
ASSERT_EQ(id1, id2);
|
||||
ASSERT_EQ(cbm_gbuf_node_count(gb), 1);
|
||||
|
||||
const cbm_gbuf_node_t *n = cbm_gbuf_find_by_qn(gb, "pkg.fn");
|
||||
ASSERT_NOT_NULL(n);
|
||||
ASSERT_STR_EQ(n->label, "Method");
|
||||
ASSERT_STR_EQ(n->name, "new_name");
|
||||
ASSERT_STR_EQ(n->file_path, "new.go");
|
||||
ASSERT_EQ(n->start_line, 20);
|
||||
ASSERT_EQ(n->end_line, 30);
|
||||
ASSERT_STR_EQ(n->properties_json, "{\"k\":\"v2\"}");
|
||||
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_upsert_long_qn) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
|
||||
/* Build a 1200-char QN */
|
||||
char long_qn[1201];
|
||||
memset(long_qn, 'a', 1200);
|
||||
long_qn[1200] = '\0';
|
||||
|
||||
int64_t id = cbm_gbuf_upsert_node(gb, "Function", "long", long_qn, "f.go", 1, 5, "{}");
|
||||
ASSERT_GT(id, 0);
|
||||
|
||||
const cbm_gbuf_node_t *n = cbm_gbuf_find_by_qn(gb, long_qn);
|
||||
ASSERT_NOT_NULL(n);
|
||||
ASSERT_EQ(strlen(n->qualified_name), 1200);
|
||||
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_find_by_qn_missing) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
cbm_gbuf_upsert_node(gb, "Function", "foo", "pkg.foo", "f.go", 1, 5, "{}");
|
||||
|
||||
ASSERT_NULL(cbm_gbuf_find_by_qn(gb, "does.not.exist"));
|
||||
ASSERT_NULL(cbm_gbuf_find_by_qn(gb, ""));
|
||||
ASSERT_NULL(cbm_gbuf_find_by_qn(gb, "pkg.FOO")); /* case sensitive */
|
||||
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_find_by_id_missing) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
cbm_gbuf_upsert_node(gb, "Function", "foo", "pkg.foo", "f.go", 1, 5, "{}");
|
||||
|
||||
ASSERT_NULL(cbm_gbuf_find_by_id(gb, 0));
|
||||
ASSERT_NULL(cbm_gbuf_find_by_id(gb, -1));
|
||||
ASSERT_NULL(cbm_gbuf_find_by_id(gb, 999));
|
||||
ASSERT_NULL(cbm_gbuf_find_by_id(gb, INT64_MAX));
|
||||
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_find_by_label_no_matches) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
cbm_gbuf_upsert_node(gb, "Function", "foo", "pkg.foo", "f.go", 1, 5, "{}");
|
||||
|
||||
const cbm_gbuf_node_t **nodes = NULL;
|
||||
int count = 0;
|
||||
int rc = cbm_gbuf_find_by_label(gb, "Route", &nodes, &count);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(count, 0);
|
||||
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_find_by_name_multiple) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
cbm_gbuf_upsert_node(gb, "Function", "init", "a.init", "a.go", 1, 5, "{}");
|
||||
cbm_gbuf_upsert_node(gb, "Function", "init", "b.init", "b.go", 1, 5, "{}");
|
||||
cbm_gbuf_upsert_node(gb, "Method", "init", "c.S.init", "c.go", 1, 5, "{}");
|
||||
cbm_gbuf_upsert_node(gb, "Function", "other", "d.other", "d.go", 1, 5, "{}");
|
||||
|
||||
const cbm_gbuf_node_t **nodes = NULL;
|
||||
int count = 0;
|
||||
int rc = cbm_gbuf_find_by_name(gb, "init", &nodes, &count);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(count, 3);
|
||||
|
||||
rc = cbm_gbuf_find_by_name(gb, "other", &nodes, &count);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(count, 1);
|
||||
|
||||
rc = cbm_gbuf_find_by_name(gb, "missing", &nodes, &count);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(count, 0);
|
||||
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_delete_by_label_cascades_edges) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
int64_t f = cbm_gbuf_upsert_node(gb, "Function", "fn", "pkg.fn", "f.go", 1, 5, "{}");
|
||||
int64_t c = cbm_gbuf_upsert_node(gb, "Class", "Cls", "pkg.Cls", "f.go", 10, 20, "{}");
|
||||
int64_t m = cbm_gbuf_upsert_node(gb, "Method", "meth", "pkg.Cls.meth", "f.go", 12, 18, "{}");
|
||||
|
||||
/* Edges: fn→Cls, fn→meth, meth→fn */
|
||||
cbm_gbuf_insert_edge(gb, f, c, "CALLS", "{}");
|
||||
cbm_gbuf_insert_edge(gb, f, m, "CALLS", "{}");
|
||||
cbm_gbuf_insert_edge(gb, m, f, "CALLS", "{}");
|
||||
ASSERT_EQ(cbm_gbuf_edge_count(gb), 3);
|
||||
|
||||
/* Delete all Class nodes — should remove fn→Cls edge only */
|
||||
cbm_gbuf_delete_by_label(gb, "Class");
|
||||
ASSERT_EQ(cbm_gbuf_node_count(gb), 2);
|
||||
ASSERT_EQ(cbm_gbuf_edge_count(gb), 2); /* fn→meth and meth→fn survive */
|
||||
|
||||
/* Verify edge source/target type lookups are consistent after cascade */
|
||||
const cbm_gbuf_edge_t **edges = NULL;
|
||||
int ecount = 0;
|
||||
cbm_gbuf_find_edges_by_source_type(gb, f, "CALLS", &edges, &ecount);
|
||||
ASSERT_EQ(ecount, 1); /* only fn→meth remains */
|
||||
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_node_count_empty) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
ASSERT_EQ(cbm_gbuf_node_count(gb), 0);
|
||||
ASSERT_EQ(cbm_gbuf_node_count(NULL), 0);
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_upsert_100_nodes_stress) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
for (int i = 0; i < 100; i++) {
|
||||
char name[32], qn[64];
|
||||
snprintf(name, sizeof(name), "f%d", i);
|
||||
snprintf(qn, sizeof(qn), "pkg.f%d", i);
|
||||
int64_t id = cbm_gbuf_upsert_node(gb, "Function", name, qn, "f.go", i, i + 1, "{}");
|
||||
ASSERT_GT(id, 0);
|
||||
}
|
||||
ASSERT_EQ(cbm_gbuf_node_count(gb), 100);
|
||||
|
||||
/* Verify each node is findable */
|
||||
for (int i = 0; i < 100; i++) {
|
||||
char qn[64];
|
||||
snprintf(qn, sizeof(qn), "pkg.f%d", i);
|
||||
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(gb, qn));
|
||||
}
|
||||
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge edge cases ──────────────────────────────────────────── */
|
||||
|
||||
TEST(gbuf_edge_nonexistent_endpoints) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
/* Edges with non-existent source/target IDs are accepted (no FK validation
|
||||
* in the buffer — validation happens at flush time when remapping IDs) */
|
||||
int64_t eid = cbm_gbuf_insert_edge(gb, 9999, 8888, "CALLS", "{}");
|
||||
ASSERT_GT(eid, 0);
|
||||
ASSERT_EQ(cbm_gbuf_edge_count(gb), 1);
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_edge_dedup_merges_properties) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
int64_t a = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
|
||||
int64_t b = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
|
||||
|
||||
int64_t eid1 = cbm_gbuf_insert_edge(gb, a, b, "CALLS", "{\"weight\":1}");
|
||||
int64_t eid2 = cbm_gbuf_insert_edge(gb, a, b, "CALLS", "{\"weight\":5}");
|
||||
ASSERT_EQ(eid1, eid2);
|
||||
ASSERT_EQ(cbm_gbuf_edge_count(gb), 1);
|
||||
|
||||
/* Verify second insert's properties win (merge = replace) */
|
||||
const cbm_gbuf_edge_t **edges = NULL;
|
||||
int count = 0;
|
||||
cbm_gbuf_find_edges_by_source_type(gb, a, "CALLS", &edges, &count);
|
||||
ASSERT_EQ(count, 1);
|
||||
ASSERT_STR_EQ(edges[0]->properties_json, "{\"weight\":5}");
|
||||
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_edge_count_empty) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
ASSERT_EQ(cbm_gbuf_edge_count(gb), 0);
|
||||
ASSERT_EQ(cbm_gbuf_edge_count(NULL), 0);
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_edge_count_by_type_missing) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, "CALLS"), 0);
|
||||
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, "UNKNOWN"), 0);
|
||||
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, ""), 0);
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_delete_edges_preserves_other_types) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
int64_t a = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
|
||||
int64_t b = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
|
||||
int64_t c = cbm_gbuf_upsert_node(gb, "Function", "c", "pkg.c", "f.go", 11, 15, "{}");
|
||||
|
||||
cbm_gbuf_insert_edge(gb, a, b, "CALLS", "{}");
|
||||
cbm_gbuf_insert_edge(gb, b, c, "CALLS", "{}");
|
||||
cbm_gbuf_insert_edge(gb, a, b, "IMPORTS", "{}");
|
||||
cbm_gbuf_insert_edge(gb, a, c, "HTTP_CALLS", "{}");
|
||||
ASSERT_EQ(cbm_gbuf_edge_count(gb), 4);
|
||||
|
||||
cbm_gbuf_delete_edges_by_type(gb, "CALLS");
|
||||
ASSERT_EQ(cbm_gbuf_edge_count(gb), 2);
|
||||
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, "CALLS"), 0);
|
||||
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, "IMPORTS"), 1);
|
||||
ASSERT_EQ(cbm_gbuf_edge_count_by_type(gb, "HTTP_CALLS"), 1);
|
||||
|
||||
/* Verify secondary indexes are consistent after rebuild */
|
||||
const cbm_gbuf_edge_t **edges = NULL;
|
||||
int count = 0;
|
||||
cbm_gbuf_find_edges_by_source_type(gb, a, "IMPORTS", &edges, &count);
|
||||
ASSERT_EQ(count, 1);
|
||||
cbm_gbuf_find_edges_by_target_type(gb, c, "HTTP_CALLS", &edges, &count);
|
||||
ASSERT_EQ(count, 1);
|
||||
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_find_edges_by_target_type_multiple) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
int64_t a = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
|
||||
int64_t b = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
|
||||
int64_t c = cbm_gbuf_upsert_node(gb, "Function", "c", "pkg.c", "f.go", 11, 15, "{}");
|
||||
|
||||
/* Both a and c call b */
|
||||
cbm_gbuf_insert_edge(gb, a, b, "CALLS", "{}");
|
||||
cbm_gbuf_insert_edge(gb, c, b, "CALLS", "{}");
|
||||
|
||||
const cbm_gbuf_edge_t **edges = NULL;
|
||||
int count = 0;
|
||||
cbm_gbuf_find_edges_by_target_type(gb, b, "CALLS", &edges, &count);
|
||||
ASSERT_EQ(count, 2);
|
||||
|
||||
/* No IMPORTS edges targeting b */
|
||||
cbm_gbuf_find_edges_by_target_type(gb, b, "IMPORTS", &edges, &count);
|
||||
ASSERT_EQ(count, 0);
|
||||
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Merge tests ──────────────────────────────────────────────── */
|
||||
|
||||
TEST(gbuf_merge_overlapping_qns) {
|
||||
cbm_gbuf_t *dst = cbm_gbuf_new("test", "/tmp");
|
||||
cbm_gbuf_t *src = cbm_gbuf_new("test", "/tmp");
|
||||
|
||||
/* dst has node with QN "pkg.fn" */
|
||||
cbm_gbuf_upsert_node(dst, "Function", "fn_old", "pkg.fn", "old.go", 1, 10, "{\"from\":\"dst\"}");
|
||||
cbm_gbuf_upsert_node(dst, "Function", "unique_dst", "pkg.unique_dst", "u.go", 1, 5, "{}");
|
||||
|
||||
/* src has same QN with different fields — src should win */
|
||||
cbm_gbuf_upsert_node(src, "Method", "fn_new", "pkg.fn", "new.go", 20, 30, "{\"from\":\"src\"}");
|
||||
cbm_gbuf_upsert_node(src, "Function", "unique_src", "pkg.unique_src", "s.go", 1, 5, "{}");
|
||||
|
||||
int rc = cbm_gbuf_merge(dst, src);
|
||||
ASSERT_EQ(rc, 0);
|
||||
|
||||
/* Total: 3 nodes (1 merged + 1 dst-only + 1 src-only) */
|
||||
ASSERT_EQ(cbm_gbuf_node_count(dst), 3);
|
||||
|
||||
/* Verify src fields won for the overlapping QN */
|
||||
const cbm_gbuf_node_t *n = cbm_gbuf_find_by_qn(dst, "pkg.fn");
|
||||
ASSERT_NOT_NULL(n);
|
||||
ASSERT_STR_EQ(n->label, "Method");
|
||||
ASSERT_STR_EQ(n->name, "fn_new");
|
||||
ASSERT_STR_EQ(n->file_path, "new.go");
|
||||
ASSERT_EQ(n->start_line, 20);
|
||||
ASSERT_STR_EQ(n->properties_json, "{\"from\":\"src\"}");
|
||||
|
||||
/* Both unique nodes present */
|
||||
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "pkg.unique_dst"));
|
||||
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "pkg.unique_src"));
|
||||
|
||||
cbm_gbuf_free(dst);
|
||||
cbm_gbuf_free(src);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_merge_edge_dedup) {
|
||||
_Atomic int64_t shared = 1;
|
||||
cbm_gbuf_t *dst = cbm_gbuf_new_shared_ids("test", "/tmp", &shared);
|
||||
cbm_gbuf_t *src = cbm_gbuf_new_shared_ids("test", "/tmp", &shared);
|
||||
|
||||
int64_t a = cbm_gbuf_upsert_node(dst, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
|
||||
int64_t b = cbm_gbuf_upsert_node(dst, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
|
||||
cbm_gbuf_insert_edge(dst, a, b, "CALLS", "{}");
|
||||
|
||||
/* src has same nodes (by QN) and same edge */
|
||||
cbm_gbuf_upsert_node(src, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
|
||||
cbm_gbuf_upsert_node(src, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
|
||||
/* This edge's src/target IDs differ from dst's, but after merge remap
|
||||
* it becomes the same (src, tgt, type) tuple → deduped */
|
||||
int64_t sa = cbm_gbuf_find_by_qn(src, "pkg.a")->id;
|
||||
int64_t sb = cbm_gbuf_find_by_qn(src, "pkg.b")->id;
|
||||
cbm_gbuf_insert_edge(src, sa, sb, "CALLS", "{}");
|
||||
|
||||
int rc = cbm_gbuf_merge(dst, src);
|
||||
ASSERT_EQ(rc, 0);
|
||||
|
||||
ASSERT_EQ(cbm_gbuf_node_count(dst), 2);
|
||||
ASSERT_EQ(cbm_gbuf_edge_count(dst), 1); /* deduped */
|
||||
|
||||
cbm_gbuf_free(dst);
|
||||
cbm_gbuf_free(src);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_merge_empty_src_into_populated_dst) {
|
||||
cbm_gbuf_t *dst = cbm_gbuf_new("test", "/tmp");
|
||||
cbm_gbuf_t *src = cbm_gbuf_new("test", "/tmp");
|
||||
|
||||
cbm_gbuf_upsert_node(dst, "Function", "foo", "pkg.foo", "f.go", 1, 5, "{}");
|
||||
cbm_gbuf_upsert_node(dst, "Function", "bar", "pkg.bar", "f.go", 6, 10, "{}");
|
||||
|
||||
int rc = cbm_gbuf_merge(dst, src);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(cbm_gbuf_node_count(dst), 2); /* unchanged */
|
||||
|
||||
cbm_gbuf_free(dst);
|
||||
cbm_gbuf_free(src);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_merge_populated_src_into_empty_dst) {
|
||||
cbm_gbuf_t *dst = cbm_gbuf_new("test", "/tmp");
|
||||
cbm_gbuf_t *src = cbm_gbuf_new("test", "/tmp");
|
||||
|
||||
int64_t a = cbm_gbuf_upsert_node(src, "Function", "foo", "pkg.foo", "f.go", 1, 5, "{}");
|
||||
int64_t b = cbm_gbuf_upsert_node(src, "Function", "bar", "pkg.bar", "f.go", 6, 10, "{}");
|
||||
cbm_gbuf_insert_edge(src, a, b, "CALLS", "{}");
|
||||
|
||||
int rc = cbm_gbuf_merge(dst, src);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(cbm_gbuf_node_count(dst), 2);
|
||||
ASSERT_EQ(cbm_gbuf_edge_count(dst), 1);
|
||||
|
||||
/* Verify nodes are findable in dst */
|
||||
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "pkg.foo"));
|
||||
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "pkg.bar"));
|
||||
|
||||
cbm_gbuf_free(dst);
|
||||
cbm_gbuf_free(src);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_merge_null_args) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
ASSERT_EQ(cbm_gbuf_merge(NULL, gb), -1);
|
||||
ASSERT_EQ(cbm_gbuf_merge(gb, NULL), -1);
|
||||
ASSERT_EQ(cbm_gbuf_merge(NULL, NULL), -1);
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Flush / merge-into-store tests ───────────────────────────── */
|
||||
|
||||
TEST(gbuf_flush_to_store_null) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
/* NULL store returns -1 */
|
||||
ASSERT_EQ(cbm_gbuf_flush_to_store(gb, NULL), -1);
|
||||
/* NULL gbuf returns -1 */
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
ASSERT_EQ(cbm_gbuf_flush_to_store(NULL, store), -1);
|
||||
cbm_store_close(store);
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_flush_verify_store_data) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("proj", "/tmp/repo");
|
||||
int64_t n1 = cbm_gbuf_upsert_node(gb, "Function", "alpha", "proj::alpha", "a.go", 1, 10, "{}");
|
||||
int64_t n2 = cbm_gbuf_upsert_node(gb, "Class", "Beta", "proj::Beta", "b.go", 1, 20, "{}");
|
||||
int64_t n3 = cbm_gbuf_upsert_node(gb, "Function", "gamma", "proj::gamma", "c.go", 1, 5, "{}");
|
||||
cbm_gbuf_insert_edge(gb, n1, n2, "CALLS", "{}");
|
||||
cbm_gbuf_insert_edge(gb, n1, n3, "IMPORTS", "{}");
|
||||
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
ASSERT_NOT_NULL(store);
|
||||
int rc = cbm_gbuf_flush_to_store(gb, store);
|
||||
ASSERT_EQ(rc, 0);
|
||||
|
||||
/* Verify counts */
|
||||
ASSERT_EQ(cbm_store_count_nodes(store, "proj"), 3);
|
||||
ASSERT_EQ(cbm_store_count_edges(store, "proj"), 2);
|
||||
|
||||
/* Verify node lookup by QN */
|
||||
cbm_node_t out;
|
||||
rc = cbm_store_find_node_by_qn(store, "proj", "proj::alpha", &out);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_STR_EQ(out.name, "alpha");
|
||||
ASSERT_STR_EQ(out.label, "Function");
|
||||
cbm_node_free_fields(&out);
|
||||
|
||||
rc = cbm_store_find_node_by_qn(store, "proj", "proj::Beta", &out);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_STR_EQ(out.label, "Class");
|
||||
cbm_node_free_fields(&out);
|
||||
|
||||
cbm_store_close(store);
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_merge_into_store_preserves) {
|
||||
/* First, flush initial data via flush_to_store */
|
||||
cbm_gbuf_t *gb1 = cbm_gbuf_new("proj", "/tmp/repo");
|
||||
cbm_gbuf_upsert_node(gb1, "Function", "existing", "proj::existing", "e.go", 1, 10, "{}");
|
||||
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
ASSERT_NOT_NULL(store);
|
||||
cbm_store_upsert_project(store, "proj", "/tmp/repo");
|
||||
int rc = cbm_gbuf_flush_to_store(gb1, store);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(cbm_store_count_nodes(store, "proj"), 1);
|
||||
|
||||
/* Now merge_into_store with new data — should NOT delete existing */
|
||||
cbm_gbuf_t *gb2 = cbm_gbuf_new("proj", "/tmp/repo");
|
||||
cbm_gbuf_upsert_node(gb2, "Function", "newone", "proj::newone", "n.go", 1, 5, "{}");
|
||||
|
||||
rc = cbm_gbuf_merge_into_store(gb2, store);
|
||||
ASSERT_EQ(rc, 0);
|
||||
|
||||
/* Both nodes should exist */
|
||||
ASSERT_EQ(cbm_store_count_nodes(store, "proj"), 2);
|
||||
|
||||
cbm_node_t out;
|
||||
rc = cbm_store_find_node_by_qn(store, "proj", "proj::existing", &out);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
cbm_node_free_fields(&out);
|
||||
|
||||
rc = cbm_store_find_node_by_qn(store, "proj", "proj::newone", &out);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
cbm_node_free_fields(&out);
|
||||
|
||||
cbm_store_close(store);
|
||||
cbm_gbuf_free(gb1);
|
||||
cbm_gbuf_free(gb2);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Shared ID tests ──────────────────────────────────────────── */
|
||||
|
||||
TEST(gbuf_shared_ids_unique) {
|
||||
_Atomic int64_t shared = 1;
|
||||
cbm_gbuf_t *gb1 = cbm_gbuf_new_shared_ids("test", "/tmp", &shared);
|
||||
cbm_gbuf_t *gb2 = cbm_gbuf_new_shared_ids("test", "/tmp", &shared);
|
||||
|
||||
int64_t id1 = cbm_gbuf_upsert_node(gb1, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
|
||||
int64_t id2 = cbm_gbuf_upsert_node(gb2, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
|
||||
int64_t id3 = cbm_gbuf_upsert_node(gb1, "Function", "c", "pkg.c", "f.go", 11, 15, "{}");
|
||||
int64_t id4 = cbm_gbuf_upsert_node(gb2, "Function", "d", "pkg.d", "f.go", 16, 20, "{}");
|
||||
|
||||
/* All IDs must be unique */
|
||||
ASSERT_NEQ(id1, id2);
|
||||
ASSERT_NEQ(id1, id3);
|
||||
ASSERT_NEQ(id1, id4);
|
||||
ASSERT_NEQ(id2, id3);
|
||||
ASSERT_NEQ(id2, id4);
|
||||
ASSERT_NEQ(id3, id4);
|
||||
|
||||
/* IDs should be sequential from the shared source */
|
||||
ASSERT_EQ(id1, 1);
|
||||
ASSERT_EQ(id2, 2);
|
||||
ASSERT_EQ(id3, 3);
|
||||
ASSERT_EQ(id4, 4);
|
||||
|
||||
cbm_gbuf_free(gb1);
|
||||
cbm_gbuf_free(gb2);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_shared_ids_null_fallback) {
|
||||
/* NULL id_source → behaves like cbm_gbuf_new() */
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new_shared_ids("test", "/tmp", NULL);
|
||||
ASSERT_NOT_NULL(gb);
|
||||
int64_t id = cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
|
||||
ASSERT_EQ(id, 1);
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_next_id_set_next_id_roundtrip) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp");
|
||||
|
||||
/* Initial next_id is 1 */
|
||||
ASSERT_EQ(cbm_gbuf_next_id(gb), 1);
|
||||
|
||||
/* Insert a node, next_id advances */
|
||||
cbm_gbuf_upsert_node(gb, "Function", "a", "pkg.a", "f.go", 1, 5, "{}");
|
||||
ASSERT_EQ(cbm_gbuf_next_id(gb), 2);
|
||||
|
||||
/* Set next_id to an arbitrary value */
|
||||
cbm_gbuf_set_next_id(gb, 100);
|
||||
ASSERT_EQ(cbm_gbuf_next_id(gb), 100);
|
||||
|
||||
/* Next insert uses the new base */
|
||||
int64_t id = cbm_gbuf_upsert_node(gb, "Function", "b", "pkg.b", "f.go", 6, 10, "{}");
|
||||
ASSERT_EQ(id, 100);
|
||||
ASSERT_EQ(cbm_gbuf_next_id(gb), 101);
|
||||
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(gbuf_next_id_null_safe) {
|
||||
/* next_id on NULL returns 1 (default) */
|
||||
ASSERT_EQ(cbm_gbuf_next_id(NULL), 1);
|
||||
/* set_next_id on NULL should not crash */
|
||||
cbm_gbuf_set_next_id(NULL, 42);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Flush edge case: orphan edges skipped ────────────────────── */
|
||||
|
||||
TEST(gbuf_flush_skips_orphan_edges) {
|
||||
cbm_gbuf_t *gb = cbm_gbuf_new("proj", "/tmp/repo");
|
||||
int64_t n1 = cbm_gbuf_upsert_node(gb, "Function", "real", "proj::real", "f.go", 1, 5, "{}");
|
||||
|
||||
/* Valid edge */
|
||||
cbm_gbuf_insert_edge(gb, n1, n1, "CALLS", "{}");
|
||||
/* Orphan edge — target ID 9999 does not map to any node */
|
||||
cbm_gbuf_insert_edge(gb, n1, 9999, "CALLS", "{}");
|
||||
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
ASSERT_NOT_NULL(store);
|
||||
int rc = cbm_gbuf_flush_to_store(gb, store);
|
||||
ASSERT_EQ(rc, 0);
|
||||
|
||||
/* Only the valid edge should land in the store */
|
||||
ASSERT_EQ(cbm_store_count_nodes(store, "proj"), 1);
|
||||
ASSERT_EQ(cbm_store_count_edges(store, "proj"), 1);
|
||||
|
||||
cbm_store_close(store);
|
||||
cbm_gbuf_free(gb);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Suite ─────────────────────────────────────────────────────── */
|
||||
|
||||
SUITE(graph_buffer) {
|
||||
/* Original tests */
|
||||
RUN_TEST(gbuf_create_free);
|
||||
RUN_TEST(gbuf_free_null);
|
||||
RUN_TEST(gbuf_upsert_node);
|
||||
@@ -358,4 +949,44 @@ SUITE(graph_buffer) {
|
||||
RUN_TEST(gbuf_dump_empty);
|
||||
RUN_TEST(gbuf_flush_to_store);
|
||||
RUN_TEST(gbuf_many_nodes);
|
||||
|
||||
/* Node edge cases */
|
||||
RUN_TEST(gbuf_upsert_null_qn);
|
||||
RUN_TEST(gbuf_upsert_empty_qn);
|
||||
RUN_TEST(gbuf_upsert_same_qn_updates_all_fields);
|
||||
RUN_TEST(gbuf_upsert_long_qn);
|
||||
RUN_TEST(gbuf_find_by_qn_missing);
|
||||
RUN_TEST(gbuf_find_by_id_missing);
|
||||
RUN_TEST(gbuf_find_by_label_no_matches);
|
||||
RUN_TEST(gbuf_find_by_name_multiple);
|
||||
RUN_TEST(gbuf_delete_by_label_cascades_edges);
|
||||
RUN_TEST(gbuf_node_count_empty);
|
||||
RUN_TEST(gbuf_upsert_100_nodes_stress);
|
||||
|
||||
/* Edge edge cases */
|
||||
RUN_TEST(gbuf_edge_nonexistent_endpoints);
|
||||
RUN_TEST(gbuf_edge_dedup_merges_properties);
|
||||
RUN_TEST(gbuf_edge_count_empty);
|
||||
RUN_TEST(gbuf_edge_count_by_type_missing);
|
||||
RUN_TEST(gbuf_delete_edges_preserves_other_types);
|
||||
RUN_TEST(gbuf_find_edges_by_target_type_multiple);
|
||||
|
||||
/* Merge tests */
|
||||
RUN_TEST(gbuf_merge_overlapping_qns);
|
||||
RUN_TEST(gbuf_merge_edge_dedup);
|
||||
RUN_TEST(gbuf_merge_empty_src_into_populated_dst);
|
||||
RUN_TEST(gbuf_merge_populated_src_into_empty_dst);
|
||||
RUN_TEST(gbuf_merge_null_args);
|
||||
|
||||
/* Flush/merge-into-store tests */
|
||||
RUN_TEST(gbuf_flush_to_store_null);
|
||||
RUN_TEST(gbuf_flush_verify_store_data);
|
||||
RUN_TEST(gbuf_merge_into_store_preserves);
|
||||
RUN_TEST(gbuf_flush_skips_orphan_edges);
|
||||
|
||||
/* Shared ID tests */
|
||||
RUN_TEST(gbuf_shared_ids_unique);
|
||||
RUN_TEST(gbuf_shared_ids_null_fallback);
|
||||
RUN_TEST(gbuf_next_id_set_next_id_roundtrip);
|
||||
RUN_TEST(gbuf_next_id_null_safe);
|
||||
}
|
||||
@@ -174,6 +174,214 @@ TEST(ht_power_of_two_capacity) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge case tests ───────────────────────────────────────────── */
|
||||
|
||||
TEST(ht_get_key_returns_stored_pointer) {
|
||||
CBMHashTable *ht = cbm_ht_create(8);
|
||||
/* Use a heap key so we can verify the stored pointer differs from lookup */
|
||||
char stored_key[] = "mykey";
|
||||
int v = 1;
|
||||
cbm_ht_set(ht, stored_key, &v);
|
||||
/* Look up with a different buffer containing same string */
|
||||
char lookup[] = "mykey";
|
||||
const char *got = cbm_ht_get_key(ht, lookup);
|
||||
ASSERT_NOT_NULL(got);
|
||||
ASSERT_STR_EQ(got, "mykey");
|
||||
/* Should return the pointer we inserted with, not the lookup buffer */
|
||||
ASSERT_EQ((uintptr_t)got, (uintptr_t)stored_key);
|
||||
cbm_ht_free(ht);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(ht_get_key_missing) {
|
||||
CBMHashTable *ht = cbm_ht_create(8);
|
||||
const char *got = cbm_ht_get_key(ht, "nonexistent");
|
||||
ASSERT_NULL(got);
|
||||
cbm_ht_free(ht);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(ht_get_key_null_ht) {
|
||||
const char *got = cbm_ht_get_key(NULL, "key");
|
||||
ASSERT_NULL(got);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(ht_get_key_null_key) {
|
||||
CBMHashTable *ht = cbm_ht_create(8);
|
||||
const char *got = cbm_ht_get_key(ht, NULL);
|
||||
ASSERT_NULL(got);
|
||||
cbm_ht_free(ht);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(ht_count_null) {
|
||||
ASSERT_EQ(cbm_ht_count(NULL), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(ht_free_null) {
|
||||
/* Should not crash */
|
||||
cbm_ht_free(NULL);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(ht_clear_null) {
|
||||
/* Should not crash */
|
||||
cbm_ht_clear(NULL);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(ht_foreach_null_ht) {
|
||||
/* Should not crash */
|
||||
cbm_ht_foreach(NULL, sum_values, NULL);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(ht_foreach_null_fn) {
|
||||
CBMHashTable *ht = cbm_ht_create(8);
|
||||
int v = 1;
|
||||
cbm_ht_set(ht, "a", &v);
|
||||
/* Should not crash with NULL function pointer */
|
||||
cbm_ht_foreach(ht, NULL, NULL);
|
||||
cbm_ht_free(ht);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(ht_delete_readd_stress) {
|
||||
/* Stress backward-shift deletion: add 100, delete all, re-add all */
|
||||
CBMHashTable *ht = cbm_ht_create(8);
|
||||
char keys[100][16];
|
||||
int vals[100];
|
||||
for (int i = 0; i < 100; i++) {
|
||||
snprintf(keys[i], sizeof(keys[i]), "key_%03d", i);
|
||||
vals[i] = i;
|
||||
cbm_ht_set(ht, keys[i], &vals[i]);
|
||||
}
|
||||
ASSERT_EQ(cbm_ht_count(ht), 100);
|
||||
|
||||
/* Delete all */
|
||||
for (int i = 0; i < 100; i++) {
|
||||
void *removed = cbm_ht_delete(ht, keys[i]);
|
||||
ASSERT_EQ(removed, &vals[i]);
|
||||
}
|
||||
ASSERT_EQ(cbm_ht_count(ht), 0);
|
||||
|
||||
/* Re-add all — exercises slots freed by backward shift */
|
||||
for (int i = 0; i < 100; i++) {
|
||||
cbm_ht_set(ht, keys[i], &vals[i]);
|
||||
}
|
||||
ASSERT_EQ(cbm_ht_count(ht), 100);
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ASSERT_EQ(*(int *)cbm_ht_get(ht, keys[i]), i);
|
||||
}
|
||||
cbm_ht_free(ht);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(ht_interleaved_insert_delete) {
|
||||
/* Insert 100, delete odd-indexed, verify even-indexed survive */
|
||||
CBMHashTable *ht = cbm_ht_create(8);
|
||||
char keys[100][16];
|
||||
int vals[100];
|
||||
for (int i = 0; i < 100; i++) {
|
||||
snprintf(keys[i], sizeof(keys[i]), "entry_%03d", i);
|
||||
vals[i] = i * 7;
|
||||
cbm_ht_set(ht, keys[i], &vals[i]);
|
||||
}
|
||||
ASSERT_EQ(cbm_ht_count(ht), 100);
|
||||
|
||||
/* Delete all odd-indexed entries */
|
||||
for (int i = 1; i < 100; i += 2) {
|
||||
cbm_ht_delete(ht, keys[i]);
|
||||
}
|
||||
ASSERT_EQ(cbm_ht_count(ht), 50);
|
||||
|
||||
/* Verify even entries intact, odd entries gone */
|
||||
for (int i = 0; i < 100; i++) {
|
||||
void *got = cbm_ht_get(ht, keys[i]);
|
||||
if (i % 2 == 0) {
|
||||
ASSERT_NOT_NULL(got);
|
||||
ASSERT_EQ(*(int *)got, i * 7);
|
||||
} else {
|
||||
ASSERT_NULL(got);
|
||||
}
|
||||
}
|
||||
cbm_ht_free(ht);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(ht_create_capacity_zero) {
|
||||
/* Capacity 0 should be rounded up to minimum (8) */
|
||||
CBMHashTable *ht = cbm_ht_create(0);
|
||||
ASSERT_NOT_NULL(ht);
|
||||
ASSERT_GTE(ht->capacity, 8);
|
||||
ASSERT_EQ(ht->capacity & (ht->capacity - 1), 0);
|
||||
/* Should be usable */
|
||||
int v = 42;
|
||||
cbm_ht_set(ht, "test", &v);
|
||||
ASSERT_EQ(*(int *)cbm_ht_get(ht, "test"), 42);
|
||||
cbm_ht_free(ht);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(ht_create_capacity_one) {
|
||||
/* Capacity 1 should be rounded up to minimum (8) */
|
||||
CBMHashTable *ht = cbm_ht_create(1);
|
||||
ASSERT_NOT_NULL(ht);
|
||||
ASSERT_GTE(ht->capacity, 8);
|
||||
int v = 99;
|
||||
cbm_ht_set(ht, "k", &v);
|
||||
ASSERT_EQ(*(int *)cbm_ht_get(ht, "k"), 99);
|
||||
cbm_ht_free(ht);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(ht_stress_10000) {
|
||||
CBMHashTable *ht = cbm_ht_create(16);
|
||||
char keys[10000][24];
|
||||
int vals[10000];
|
||||
for (int i = 0; i < 10000; i++) {
|
||||
snprintf(keys[i], sizeof(keys[i]), "stress_key_%06d", i);
|
||||
vals[i] = i;
|
||||
cbm_ht_set(ht, keys[i], &vals[i]);
|
||||
}
|
||||
ASSERT_EQ(cbm_ht_count(ht), 10000);
|
||||
/* Verify all */
|
||||
for (int i = 0; i < 10000; i++) {
|
||||
void *got = cbm_ht_get(ht, keys[i]);
|
||||
ASSERT_NOT_NULL(got);
|
||||
ASSERT_EQ(*(int *)got, i);
|
||||
}
|
||||
/* Capacity should still be power of 2 */
|
||||
ASSERT_EQ(ht->capacity & (ht->capacity - 1), 0);
|
||||
cbm_ht_free(ht);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(ht_robin_hood_bounded_psl) {
|
||||
/* Max PSL should be bounded — Robin Hood keeps probes short.
|
||||
* With 10000 entries and power-of-2 sizing, max PSL should be < 32. */
|
||||
CBMHashTable *ht = cbm_ht_create(16);
|
||||
char key[24];
|
||||
int val = 0;
|
||||
for (int i = 0; i < 10000; i++) {
|
||||
snprintf(key, sizeof(key), "rh_%06d", i);
|
||||
cbm_ht_set(ht, key, &val);
|
||||
}
|
||||
/* Scan entries, find max PSL */
|
||||
uint32_t max_psl = 0;
|
||||
for (uint32_t i = 0; i < ht->capacity; i++) {
|
||||
if (ht->entries[i].psl > max_psl) {
|
||||
max_psl = ht->entries[i].psl;
|
||||
}
|
||||
}
|
||||
/* Robin Hood guarantees bounded PSL — should be well under 32 */
|
||||
ASSERT_LT(max_psl, 32);
|
||||
cbm_ht_free(ht);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(hash_table) {
|
||||
RUN_TEST(ht_create_free);
|
||||
RUN_TEST(ht_set_get);
|
||||
@@ -189,4 +397,20 @@ SUITE(hash_table) {
|
||||
RUN_TEST(ht_empty_string_key);
|
||||
RUN_TEST(ht_long_key);
|
||||
RUN_TEST(ht_power_of_two_capacity);
|
||||
/* Edge cases */
|
||||
RUN_TEST(ht_get_key_returns_stored_pointer);
|
||||
RUN_TEST(ht_get_key_missing);
|
||||
RUN_TEST(ht_get_key_null_ht);
|
||||
RUN_TEST(ht_get_key_null_key);
|
||||
RUN_TEST(ht_count_null);
|
||||
RUN_TEST(ht_free_null);
|
||||
RUN_TEST(ht_clear_null);
|
||||
RUN_TEST(ht_foreach_null_ht);
|
||||
RUN_TEST(ht_foreach_null_fn);
|
||||
RUN_TEST(ht_delete_readd_stress);
|
||||
RUN_TEST(ht_interleaved_insert_delete);
|
||||
RUN_TEST(ht_create_capacity_zero);
|
||||
RUN_TEST(ht_create_capacity_one);
|
||||
RUN_TEST(ht_stress_10000);
|
||||
RUN_TEST(ht_robin_hood_bounded_psl);
|
||||
}
|
||||
@@ -32,6 +32,7 @@ extern void suite_discover(void);
|
||||
extern void suite_graph_buffer(void);
|
||||
extern void suite_registry(void);
|
||||
extern void suite_pipeline(void);
|
||||
extern void suite_fqn(void);
|
||||
extern void suite_watcher(void);
|
||||
extern void suite_lz4(void);
|
||||
extern void suite_sqlite_writer(void);
|
||||
@@ -50,6 +51,7 @@ extern void suite_parallel(void);
|
||||
extern void suite_mem(void);
|
||||
extern void suite_ui(void);
|
||||
extern void suite_security(void);
|
||||
extern void suite_yaml(void);
|
||||
extern void suite_integration(void);
|
||||
|
||||
int main(void) {
|
||||
@@ -92,6 +94,7 @@ int main(void) {
|
||||
/* Pipeline (M8) */
|
||||
RUN_SUITE(registry);
|
||||
RUN_SUITE(pipeline);
|
||||
RUN_SUITE(fqn);
|
||||
|
||||
/* Watcher (M10) */
|
||||
RUN_SUITE(watcher);
|
||||
@@ -138,6 +141,9 @@ int main(void) {
|
||||
/* Security defenses */
|
||||
RUN_SUITE(security);
|
||||
|
||||
/* YAML parser */
|
||||
RUN_SUITE(yaml);
|
||||
|
||||
/* Integration (end-to-end) */
|
||||
RUN_SUITE(integration);
|
||||
|
||||
|
||||
@@ -1276,6 +1276,279 @@ TEST(snippet_include_neighbors_enabled) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
* JSON-RPC PARSING — EDGE CASES
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
TEST(jsonrpc_parse_empty_string) {
|
||||
cbm_jsonrpc_request_t req = {0};
|
||||
int rc = cbm_jsonrpc_parse("", &req);
|
||||
ASSERT_EQ(rc, -1);
|
||||
cbm_jsonrpc_request_free(&req);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(jsonrpc_parse_missing_jsonrpc_field) {
|
||||
/* jsonrpc field absent — parser defaults to "2.0" if method present */
|
||||
const char *line = "{\"id\":1,\"method\":\"initialize\",\"params\":{}}";
|
||||
cbm_jsonrpc_request_t req = {0};
|
||||
int rc = cbm_jsonrpc_parse(line, &req);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_STR_EQ(req.jsonrpc, "2.0");
|
||||
ASSERT_STR_EQ(req.method, "initialize");
|
||||
ASSERT_TRUE(req.has_id);
|
||||
cbm_jsonrpc_request_free(&req);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(jsonrpc_parse_missing_method) {
|
||||
/* method is required — should fail */
|
||||
const char *line = "{\"jsonrpc\":\"2.0\",\"id\":1,\"params\":{}}";
|
||||
cbm_jsonrpc_request_t req = {0};
|
||||
int rc = cbm_jsonrpc_parse(line, &req);
|
||||
ASSERT_EQ(rc, -1);
|
||||
cbm_jsonrpc_request_free(&req);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(jsonrpc_parse_string_id) {
|
||||
/* JSON-RPC spec allows string IDs; parser converts via strtol */
|
||||
const char *line = "{\"jsonrpc\":\"2.0\",\"id\":\"99\",\"method\":\"tools/list\"}";
|
||||
cbm_jsonrpc_request_t req = {0};
|
||||
int rc = cbm_jsonrpc_parse(line, &req);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_TRUE(req.has_id);
|
||||
ASSERT_EQ(req.id, 99);
|
||||
ASSERT_STR_EQ(req.method, "tools/list");
|
||||
cbm_jsonrpc_request_free(&req);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(jsonrpc_parse_no_params) {
|
||||
/* Request with no params field — params_raw should be NULL */
|
||||
const char *line = "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"tools/list\"}";
|
||||
cbm_jsonrpc_request_t req = {0};
|
||||
int rc = cbm_jsonrpc_parse(line, &req);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_NULL(req.params_raw);
|
||||
ASSERT_EQ(req.id, 5);
|
||||
cbm_jsonrpc_request_free(&req);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(jsonrpc_parse_extra_whitespace) {
|
||||
/* Leading/trailing whitespace and internal spacing in JSON */
|
||||
const char *line = " { \"jsonrpc\" : \"2.0\" , \"id\" : 7 , \"method\" : \"ping\" } ";
|
||||
cbm_jsonrpc_request_t req = {0};
|
||||
int rc = cbm_jsonrpc_parse(line, &req);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(req.id, 7);
|
||||
ASSERT_STR_EQ(req.method, "ping");
|
||||
cbm_jsonrpc_request_free(&req);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(jsonrpc_parse_array_not_object) {
|
||||
/* JSON array at root — not a valid JSON-RPC request */
|
||||
cbm_jsonrpc_request_t req = {0};
|
||||
int rc = cbm_jsonrpc_parse("[1,2,3]", &req);
|
||||
ASSERT_EQ(rc, -1);
|
||||
cbm_jsonrpc_request_free(&req);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
* ARGUMENT EXTRACTION — EDGE CASES
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
TEST(mcp_get_string_arg_empty_json) {
|
||||
/* Empty JSON string — yyjson_read fails → NULL */
|
||||
char *val = cbm_mcp_get_string_arg("", "key");
|
||||
ASSERT_NULL(val);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mcp_get_string_arg_empty_object) {
|
||||
/* Valid JSON with no keys → NULL for any key */
|
||||
char *val = cbm_mcp_get_string_arg("{}", "key");
|
||||
ASSERT_NULL(val);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mcp_get_string_arg_nested_value) {
|
||||
/* Value is an object, not a string → should return NULL */
|
||||
const char *args = "{\"config\":{\"nested\":true},\"name\":\"hello\"}";
|
||||
char *val = cbm_mcp_get_string_arg(args, "config");
|
||||
ASSERT_NULL(val); /* not a string type */
|
||||
val = cbm_mcp_get_string_arg(args, "name");
|
||||
ASSERT_NOT_NULL(val);
|
||||
ASSERT_STR_EQ(val, "hello");
|
||||
free(val);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mcp_get_string_arg_int_value) {
|
||||
/* Value is an integer, not a string → NULL */
|
||||
char *val = cbm_mcp_get_string_arg("{\"count\":42}", "count");
|
||||
ASSERT_NULL(val);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mcp_get_int_arg_empty_json) {
|
||||
int val = cbm_mcp_get_int_arg("", "key", 99);
|
||||
ASSERT_EQ(val, 99);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mcp_get_int_arg_string_value) {
|
||||
/* Value is a string, not int → should return default */
|
||||
int val = cbm_mcp_get_int_arg("{\"limit\":\"ten\"}", "limit", 5);
|
||||
ASSERT_EQ(val, 5);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mcp_get_int_arg_bool_value) {
|
||||
/* Value is a bool, not int → default */
|
||||
int val = cbm_mcp_get_int_arg("{\"flag\":true}", "flag", -1);
|
||||
ASSERT_EQ(val, -1);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mcp_get_bool_arg_empty_json) {
|
||||
bool val = cbm_mcp_get_bool_arg("", "key");
|
||||
ASSERT_FALSE(val);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mcp_get_bool_arg_int_value) {
|
||||
/* Value is int 1, not bool → should return false */
|
||||
bool val = cbm_mcp_get_bool_arg("{\"flag\":1}", "flag");
|
||||
ASSERT_FALSE(val);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mcp_get_tool_name_empty_json) {
|
||||
char *name = cbm_mcp_get_tool_name("");
|
||||
ASSERT_NULL(name);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mcp_get_tool_name_missing_name) {
|
||||
char *name = cbm_mcp_get_tool_name("{\"arguments\":{}}");
|
||||
ASSERT_NULL(name);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mcp_get_arguments_empty_json) {
|
||||
char *args = cbm_mcp_get_arguments("");
|
||||
ASSERT_NULL(args);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mcp_get_arguments_no_arguments_key) {
|
||||
/* No "arguments" key → returns "{}" */
|
||||
char *args = cbm_mcp_get_arguments("{\"name\":\"tool\"}");
|
||||
ASSERT_NOT_NULL(args);
|
||||
ASSERT_STR_EQ(args, "{}");
|
||||
free(args);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
* FILE URI PARSING — EDGE CASES
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
TEST(parse_file_uri_http_scheme) {
|
||||
char path[256];
|
||||
ASSERT_FALSE(cbm_parse_file_uri("http://example.com/path", path, sizeof(path)));
|
||||
ASSERT_STR_EQ(path, "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parse_file_uri_ftp_scheme) {
|
||||
char path[256];
|
||||
ASSERT_FALSE(cbm_parse_file_uri("ftp://server/file.txt", path, sizeof(path)));
|
||||
ASSERT_STR_EQ(path, "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parse_file_uri_buffer_too_small) {
|
||||
char path[5]; /* only 5 bytes — path gets truncated */
|
||||
ASSERT_TRUE(cbm_parse_file_uri("file:///usr/local/bin", path, sizeof(path)));
|
||||
/* snprintf truncates to 4 chars + NUL */
|
||||
ASSERT_EQ(strlen(path), 4);
|
||||
ASSERT_STR_EQ(path, "/usr");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parse_file_uri_spaces_in_path) {
|
||||
char path[256];
|
||||
ASSERT_TRUE(cbm_parse_file_uri("file:///home/user/my%20project", path, sizeof(path)));
|
||||
/* Raw percent-encoding is preserved (not decoded) */
|
||||
ASSERT_STR_EQ(path, "/home/user/my%20project");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parse_file_uri_null_out_path) {
|
||||
/* NULL out_path — should not crash */
|
||||
ASSERT_FALSE(cbm_parse_file_uri("file:///tmp", NULL, 256));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parse_file_uri_zero_size) {
|
||||
char path[256] = "garbage";
|
||||
/* out_size=0 → should fail safely */
|
||||
ASSERT_FALSE(cbm_parse_file_uri("file:///tmp", path, 0));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
* SERVER HANDLE — EDGE CASES
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
TEST(server_handle_invalid_json) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
|
||||
char *resp = cbm_mcp_server_handle(srv, "this is not json at all");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
ASSERT_NOT_NULL(strstr(resp, "\"error\""));
|
||||
ASSERT_NOT_NULL(strstr(resp, "-32700")); /* Parse error */
|
||||
free(resp);
|
||||
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(server_handle_empty_object) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
|
||||
/* Valid JSON but no method field → parse error */
|
||||
char *resp = cbm_mcp_server_handle(srv, "{}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
ASSERT_NOT_NULL(strstr(resp, "\"error\""));
|
||||
free(resp);
|
||||
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(server_handle_tools_call_missing_name) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
|
||||
/* tools/call with no tool name in params */
|
||||
char *resp = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":50,\"method\":\"tools/call\","
|
||||
"\"params\":{\"arguments\":{}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
/* Should return error about unknown/missing tool */
|
||||
ASSERT_NOT_NULL(strstr(resp, "\"id\":50"));
|
||||
ASSERT_TRUE(strstr(resp, "error") || strstr(resp, "isError") || strstr(resp, "unknown"));
|
||||
free(resp);
|
||||
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
* POLL/GETLINE FILE* BUFFERING FIX
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
@@ -1364,6 +1637,15 @@ SUITE(mcp) {
|
||||
RUN_TEST(jsonrpc_parse_invalid);
|
||||
RUN_TEST(jsonrpc_parse_tools_call);
|
||||
|
||||
/* JSON-RPC parsing — edge cases */
|
||||
RUN_TEST(jsonrpc_parse_empty_string);
|
||||
RUN_TEST(jsonrpc_parse_missing_jsonrpc_field);
|
||||
RUN_TEST(jsonrpc_parse_missing_method);
|
||||
RUN_TEST(jsonrpc_parse_string_id);
|
||||
RUN_TEST(jsonrpc_parse_no_params);
|
||||
RUN_TEST(jsonrpc_parse_extra_whitespace);
|
||||
RUN_TEST(jsonrpc_parse_array_not_object);
|
||||
|
||||
/* JSON-RPC formatting */
|
||||
RUN_TEST(jsonrpc_format_response);
|
||||
RUN_TEST(jsonrpc_format_error);
|
||||
@@ -1382,12 +1664,32 @@ SUITE(mcp) {
|
||||
RUN_TEST(mcp_get_int_arg);
|
||||
RUN_TEST(mcp_get_bool_arg);
|
||||
|
||||
/* Argument extraction — edge cases */
|
||||
RUN_TEST(mcp_get_string_arg_empty_json);
|
||||
RUN_TEST(mcp_get_string_arg_empty_object);
|
||||
RUN_TEST(mcp_get_string_arg_nested_value);
|
||||
RUN_TEST(mcp_get_string_arg_int_value);
|
||||
RUN_TEST(mcp_get_int_arg_empty_json);
|
||||
RUN_TEST(mcp_get_int_arg_string_value);
|
||||
RUN_TEST(mcp_get_int_arg_bool_value);
|
||||
RUN_TEST(mcp_get_bool_arg_empty_json);
|
||||
RUN_TEST(mcp_get_bool_arg_int_value);
|
||||
RUN_TEST(mcp_get_tool_name_empty_json);
|
||||
RUN_TEST(mcp_get_tool_name_missing_name);
|
||||
RUN_TEST(mcp_get_arguments_empty_json);
|
||||
RUN_TEST(mcp_get_arguments_no_arguments_key);
|
||||
|
||||
/* Server protocol handling */
|
||||
RUN_TEST(server_handle_initialize);
|
||||
RUN_TEST(server_handle_initialized_notification);
|
||||
RUN_TEST(server_handle_tools_list);
|
||||
RUN_TEST(server_handle_unknown_method);
|
||||
|
||||
/* Server handle — edge cases */
|
||||
RUN_TEST(server_handle_invalid_json);
|
||||
RUN_TEST(server_handle_empty_object);
|
||||
RUN_TEST(server_handle_tools_call_missing_name);
|
||||
|
||||
/* Tool handlers */
|
||||
RUN_TEST(tool_list_projects_empty);
|
||||
RUN_TEST(tool_get_graph_schema_empty);
|
||||
@@ -1426,6 +1728,14 @@ SUITE(mcp) {
|
||||
RUN_TEST(parse_file_uri_windows);
|
||||
RUN_TEST(parse_file_uri_invalid);
|
||||
|
||||
/* URI helpers — edge cases */
|
||||
RUN_TEST(parse_file_uri_http_scheme);
|
||||
RUN_TEST(parse_file_uri_ftp_scheme);
|
||||
RUN_TEST(parse_file_uri_buffer_too_small);
|
||||
RUN_TEST(parse_file_uri_spaces_in_path);
|
||||
RUN_TEST(parse_file_uri_null_out_path);
|
||||
RUN_TEST(parse_file_uri_zero_size);
|
||||
|
||||
/* Poll/getline FILE* buffering fix */
|
||||
#ifndef _WIN32
|
||||
RUN_TEST(mcp_server_run_rapid_messages);
|
||||
|
||||
@@ -92,6 +92,203 @@ TEST(mem_budget_check) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── mem budget edge-case tests ─────────────────────────────── */
|
||||
|
||||
TEST(mem_worker_budget_zero_workers) {
|
||||
cbm_mem_init(0.5);
|
||||
size_t budget = cbm_mem_budget();
|
||||
/* 0 workers clamps to 1 → worker_budget == full budget */
|
||||
size_t wb = cbm_mem_worker_budget(0);
|
||||
ASSERT_EQ(wb, budget);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mem_worker_budget_negative_workers) {
|
||||
cbm_mem_init(0.5);
|
||||
size_t budget = cbm_mem_budget();
|
||||
/* Negative workers clamps to 1 → worker_budget == full budget */
|
||||
size_t wb = cbm_mem_worker_budget(-5);
|
||||
ASSERT_EQ(wb, budget);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mem_worker_budget_one_worker) {
|
||||
cbm_mem_init(0.5);
|
||||
size_t budget = cbm_mem_budget();
|
||||
/* 1 worker → equals full budget */
|
||||
size_t wb = cbm_mem_worker_budget(1);
|
||||
ASSERT_EQ(wb, budget);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mem_worker_budget_many_workers) {
|
||||
cbm_mem_init(0.5);
|
||||
/* 1000 workers → should produce non-zero result (budget is huge) */
|
||||
size_t wb = cbm_mem_worker_budget(1000);
|
||||
ASSERT_GT(wb, 0);
|
||||
/* Must be budget / 1000 */
|
||||
ASSERT_EQ(wb, cbm_mem_budget() / 1000);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mem_over_budget_low_rss) {
|
||||
cbm_mem_init(0.5);
|
||||
/* We're a test process with tiny RSS — should not be over budget */
|
||||
bool over = cbm_mem_over_budget();
|
||||
ASSERT_FALSE(over);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── RSS tracking tests ───────────────────────────────────────── */
|
||||
|
||||
TEST(mem_rss_positive) {
|
||||
cbm_mem_init(0.5);
|
||||
/* A running process always has nonzero RSS */
|
||||
size_t rss = cbm_mem_rss();
|
||||
ASSERT_GT(rss, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mem_peak_rss_gte_rss) {
|
||||
cbm_mem_init(0.5);
|
||||
size_t rss = cbm_mem_rss();
|
||||
size_t peak = cbm_mem_peak_rss();
|
||||
/* Peak must be >= current RSS */
|
||||
ASSERT_GTE(peak, rss);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mem_rss_increases_after_alloc) {
|
||||
cbm_mem_init(0.5);
|
||||
|
||||
/* Allocate 10 MB and touch all pages */
|
||||
size_t alloc_size = 10 * 1024 * 1024;
|
||||
char *p = (char *)malloc(alloc_size);
|
||||
ASSERT_NOT_NULL(p);
|
||||
memset(p, 0xBE, alloc_size);
|
||||
|
||||
size_t rss_after = cbm_mem_rss();
|
||||
/* RSS must be non-zero after allocating 10MB */
|
||||
ASSERT_GT(rss_after, 0);
|
||||
|
||||
free(p);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mem_collect_no_crash) {
|
||||
cbm_mem_init(0.5);
|
||||
/* collect() must not crash even with nothing to collect */
|
||||
cbm_mem_collect();
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mem_collect_rss_still_positive) {
|
||||
cbm_mem_init(0.5);
|
||||
cbm_mem_collect();
|
||||
/* After collect, RSS must still be > 0 (we're alive) */
|
||||
size_t rss = cbm_mem_rss();
|
||||
ASSERT_GT(rss, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Memory pressure simulation ───────────────────────────────── */
|
||||
|
||||
TEST(mem_progressive_alloc_rss_increases) {
|
||||
cbm_mem_init(0.5);
|
||||
|
||||
size_t chunk_size = 2 * 1024 * 1024; /* 2 MB chunks */
|
||||
int nchunks = 5;
|
||||
char *chunks[5];
|
||||
|
||||
for (int i = 0; i < nchunks; i++) {
|
||||
chunks[i] = (char *)malloc(chunk_size);
|
||||
ASSERT_NOT_NULL(chunks[i]);
|
||||
memset(chunks[i], (unsigned char)(0xA0 + i), chunk_size);
|
||||
}
|
||||
|
||||
size_t rss_peak = cbm_mem_rss();
|
||||
ASSERT_GT(rss_peak, 0);
|
||||
|
||||
for (int i = 0; i < nchunks; i++) {
|
||||
free(chunks[i]);
|
||||
}
|
||||
cbm_mem_collect();
|
||||
|
||||
/* After free + collect, RSS may or may not drop, but must not crash */
|
||||
size_t rss_end = cbm_mem_rss();
|
||||
ASSERT_GT(rss_end, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mem_free_and_collect_no_crash) {
|
||||
cbm_mem_init(0.5);
|
||||
|
||||
/* Allocate, free, collect — verify no crash */
|
||||
size_t sz = 4 * 1024 * 1024;
|
||||
char *p = (char *)malloc(sz);
|
||||
ASSERT_NOT_NULL(p);
|
||||
memset(p, 0xCC, sz);
|
||||
free(p);
|
||||
cbm_mem_collect();
|
||||
|
||||
/* RSS must remain positive */
|
||||
ASSERT_GT(cbm_mem_rss(), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mem_multiple_collect_idempotent) {
|
||||
cbm_mem_init(0.5);
|
||||
|
||||
/* Multiple collect() calls must be idempotent and not crash */
|
||||
cbm_mem_collect();
|
||||
cbm_mem_collect();
|
||||
cbm_mem_collect();
|
||||
|
||||
size_t rss = cbm_mem_rss();
|
||||
ASSERT_GT(rss, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Init edge cases ──────────────────────────────────────────── */
|
||||
/* NOTE: cbm_mem_init uses atomic CAS — only the very first call in the
|
||||
* process takes effect. Since mem_rss_tracking runs first with 0.5,
|
||||
* all subsequent init calls are no-ops. We verify that they don't
|
||||
* crash and that the budget remains unchanged. */
|
||||
|
||||
TEST(mem_init_zero_fraction) {
|
||||
/* First init already happened with 0.5 — this is a no-op */
|
||||
size_t budget_before = cbm_mem_budget();
|
||||
cbm_mem_init(0.0);
|
||||
size_t budget_after = cbm_mem_budget();
|
||||
/* Budget must not change (second init is no-op) */
|
||||
ASSERT_EQ(budget_before, budget_after);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mem_init_negative_fraction) {
|
||||
size_t budget_before = cbm_mem_budget();
|
||||
cbm_mem_init(-1.0);
|
||||
size_t budget_after = cbm_mem_budget();
|
||||
ASSERT_EQ(budget_before, budget_after);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mem_init_over_one_fraction) {
|
||||
size_t budget_before = cbm_mem_budget();
|
||||
cbm_mem_init(1.5);
|
||||
size_t budget_after = cbm_mem_budget();
|
||||
ASSERT_EQ(budget_before, budget_after);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(mem_init_second_call_noop) {
|
||||
size_t budget_before = cbm_mem_budget();
|
||||
cbm_mem_init(0.9); /* different fraction — but it's a no-op */
|
||||
size_t budget_after = cbm_mem_budget();
|
||||
ASSERT_EQ(budget_before, budget_after);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Arena integration tests ──────────────────────────────────── */
|
||||
|
||||
TEST(arena_alloc_and_destroy) {
|
||||
@@ -436,6 +633,27 @@ SUITE(mem) {
|
||||
RUN_TEST(mem_rss_tracking);
|
||||
RUN_TEST(mem_collect_reclaims);
|
||||
RUN_TEST(mem_budget_check);
|
||||
/* Budget edge cases */
|
||||
RUN_TEST(mem_worker_budget_zero_workers);
|
||||
RUN_TEST(mem_worker_budget_negative_workers);
|
||||
RUN_TEST(mem_worker_budget_one_worker);
|
||||
RUN_TEST(mem_worker_budget_many_workers);
|
||||
RUN_TEST(mem_over_budget_low_rss);
|
||||
/* RSS tracking */
|
||||
RUN_TEST(mem_rss_positive);
|
||||
RUN_TEST(mem_peak_rss_gte_rss);
|
||||
RUN_TEST(mem_rss_increases_after_alloc);
|
||||
RUN_TEST(mem_collect_no_crash);
|
||||
RUN_TEST(mem_collect_rss_still_positive);
|
||||
/* Memory pressure simulation */
|
||||
RUN_TEST(mem_progressive_alloc_rss_increases);
|
||||
RUN_TEST(mem_free_and_collect_no_crash);
|
||||
RUN_TEST(mem_multiple_collect_idempotent);
|
||||
/* Init edge cases */
|
||||
RUN_TEST(mem_init_zero_fraction);
|
||||
RUN_TEST(mem_init_negative_fraction);
|
||||
RUN_TEST(mem_init_over_one_fraction);
|
||||
RUN_TEST(mem_init_second_call_noop);
|
||||
/* Arena integration */
|
||||
RUN_TEST(arena_alloc_and_destroy);
|
||||
RUN_TEST(arena_grow_tracks_sizes);
|
||||
|
||||
@@ -5291,6 +5291,437 @@ TEST(pipeline_lock_release_allows_contender) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Resource management & internal helper tests ─────────────────── */
|
||||
|
||||
TEST(pipeline_empty_path) {
|
||||
/* Empty string repo path — should handle gracefully */
|
||||
cbm_pipeline_t *p = cbm_pipeline_new("", NULL, CBM_MODE_FULL);
|
||||
/* Implementation may return NULL or a valid pipeline with empty project name.
|
||||
* Either behavior is acceptable — the key is no crash. */
|
||||
if (p) {
|
||||
cbm_pipeline_free(p);
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_project_name_content) {
|
||||
/* Verify project name is derived from the repo_path */
|
||||
cbm_pipeline_t *p = cbm_pipeline_new("/home/user/my-project", NULL, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
const char *name = cbm_pipeline_project_name(p);
|
||||
ASSERT_NOT_NULL(name);
|
||||
ASSERT_TRUE(strlen(name) > 0);
|
||||
/* Should contain "my-project" as part of the derived name */
|
||||
ASSERT_TRUE(strstr(name, "my-project") != NULL);
|
||||
cbm_pipeline_free(p);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_cancel_sets_flag) {
|
||||
/* Verify cancel sets the flag so subsequent run exits early */
|
||||
cbm_pipeline_t *p = cbm_pipeline_new("/tmp/nonexistent", NULL, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
/* Cancel before run */
|
||||
cbm_pipeline_cancel(p);
|
||||
/* Cancelled pipeline should return quickly (either -1 from cancel or from
|
||||
* missing path — both are acceptable; key is no hang) */
|
||||
int rc = cbm_pipeline_run(p);
|
||||
ASSERT_EQ(rc, -1);
|
||||
cbm_pipeline_free(p);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_double_cancel) {
|
||||
/* Calling cancel twice should not crash */
|
||||
cbm_pipeline_t *p = cbm_pipeline_new("/tmp/nonexistent", NULL, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
cbm_pipeline_cancel(p);
|
||||
cbm_pipeline_cancel(p);
|
||||
cbm_pipeline_free(p);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(pipeline_double_free_prevention) {
|
||||
/* free(NULL) after free should not crash. We can't truly double-free
|
||||
* the same pointer, but we verify NULL is safe as documented. */
|
||||
cbm_pipeline_free(NULL);
|
||||
cbm_pipeline_free(NULL);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(trackable_source_files) {
|
||||
/* Common source extensions are trackable */
|
||||
ASSERT_TRUE(cbm_is_trackable_file("main.go"));
|
||||
ASSERT_TRUE(cbm_is_trackable_file("src/handler.py"));
|
||||
ASSERT_TRUE(cbm_is_trackable_file("lib/server.js"));
|
||||
ASSERT_TRUE(cbm_is_trackable_file("components/App.tsx"));
|
||||
ASSERT_TRUE(cbm_is_trackable_file("pkg/svc.rs"));
|
||||
ASSERT_TRUE(cbm_is_trackable_file("config.yaml"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(trackable_rejects_images) {
|
||||
/* Image and binary files are not trackable */
|
||||
ASSERT_FALSE(cbm_is_trackable_file("logo.png"));
|
||||
ASSERT_FALSE(cbm_is_trackable_file("photo.jpg"));
|
||||
ASSERT_FALSE(cbm_is_trackable_file("icon.gif"));
|
||||
ASSERT_FALSE(cbm_is_trackable_file("diagram.svg"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(trackable_rejects_minified) {
|
||||
/* Minified files are not trackable */
|
||||
ASSERT_FALSE(cbm_is_trackable_file("bundle.min.js"));
|
||||
ASSERT_FALSE(cbm_is_trackable_file("style.min.css"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(trackable_rejects_vendor_dirs) {
|
||||
/* Files in vendor/node_modules/__pycache__ are not trackable */
|
||||
ASSERT_FALSE(cbm_is_trackable_file("vendor/github.com/lib/dep.go"));
|
||||
ASSERT_FALSE(cbm_is_trackable_file("node_modules/lodash/index.js"));
|
||||
ASSERT_FALSE(cbm_is_trackable_file("__pycache__/module.pyc"));
|
||||
ASSERT_FALSE(cbm_is_trackable_file(".git/objects/pack/data"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(trackable_rejects_lock_files) {
|
||||
/* Lock files are not trackable */
|
||||
ASSERT_FALSE(cbm_is_trackable_file("package-lock.json"));
|
||||
ASSERT_FALSE(cbm_is_trackable_file("go.sum"));
|
||||
ASSERT_FALSE(cbm_is_trackable_file("yarn.lock"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(test_path_directory_patterns) {
|
||||
/* Test directory patterns: __tests__, test/, tests/, spec/ */
|
||||
ASSERT_TRUE(cbm_is_test_path("__tests__/Component.js"));
|
||||
ASSERT_TRUE(cbm_is_test_path("tests/test_main.py"));
|
||||
ASSERT_TRUE(cbm_is_test_path("spec/handler_spec.rb"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(test_path_suffix_patterns) {
|
||||
/* Various language-specific test suffix patterns */
|
||||
ASSERT_TRUE(cbm_is_test_path("handler.spec.ts"));
|
||||
ASSERT_TRUE(cbm_is_test_path("handler.test.tsx"));
|
||||
ASSERT_TRUE(cbm_is_test_path("handler_test.go"));
|
||||
ASSERT_TRUE(cbm_is_test_path("test_handler.py"));
|
||||
/* Non-test files */
|
||||
ASSERT_FALSE(cbm_is_test_path("handler.ts"));
|
||||
ASSERT_FALSE(cbm_is_test_path("contest.go"));
|
||||
ASSERT_FALSE(cbm_is_test_path("latest.py"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(test_func_name_go_patterns) {
|
||||
/* Go test function names: Test + uppercase char */
|
||||
ASSERT_TRUE(cbm_is_test_func_name("TestFoo"));
|
||||
ASSERT_TRUE(cbm_is_test_func_name("TestHTTPHandler"));
|
||||
/* Non-test: "Test" alone or Test + lowercase */
|
||||
ASSERT_FALSE(cbm_is_test_func_name("Testable")); /* lowercase 'a' after Test */
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(test_func_name_js_helpers) {
|
||||
/* JS/TS test helper function names */
|
||||
ASSERT_TRUE(cbm_is_test_func_name("it"));
|
||||
ASSERT_TRUE(cbm_is_test_func_name("describe"));
|
||||
ASSERT_TRUE(cbm_is_test_func_name("test"));
|
||||
ASSERT_TRUE(cbm_is_test_func_name("beforeEach"));
|
||||
ASSERT_TRUE(cbm_is_test_func_name("afterEach"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(env_var_name_valid) {
|
||||
/* Valid env var names: uppercase + underscores, at least 2 chars with uppercase */
|
||||
ASSERT_TRUE(cbm_is_env_var_name("DATABASE_URL"));
|
||||
ASSERT_TRUE(cbm_is_env_var_name("API_KEY"));
|
||||
ASSERT_TRUE(cbm_is_env_var_name("PORT"));
|
||||
ASSERT_TRUE(cbm_is_env_var_name("DB_2"));
|
||||
ASSERT_TRUE(cbm_is_env_var_name("MY_VAR_123"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(env_var_name_invalid) {
|
||||
/* Invalid: too short, lowercase, mixed case, empty */
|
||||
ASSERT_FALSE(cbm_is_env_var_name("A")); /* single char */
|
||||
ASSERT_FALSE(cbm_is_env_var_name("port")); /* lowercase */
|
||||
ASSERT_FALSE(cbm_is_env_var_name("apiKey")); /* camelCase */
|
||||
ASSERT_FALSE(cbm_is_env_var_name("__")); /* no uppercase */
|
||||
ASSERT_FALSE(cbm_is_env_var_name("")); /* empty */
|
||||
ASSERT_FALSE(cbm_is_env_var_name("123")); /* digits only */
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(config_ext_positive) {
|
||||
/* Config file extensions recognized */
|
||||
ASSERT_TRUE(cbm_has_config_extension(".env"));
|
||||
ASSERT_TRUE(cbm_has_config_extension("config.yaml"));
|
||||
ASSERT_TRUE(cbm_has_config_extension("config.yml"));
|
||||
ASSERT_TRUE(cbm_has_config_extension("settings.toml"));
|
||||
ASSERT_TRUE(cbm_has_config_extension("app.ini"));
|
||||
ASSERT_TRUE(cbm_has_config_extension("data.json"));
|
||||
ASSERT_TRUE(cbm_has_config_extension("app.properties"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(config_ext_negative) {
|
||||
/* Non-config extensions rejected */
|
||||
ASSERT_FALSE(cbm_has_config_extension("main.go"));
|
||||
ASSERT_FALSE(cbm_has_config_extension("app.py"));
|
||||
ASSERT_FALSE(cbm_has_config_extension("handler.rs"));
|
||||
ASSERT_FALSE(cbm_has_config_extension("data.csv"));
|
||||
ASSERT_FALSE(cbm_has_config_extension("README.md"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(split_camel_basic) {
|
||||
/* Basic camelCase splitting */
|
||||
char *parts[16];
|
||||
int n;
|
||||
|
||||
n = cbm_split_camel_case("getCamelCase", parts, 16);
|
||||
ASSERT_EQ(n, 3);
|
||||
ASSERT_STR_EQ(parts[0], "get");
|
||||
ASSERT_STR_EQ(parts[1], "Camel");
|
||||
ASSERT_STR_EQ(parts[2], "Case");
|
||||
for (int i = 0; i < n; i++) free(parts[i]);
|
||||
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(split_camel_single_word) {
|
||||
/* Single lowercase word — no splits */
|
||||
char *parts[16];
|
||||
int n = cbm_split_camel_case("hello", parts, 16);
|
||||
ASSERT_EQ(n, 1);
|
||||
ASSERT_STR_EQ(parts[0], "hello");
|
||||
for (int i = 0; i < n; i++) free(parts[i]);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(split_camel_empty) {
|
||||
/* Empty string — should return 0 or 1 empty part */
|
||||
char *parts[16];
|
||||
int n = cbm_split_camel_case("", parts, 16);
|
||||
for (int i = 0; i < n; i++) free(parts[i]);
|
||||
/* Either 0 parts or 1 empty part is acceptable */
|
||||
ASSERT_TRUE(n >= 0 && n <= 1);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tokenize_decorator_login_required) {
|
||||
/* @login_required → ["login", "required"] */
|
||||
char *tokens[16];
|
||||
int n = cbm_tokenize_decorator("@login_required", tokens, 16);
|
||||
ASSERT_EQ(n, 2);
|
||||
ASSERT_STR_EQ(tokens[0], "login");
|
||||
ASSERT_STR_EQ(tokens[1], "required");
|
||||
for (int i = 0; i < n; i++) free(tokens[i]);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(tokenize_decorator_single) {
|
||||
/* @Override → ["override"] */
|
||||
char *tokens[16];
|
||||
int n = cbm_tokenize_decorator("@Override", tokens, 16);
|
||||
ASSERT_EQ(n, 1);
|
||||
ASSERT_STR_EQ(tokens[0], "override");
|
||||
for (int i = 0; i < n; i++) free(tokens[i]);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(split_command_basic) {
|
||||
/* Basic command splitting with spaces */
|
||||
char *args[16];
|
||||
int n = cbm_split_command("gcc -c main.c -o main.o", args, 16);
|
||||
ASSERT_EQ(n, 5);
|
||||
ASSERT_STR_EQ(args[0], "gcc");
|
||||
ASSERT_STR_EQ(args[1], "-c");
|
||||
ASSERT_STR_EQ(args[2], "main.c");
|
||||
ASSERT_STR_EQ(args[3], "-o");
|
||||
ASSERT_STR_EQ(args[4], "main.o");
|
||||
for (int i = 0; i < n; i++) free(args[i]);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(split_command_quoted) {
|
||||
/* Command with quoted arguments */
|
||||
char *args[16];
|
||||
int n = cbm_split_command("gcc -DFOO=\"bar baz\" main.c", args, 16);
|
||||
ASSERT_GTE(n, 3);
|
||||
ASSERT_STR_EQ(args[0], "gcc");
|
||||
for (int i = 0; i < n; i++) free(args[i]);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(split_command_empty) {
|
||||
/* Empty command string */
|
||||
char *args[16];
|
||||
int n = cbm_split_command("", args, 16);
|
||||
ASSERT_EQ(n, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parse_range_with_count) {
|
||||
/* "10,5" → start=10, count=5 */
|
||||
int start, count;
|
||||
cbm_parse_range("10,5", &start, &count);
|
||||
ASSERT_EQ(start, 10);
|
||||
ASSERT_EQ(count, 5);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parse_range_single) {
|
||||
/* "42" → start=42, count=1 */
|
||||
int start, count;
|
||||
cbm_parse_range("42", &start, &count);
|
||||
ASSERT_EQ(start, 42);
|
||||
ASSERT_EQ(count, 1);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parse_range_zero_count) {
|
||||
/* "100,0" → start=100, count=0 */
|
||||
int start, count;
|
||||
cbm_parse_range("100,0", &start, &count);
|
||||
ASSERT_EQ(start, 100);
|
||||
ASSERT_EQ(count, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parse_name_status_basic) {
|
||||
/* Parse standard git diff --name-status output */
|
||||
const char *input = "M\tsrc/main.go\n"
|
||||
"A\tsrc/new.go\n"
|
||||
"D\tsrc/old.go\n";
|
||||
cbm_changed_file_t files[16];
|
||||
int n = cbm_parse_name_status(input, files, 16);
|
||||
/* Exact count depends on which files pass cbm_is_trackable_file */
|
||||
ASSERT_GTE(n, 0);
|
||||
ASSERT_LTE(n, 3);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parse_name_status_empty) {
|
||||
/* Empty input */
|
||||
cbm_changed_file_t files[16];
|
||||
int n = cbm_parse_name_status("", files, 16);
|
||||
ASSERT_EQ(n, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parse_hunks_empty) {
|
||||
/* Empty input */
|
||||
cbm_changed_hunk_t hunks[16];
|
||||
int n = cbm_parse_hunks("", hunks, 16);
|
||||
ASSERT_EQ(n, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(coupling_empty_commits) {
|
||||
/* Zero commits → zero couplings */
|
||||
cbm_change_coupling_t results[16];
|
||||
int n = cbm_compute_change_coupling(NULL, 0, results, 16);
|
||||
ASSERT_EQ(n, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(coupling_single_file_commit) {
|
||||
/* Commits with single files → no pairs → zero couplings */
|
||||
char *f1[] = {"a.go"};
|
||||
char *f2[] = {"b.go"};
|
||||
cbm_commit_files_t commits[] = {{f1, 1}, {f2, 1}};
|
||||
cbm_change_coupling_t results[16];
|
||||
int n = cbm_compute_change_coupling(commits, 2, results, 16);
|
||||
ASSERT_EQ(n, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(clean_json_brackets_array) {
|
||||
/* JSON array → space-joined */
|
||||
char out[256];
|
||||
cbm_clean_json_brackets("[\"./app\", \"--flag\"]", out, sizeof(out));
|
||||
ASSERT_TRUE(strstr(out, "./app") != NULL);
|
||||
ASSERT_TRUE(strstr(out, "--flag") != NULL);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(clean_json_brackets_plain) {
|
||||
/* Plain string → unchanged */
|
||||
char out[256];
|
||||
cbm_clean_json_brackets("./app --flag", out, sizeof(out));
|
||||
ASSERT_STR_EQ(out, "./app --flag");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(clean_json_brackets_empty) {
|
||||
/* Empty string → empty output */
|
||||
char out[256];
|
||||
cbm_clean_json_brackets("", out, sizeof(out));
|
||||
ASSERT_STR_EQ(out, "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_basic) {
|
||||
/* Basic FQN: project.dir.name */
|
||||
char *fqn = cbm_pipeline_fqn_compute("proj", "pkg/handler.go", "Serve");
|
||||
ASSERT_NOT_NULL(fqn);
|
||||
ASSERT_TRUE(strstr(fqn, "proj") != NULL);
|
||||
ASSERT_TRUE(strstr(fqn, "Serve") != NULL);
|
||||
free(fqn);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_compute_strips_ext) {
|
||||
/* FQN should strip file extension */
|
||||
char *fqn = cbm_pipeline_fqn_compute("proj", "main.go", "main");
|
||||
ASSERT_NOT_NULL(fqn);
|
||||
/* Should not contain ".go" */
|
||||
ASSERT_TRUE(strstr(fqn, ".go") == NULL);
|
||||
ASSERT_TRUE(strstr(fqn, "main") != NULL);
|
||||
free(fqn);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_module_basic) {
|
||||
/* Module QN: project.dir.parts */
|
||||
char *mod = cbm_pipeline_fqn_module("proj", "pkg/util/helper.go");
|
||||
ASSERT_NOT_NULL(mod);
|
||||
ASSERT_TRUE(strstr(mod, "proj") != NULL);
|
||||
free(mod);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fqn_folder_basic) {
|
||||
/* Folder QN from directory path */
|
||||
char *folder = cbm_pipeline_fqn_folder("proj", "pkg/util");
|
||||
ASSERT_NOT_NULL(folder);
|
||||
ASSERT_TRUE(strstr(folder, "proj") != NULL);
|
||||
free(folder);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_special_chars) {
|
||||
/* Project name with colons, slashes — should be normalized */
|
||||
char *name = cbm_project_name_from_path("/home/user/my:project");
|
||||
ASSERT_NOT_NULL(name);
|
||||
/* Colons should be converted to dashes */
|
||||
ASSERT_TRUE(strchr(name, ':') == NULL);
|
||||
ASSERT_TRUE(strchr(name, '/') == NULL);
|
||||
free(name);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(project_name_trailing_slash) {
|
||||
/* Trailing slash should not affect result */
|
||||
char *a = cbm_project_name_from_path("/home/user/project");
|
||||
ASSERT_NOT_NULL(a);
|
||||
free(a);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(pipeline) {
|
||||
/* Index lock */
|
||||
RUN_TEST(pipeline_lock_try_acquire);
|
||||
@@ -5488,4 +5919,63 @@ SUITE(pipeline) {
|
||||
RUN_TEST(incremental_new_file_added);
|
||||
RUN_TEST(incremental_k8s_manifest_indexed);
|
||||
RUN_TEST(incremental_kustomize_module_indexed);
|
||||
/* Resource management & internal helper tests */
|
||||
RUN_TEST(pipeline_empty_path);
|
||||
RUN_TEST(pipeline_project_name_content);
|
||||
RUN_TEST(pipeline_cancel_sets_flag);
|
||||
RUN_TEST(pipeline_double_cancel);
|
||||
RUN_TEST(pipeline_double_free_prevention);
|
||||
/* Trackable file tests */
|
||||
RUN_TEST(trackable_source_files);
|
||||
RUN_TEST(trackable_rejects_images);
|
||||
RUN_TEST(trackable_rejects_minified);
|
||||
RUN_TEST(trackable_rejects_vendor_dirs);
|
||||
RUN_TEST(trackable_rejects_lock_files);
|
||||
/* Test path detection */
|
||||
RUN_TEST(test_path_directory_patterns);
|
||||
RUN_TEST(test_path_suffix_patterns);
|
||||
/* Test function name detection */
|
||||
RUN_TEST(test_func_name_go_patterns);
|
||||
RUN_TEST(test_func_name_js_helpers);
|
||||
/* Env var name detection */
|
||||
RUN_TEST(env_var_name_valid);
|
||||
RUN_TEST(env_var_name_invalid);
|
||||
/* Config extension detection */
|
||||
RUN_TEST(config_ext_positive);
|
||||
RUN_TEST(config_ext_negative);
|
||||
/* Camel case splitting */
|
||||
RUN_TEST(split_camel_basic);
|
||||
RUN_TEST(split_camel_single_word);
|
||||
RUN_TEST(split_camel_empty);
|
||||
/* Decorator tokenization */
|
||||
RUN_TEST(tokenize_decorator_login_required);
|
||||
RUN_TEST(tokenize_decorator_single);
|
||||
/* Command splitting */
|
||||
RUN_TEST(split_command_basic);
|
||||
RUN_TEST(split_command_quoted);
|
||||
RUN_TEST(split_command_empty);
|
||||
/* Range parsing */
|
||||
RUN_TEST(parse_range_with_count);
|
||||
RUN_TEST(parse_range_single);
|
||||
RUN_TEST(parse_range_zero_count);
|
||||
/* Name status parsing */
|
||||
RUN_TEST(parse_name_status_basic);
|
||||
RUN_TEST(parse_name_status_empty);
|
||||
/* Hunk parsing */
|
||||
RUN_TEST(parse_hunks_empty);
|
||||
/* Change coupling */
|
||||
RUN_TEST(coupling_empty_commits);
|
||||
RUN_TEST(coupling_single_file_commit);
|
||||
/* JSON bracket cleaning */
|
||||
RUN_TEST(clean_json_brackets_array);
|
||||
RUN_TEST(clean_json_brackets_plain);
|
||||
RUN_TEST(clean_json_brackets_empty);
|
||||
/* FQN computation */
|
||||
RUN_TEST(fqn_compute_basic);
|
||||
RUN_TEST(fqn_compute_strips_ext);
|
||||
RUN_TEST(fqn_module_basic);
|
||||
RUN_TEST(fqn_folder_basic);
|
||||
/* Project name edge cases */
|
||||
RUN_TEST(project_name_special_chars);
|
||||
RUN_TEST(project_name_trailing_slash);
|
||||
}
|
||||
@@ -294,6 +294,290 @@ TEST(store_edge_cascade_on_node_delete) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Batch insert with count=0 (non-NULL array) ────────────────── */
|
||||
|
||||
TEST(store_edge_batch_insert_zero_count) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
cbm_edge_t dummy = {.project = "test", .source_id = 1, .target_id = 2, .type = "CALLS"};
|
||||
int rc = cbm_store_insert_edge_batch(s, &dummy, 0);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
|
||||
int ecnt = cbm_store_count_edges(s, "test");
|
||||
ASSERT_EQ(ecnt, 0);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Batch insert stress: 50 edges ─────────────────────────────── */
|
||||
|
||||
TEST(store_edge_batch_insert_50) {
|
||||
/* Create 51 nodes so we can have 50 A→B edges with distinct targets */
|
||||
int64_t ids[51];
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
for (int i = 0; i < 51; i++) {
|
||||
char name[8], qn[32];
|
||||
snprintf(name, sizeof(name), "N%d", i);
|
||||
snprintf(qn, sizeof(qn), "test.N%d", i);
|
||||
cbm_node_t node = {
|
||||
.project = "test", .label = "Function", .name = name, .qualified_name = qn};
|
||||
ids[i] = cbm_store_upsert_node(s, &node);
|
||||
}
|
||||
|
||||
/* 50 edges: N0→N1, N1→N2, ..., N49→N50 */
|
||||
cbm_edge_t edges[50];
|
||||
for (int i = 0; i < 50; i++) {
|
||||
edges[i] = (cbm_edge_t){
|
||||
.project = "test", .source_id = ids[i], .target_id = ids[i + 1], .type = "CALLS"};
|
||||
}
|
||||
|
||||
int rc = cbm_store_insert_edge_batch(s, edges, 50);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
|
||||
int ecnt = cbm_store_count_edges(s, "test");
|
||||
ASSERT_EQ(ecnt, 50);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── find_edges_by_source with non-existent source ─────────────── */
|
||||
|
||||
TEST(store_edge_find_source_nonexistent) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
cbm_edge_t *edges = NULL;
|
||||
int count = 0;
|
||||
int rc = cbm_store_find_edges_by_source(s, 999999, &edges, &count);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_EQ(count, 0);
|
||||
cbm_store_free_edges(edges, count);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── find_edges_by_target with non-existent target ─────────────── */
|
||||
|
||||
TEST(store_edge_find_target_nonexistent) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
cbm_edge_t *edges = NULL;
|
||||
int count = 0;
|
||||
int rc = cbm_store_find_edges_by_target(s, 999999, &edges, &count);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_EQ(count, 0);
|
||||
cbm_store_free_edges(edges, count);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── find_edges_by_type for non-existent type ──────────────────── */
|
||||
|
||||
TEST(store_edge_find_type_nonexistent) {
|
||||
int64_t ids[2];
|
||||
cbm_store_t *s = setup_store_with_nodes(2, ids);
|
||||
|
||||
cbm_edge_t e = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
||||
cbm_store_insert_edge(s, &e);
|
||||
|
||||
cbm_edge_t *edges = NULL;
|
||||
int count = 0;
|
||||
int rc = cbm_store_find_edges_by_type(s, "test", "DOES_NOT_EXIST", &edges, &count);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_EQ(count, 0);
|
||||
cbm_store_free_edges(edges, count);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── count_edges on empty project ──────────────────────────────── */
|
||||
|
||||
TEST(store_edge_count_empty_project) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "empty", "/tmp/empty");
|
||||
|
||||
int ecnt = cbm_store_count_edges(s, "empty");
|
||||
ASSERT_EQ(ecnt, 0);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── count_edges_by_type for missing type ──────────────────────── */
|
||||
|
||||
TEST(store_edge_count_by_type_missing) {
|
||||
int64_t ids[2];
|
||||
cbm_store_t *s = setup_store_with_nodes(2, ids);
|
||||
|
||||
cbm_edge_t e = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
||||
cbm_store_insert_edge(s, &e);
|
||||
|
||||
int cnt = cbm_store_count_edges_by_type(s, "test", "NEVER_EXISTS");
|
||||
ASSERT_EQ(cnt, 0);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── delete_edges_by_type preserves other types ────────────────── */
|
||||
|
||||
TEST(store_edge_delete_by_type_preserves_others) {
|
||||
int64_t ids[3];
|
||||
cbm_store_t *s = setup_store_with_nodes(3, ids);
|
||||
|
||||
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[0], .target_id = ids[2], .type = "IMPORTS"};
|
||||
cbm_edge_t e3 = {
|
||||
.project = "test", .source_id = ids[1], .target_id = ids[2], .type = "HTTP_CALLS"};
|
||||
cbm_store_insert_edge(s, &e1);
|
||||
cbm_store_insert_edge(s, &e2);
|
||||
cbm_store_insert_edge(s, &e3);
|
||||
ASSERT_EQ(cbm_store_count_edges(s, "test"), 3);
|
||||
|
||||
/* Delete only CALLS */
|
||||
cbm_store_delete_edges_by_type(s, "test", "CALLS");
|
||||
ASSERT_EQ(cbm_store_count_edges(s, "test"), 2);
|
||||
|
||||
/* IMPORTS and HTTP_CALLS still exist */
|
||||
ASSERT_EQ(cbm_store_count_edges_by_type(s, "test", "IMPORTS"), 1);
|
||||
ASSERT_EQ(cbm_store_count_edges_by_type(s, "test", "HTTP_CALLS"), 1);
|
||||
ASSERT_EQ(cbm_store_count_edges_by_type(s, "test", "CALLS"), 0);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── delete_edges_by_project preserves other projects ──────────── */
|
||||
|
||||
TEST(store_edge_delete_by_project_preserves_others) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "alpha", "/tmp/alpha");
|
||||
cbm_store_upsert_project(s, "beta", "/tmp/beta");
|
||||
|
||||
/* Create nodes in both projects */
|
||||
cbm_node_t na = {
|
||||
.project = "alpha", .label = "Function", .name = "A", .qualified_name = "alpha.A"};
|
||||
cbm_node_t nb = {
|
||||
.project = "alpha", .label = "Function", .name = "B", .qualified_name = "alpha.B"};
|
||||
cbm_node_t nc = {
|
||||
.project = "beta", .label = "Function", .name = "C", .qualified_name = "beta.C"};
|
||||
cbm_node_t nd = {
|
||||
.project = "beta", .label = "Function", .name = "D", .qualified_name = "beta.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 = "alpha", .source_id = idA, .target_id = idB, .type = "CALLS"};
|
||||
cbm_edge_t e2 = {
|
||||
.project = "beta", .source_id = idC, .target_id = idD, .type = "CALLS"};
|
||||
cbm_store_insert_edge(s, &e1);
|
||||
cbm_store_insert_edge(s, &e2);
|
||||
|
||||
ASSERT_EQ(cbm_store_count_edges(s, "alpha"), 1);
|
||||
ASSERT_EQ(cbm_store_count_edges(s, "beta"), 1);
|
||||
|
||||
/* Delete alpha edges only */
|
||||
cbm_store_delete_edges_by_project(s, "alpha");
|
||||
ASSERT_EQ(cbm_store_count_edges(s, "alpha"), 0);
|
||||
ASSERT_EQ(cbm_store_count_edges(s, "beta"), 1);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge with special chars in properties_json ────────────────── */
|
||||
|
||||
TEST(store_edge_properties_special_chars) {
|
||||
int64_t ids[2];
|
||||
cbm_store_t *s = setup_store_with_nodes(2, ids);
|
||||
|
||||
/* JSON with unicode, quotes, backslashes */
|
||||
const char *json = "{\"desc\":\"line1\\nline2\",\"path\":\"/api/v1/users?q=foo&bar=baz\","
|
||||
"\"emoji\":\"\\u2603\"}";
|
||||
cbm_edge_t e = {.project = "test",
|
||||
.source_id = ids[0],
|
||||
.target_id = ids[1],
|
||||
.type = "HTTP_CALLS",
|
||||
.properties_json = json};
|
||||
cbm_store_insert_edge(s, &e);
|
||||
|
||||
cbm_edge_t *edges = NULL;
|
||||
int count = 0;
|
||||
cbm_store_find_edges_by_source(s, ids[0], &edges, &count);
|
||||
ASSERT_EQ(count, 1);
|
||||
ASSERT_NOT_NULL(edges[0].properties_json);
|
||||
/* Round-trip should preserve the JSON string */
|
||||
ASSERT(strstr(edges[0].properties_json, "line1\\nline2") != NULL);
|
||||
ASSERT(strstr(edges[0].properties_json, "/api/v1/users") != NULL);
|
||||
cbm_store_free_edges(edges, count);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge with very long type string ───────────────────────────── */
|
||||
|
||||
TEST(store_edge_long_type_string) {
|
||||
int64_t ids[2];
|
||||
cbm_store_t *s = setup_store_with_nodes(2, ids);
|
||||
|
||||
/* 200-char type string */
|
||||
char long_type[201];
|
||||
memset(long_type, 'X', 200);
|
||||
long_type[200] = '\0';
|
||||
|
||||
cbm_edge_t e = {.project = "test",
|
||||
.source_id = ids[0],
|
||||
.target_id = ids[1],
|
||||
.type = long_type};
|
||||
int64_t eid = cbm_store_insert_edge(s, &e);
|
||||
ASSERT_GT(eid, 0);
|
||||
|
||||
/* Verify it round-trips */
|
||||
cbm_edge_t *edges = NULL;
|
||||
int count = 0;
|
||||
int rc = cbm_store_find_edges_by_source(s, ids[0], &edges, &count);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_EQ(count, 1);
|
||||
ASSERT_STR_EQ(edges[0].type, long_type);
|
||||
cbm_store_free_edges(edges, count);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── find_edges_by_source_type with non-existent type ──────────── */
|
||||
|
||||
TEST(store_edge_find_source_type_nonexistent) {
|
||||
int64_t ids[2];
|
||||
cbm_store_t *s = setup_store_with_nodes(2, ids);
|
||||
|
||||
cbm_edge_t e = {.project = "test", .source_id = ids[0], .target_id = ids[1], .type = "CALLS"};
|
||||
cbm_store_insert_edge(s, &e);
|
||||
|
||||
cbm_edge_t *edges = NULL;
|
||||
int count = 0;
|
||||
int rc = cbm_store_find_edges_by_source_type(s, ids[0], "NOPE", &edges, &count);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_EQ(count, 0);
|
||||
cbm_store_free_edges(edges, count);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(store_edges) {
|
||||
RUN_TEST(store_edge_insert_find);
|
||||
RUN_TEST(store_edge_dedup);
|
||||
@@ -307,4 +591,17 @@ SUITE(store_edges) {
|
||||
RUN_TEST(store_edge_batch_insert);
|
||||
RUN_TEST(store_edge_batch_empty);
|
||||
RUN_TEST(store_edge_cascade_on_node_delete);
|
||||
/* Edge case tests */
|
||||
RUN_TEST(store_edge_batch_insert_zero_count);
|
||||
RUN_TEST(store_edge_batch_insert_50);
|
||||
RUN_TEST(store_edge_find_source_nonexistent);
|
||||
RUN_TEST(store_edge_find_target_nonexistent);
|
||||
RUN_TEST(store_edge_find_type_nonexistent);
|
||||
RUN_TEST(store_edge_count_empty_project);
|
||||
RUN_TEST(store_edge_count_by_type_missing);
|
||||
RUN_TEST(store_edge_delete_by_type_preserves_others);
|
||||
RUN_TEST(store_edge_delete_by_project_preserves_others);
|
||||
RUN_TEST(store_edge_properties_special_chars);
|
||||
RUN_TEST(store_edge_long_type_string);
|
||||
RUN_TEST(store_edge_find_source_type_nonexistent);
|
||||
}
|
||||
@@ -956,6 +956,549 @@ TEST(store_integrity_null_check) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge case: NULL / empty field handling ────────────────────── */
|
||||
|
||||
TEST(store_node_null_project) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
/* Upsert with NULL project — should fail gracefully */
|
||||
cbm_node_t n = {.project = NULL,
|
||||
.label = "Function",
|
||||
.name = "Foo",
|
||||
.qualified_name = "null.Foo"};
|
||||
int64_t id = cbm_store_upsert_node(s, &n);
|
||||
/* Either returns error or silently succeeds; must not crash */
|
||||
(void)id;
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_node_null_qn) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
/* Upsert with NULL qualified_name */
|
||||
cbm_node_t n = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "Bar",
|
||||
.qualified_name = NULL};
|
||||
int64_t id = cbm_store_upsert_node(s, &n);
|
||||
/* Must not crash regardless of return value */
|
||||
(void)id;
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_node_empty_strings) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
/* Upsert with all fields as empty strings */
|
||||
cbm_node_t n = {.project = "test",
|
||||
.label = "",
|
||||
.name = "",
|
||||
.qualified_name = "",
|
||||
.file_path = "",
|
||||
.start_line = 0,
|
||||
.end_line = 0,
|
||||
.properties_json = ""};
|
||||
int64_t id = cbm_store_upsert_node(s, &n);
|
||||
/* Should succeed — empty strings are valid */
|
||||
ASSERT_GT(id, 0);
|
||||
|
||||
cbm_node_t found = {0};
|
||||
int rc = cbm_store_find_node_by_qn(s, "test", "", &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_STR_EQ(found.name, "");
|
||||
ASSERT_STR_EQ(found.label, "");
|
||||
cbm_node_free_fields(&found);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge case: not-found lookups ──────────────────────────────── */
|
||||
|
||||
TEST(store_find_by_id_not_found) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
cbm_node_t found = {0};
|
||||
int rc = cbm_store_find_node_by_id(s, 999999, &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_NOT_FOUND);
|
||||
|
||||
/* Negative ID should also be not found */
|
||||
rc = cbm_store_find_node_by_id(s, -1, &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_NOT_FOUND);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_find_by_qn_not_found) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
/* Insert a node so the store is non-empty */
|
||||
cbm_node_t n = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "Exists",
|
||||
.qualified_name = "test.Exists"};
|
||||
cbm_store_upsert_node(s, &n);
|
||||
|
||||
/* Search for a non-existent QN */
|
||||
cbm_node_t found = {0};
|
||||
int rc = cbm_store_find_node_by_qn(s, "test", "test.DoesNotExist", &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_NOT_FOUND);
|
||||
|
||||
/* Wrong project */
|
||||
rc = cbm_store_find_node_by_qn(s, "other-project", "test.Exists", &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_NOT_FOUND);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge case: cross-project lookups ──────────────────────────── */
|
||||
|
||||
TEST(store_find_by_qn_any_cross_project) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "proj-a", "/tmp/a");
|
||||
cbm_store_upsert_project(s, "proj-b", "/tmp/b");
|
||||
|
||||
cbm_node_t na = {.project = "proj-a",
|
||||
.label = "Function",
|
||||
.name = "SharedFunc",
|
||||
.qualified_name = "proj-a.main.SharedFunc"};
|
||||
cbm_node_t nb = {.project = "proj-b",
|
||||
.label = "Class",
|
||||
.name = "Widget",
|
||||
.qualified_name = "proj-b.pkg.Widget"};
|
||||
cbm_store_upsert_node(s, &na);
|
||||
cbm_store_upsert_node(s, &nb);
|
||||
|
||||
/* find_node_by_qn_any finds without project filter */
|
||||
cbm_node_t found = {0};
|
||||
int rc = cbm_store_find_node_by_qn_any(s, "proj-a.main.SharedFunc", &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_STR_EQ(found.name, "SharedFunc");
|
||||
ASSERT_STR_EQ(found.project, "proj-a");
|
||||
cbm_node_free_fields(&found);
|
||||
|
||||
rc = cbm_store_find_node_by_qn_any(s, "proj-b.pkg.Widget", &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_STR_EQ(found.name, "Widget");
|
||||
ASSERT_STR_EQ(found.project, "proj-b");
|
||||
cbm_node_free_fields(&found);
|
||||
|
||||
/* Non-existent QN */
|
||||
rc = cbm_store_find_node_by_qn_any(s, "nonexistent.Nope", &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_NOT_FOUND);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_find_by_name_any_cross_project) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "proj-a", "/tmp/a");
|
||||
cbm_store_upsert_project(s, "proj-b", "/tmp/b");
|
||||
|
||||
/* Same name in two projects */
|
||||
cbm_node_t na = {.project = "proj-a",
|
||||
.label = "Function",
|
||||
.name = "Init",
|
||||
.qualified_name = "proj-a.main.Init"};
|
||||
cbm_node_t nb = {.project = "proj-b",
|
||||
.label = "Function",
|
||||
.name = "Init",
|
||||
.qualified_name = "proj-b.main.Init"};
|
||||
cbm_node_t nc = {.project = "proj-b",
|
||||
.label = "Function",
|
||||
.name = "Other",
|
||||
.qualified_name = "proj-b.main.Other"};
|
||||
cbm_store_upsert_node(s, &na);
|
||||
cbm_store_upsert_node(s, &nb);
|
||||
cbm_store_upsert_node(s, &nc);
|
||||
|
||||
/* find_nodes_by_name_any should find both "Init" across projects */
|
||||
cbm_node_t *nodes = NULL;
|
||||
int count = 0;
|
||||
int rc = cbm_store_find_nodes_by_name_any(s, "Init", &nodes, &count);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_EQ(count, 2);
|
||||
cbm_store_free_nodes(nodes, count);
|
||||
|
||||
/* Name that doesn't exist */
|
||||
rc = cbm_store_find_nodes_by_name_any(s, "Nonexistent", &nodes, &count);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_EQ(count, 0);
|
||||
cbm_store_free_nodes(nodes, count);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge case: empty result sets ──────────────────────────────── */
|
||||
|
||||
TEST(store_find_by_file_no_match) {
|
||||
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 = "Foo",
|
||||
.qualified_name = "test.Foo",
|
||||
.file_path = "main.go"};
|
||||
cbm_store_upsert_node(s, &n);
|
||||
|
||||
/* Search for a file that has no nodes */
|
||||
cbm_node_t *nodes = NULL;
|
||||
int count = 0;
|
||||
int rc = cbm_store_find_nodes_by_file(s, "test", "nonexistent.go", &nodes, &count);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_EQ(count, 0);
|
||||
cbm_store_free_nodes(nodes, count);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge case: batch upsert boundary ─────────────────────────── */
|
||||
|
||||
TEST(store_node_batch_upsert_zero) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
/* Batch upsert with count=0 should succeed, do nothing */
|
||||
int rc = cbm_store_upsert_node_batch(s, NULL, 0, NULL);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
|
||||
int cnt = cbm_store_count_nodes(s, "test");
|
||||
ASSERT_EQ(cnt, 0);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_node_batch_upsert_100) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
cbm_node_t nodes[100];
|
||||
int64_t ids[100];
|
||||
char names[100][32];
|
||||
char qns[100][64];
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
snprintf(names[i], sizeof(names[i]), "stress_%d", i);
|
||||
snprintf(qns[i], sizeof(qns[i]), "test.stress.stress_%d", i);
|
||||
nodes[i] = (cbm_node_t){.project = "test",
|
||||
.label = "Function",
|
||||
.name = names[i],
|
||||
.qualified_name = qns[i],
|
||||
.file_path = "stress.go",
|
||||
.start_line = i,
|
||||
.end_line = i + 1};
|
||||
}
|
||||
|
||||
int rc = cbm_store_upsert_node_batch(s, nodes, 100, ids);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
|
||||
/* All IDs should be positive */
|
||||
for (int i = 0; i < 100; i++)
|
||||
ASSERT_GT(ids[i], 0);
|
||||
|
||||
/* IDs should all be unique */
|
||||
for (int i = 0; i < 100; i++)
|
||||
for (int j = i + 1; j < 100; j++)
|
||||
ASSERT_NEQ(ids[i], ids[j]);
|
||||
|
||||
int cnt = cbm_store_count_nodes(s, "test");
|
||||
ASSERT_EQ(cnt, 100);
|
||||
|
||||
/* Verify a few random lookups */
|
||||
cbm_node_t found = {0};
|
||||
rc = cbm_store_find_node_by_qn(s, "test", "test.stress.stress_0", &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_STR_EQ(found.name, "stress_0");
|
||||
cbm_node_free_fields(&found);
|
||||
|
||||
rc = cbm_store_find_node_by_qn(s, "test", "test.stress.stress_99", &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_STR_EQ(found.name, "stress_99");
|
||||
cbm_node_free_fields(&found);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge case: delete then verify remaining ──────────────────── */
|
||||
|
||||
TEST(store_delete_by_label_verify_remaining) {
|
||||
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 = "FuncA",
|
||||
.qualified_name = "test.FuncA"};
|
||||
cbm_node_t n2 = {.project = "test",
|
||||
.label = "Class",
|
||||
.name = "ClassB",
|
||||
.qualified_name = "test.ClassB"};
|
||||
cbm_node_t n3 = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "FuncC",
|
||||
.qualified_name = "test.FuncC"};
|
||||
cbm_node_t n4 = {.project = "test",
|
||||
.label = "Method",
|
||||
.name = "MethodD",
|
||||
.qualified_name = "test.MethodD"};
|
||||
cbm_store_upsert_node(s, &n1);
|
||||
cbm_store_upsert_node(s, &n2);
|
||||
cbm_store_upsert_node(s, &n3);
|
||||
cbm_store_upsert_node(s, &n4);
|
||||
|
||||
/* Delete all Functions */
|
||||
int rc = cbm_store_delete_nodes_by_label(s, "test", "Function");
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
|
||||
int cnt = cbm_store_count_nodes(s, "test");
|
||||
ASSERT_EQ(cnt, 2);
|
||||
|
||||
/* Class and Method should remain */
|
||||
cbm_node_t found = {0};
|
||||
rc = cbm_store_find_node_by_qn(s, "test", "test.ClassB", &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_STR_EQ(found.label, "Class");
|
||||
cbm_node_free_fields(&found);
|
||||
|
||||
rc = cbm_store_find_node_by_qn(s, "test", "test.MethodD", &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_STR_EQ(found.label, "Method");
|
||||
cbm_node_free_fields(&found);
|
||||
|
||||
/* Deleted ones should be gone */
|
||||
rc = cbm_store_find_node_by_qn(s, "test", "test.FuncA", &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_NOT_FOUND);
|
||||
rc = cbm_store_find_node_by_qn(s, "test", "test.FuncC", &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_NOT_FOUND);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(store_delete_by_file_verify_remaining) {
|
||||
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 = "A",
|
||||
.qualified_name = "test.A",
|
||||
.file_path = "delete_me.go"};
|
||||
cbm_node_t n2 = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "B",
|
||||
.qualified_name = "test.B",
|
||||
.file_path = "keep_me.go"};
|
||||
cbm_node_t n3 = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "C",
|
||||
.qualified_name = "test.C",
|
||||
.file_path = "delete_me.go"};
|
||||
cbm_store_upsert_node(s, &n1);
|
||||
cbm_store_upsert_node(s, &n2);
|
||||
cbm_store_upsert_node(s, &n3);
|
||||
|
||||
int rc = cbm_store_delete_nodes_by_file(s, "test", "delete_me.go");
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
|
||||
int cnt = cbm_store_count_nodes(s, "test");
|
||||
ASSERT_EQ(cnt, 1);
|
||||
|
||||
/* Only keep_me.go node should remain */
|
||||
cbm_node_t found = {0};
|
||||
rc = cbm_store_find_node_by_qn(s, "test", "test.B", &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_STR_EQ(found.file_path, "keep_me.go");
|
||||
cbm_node_free_fields(&found);
|
||||
|
||||
/* Deleted nodes should be gone */
|
||||
rc = cbm_store_find_node_by_qn(s, "test", "test.A", &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_NOT_FOUND);
|
||||
rc = cbm_store_find_node_by_qn(s, "test", "test.C", &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_NOT_FOUND);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge case: upsert dedup with field changes ───────────────── */
|
||||
|
||||
TEST(store_node_upsert_updates_fields) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
/* Insert initial node */
|
||||
cbm_node_t n1 = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "MyFunc",
|
||||
.qualified_name = "test.MyFunc",
|
||||
.file_path = "old.go",
|
||||
.start_line = 1,
|
||||
.end_line = 10,
|
||||
.properties_json = "{\"version\":1}"};
|
||||
int64_t id1 = cbm_store_upsert_node(s, &n1);
|
||||
ASSERT_GT(id1, 0);
|
||||
|
||||
/* Upsert same QN with changed fields */
|
||||
cbm_node_t n2 = {.project = "test",
|
||||
.label = "Method",
|
||||
.name = "MyFunc",
|
||||
.qualified_name = "test.MyFunc",
|
||||
.file_path = "new.go",
|
||||
.start_line = 50,
|
||||
.end_line = 60,
|
||||
.properties_json = "{\"version\":2}"};
|
||||
int64_t id2 = cbm_store_upsert_node(s, &n2);
|
||||
ASSERT_EQ(id1, id2); /* Same ID — updated, not duplicated */
|
||||
|
||||
/* Count should still be 1 */
|
||||
int cnt = cbm_store_count_nodes(s, "test");
|
||||
ASSERT_EQ(cnt, 1);
|
||||
|
||||
/* Verify fields were updated */
|
||||
cbm_node_t found = {0};
|
||||
int rc = cbm_store_find_node_by_id(s, id1, &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_STR_EQ(found.label, "Method");
|
||||
ASSERT_STR_EQ(found.file_path, "new.go");
|
||||
ASSERT_EQ(found.start_line, 50);
|
||||
ASSERT_EQ(found.end_line, 60);
|
||||
ASSERT(strstr(found.properties_json, "version") != NULL);
|
||||
ASSERT(strstr(found.properties_json, "2") != NULL);
|
||||
cbm_node_free_fields(&found);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge case: long qualified name ───────────────────────────── */
|
||||
|
||||
TEST(store_node_long_qn) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
/* Build a 1200-char qualified name */
|
||||
char long_qn[1201];
|
||||
memset(long_qn, 'a', 1200);
|
||||
long_qn[0] = 't'; /* make it look like a dotted path */
|
||||
for (int i = 50; i < 1200; i += 50)
|
||||
long_qn[i] = '.';
|
||||
long_qn[1200] = '\0';
|
||||
|
||||
cbm_node_t n = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "LongName",
|
||||
.qualified_name = long_qn,
|
||||
.file_path = "big.go"};
|
||||
int64_t id = cbm_store_upsert_node(s, &n);
|
||||
ASSERT_GT(id, 0);
|
||||
|
||||
/* Should be retrievable by QN */
|
||||
cbm_node_t found = {0};
|
||||
int rc = cbm_store_find_node_by_qn(s, "test", long_qn, &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_STR_EQ(found.qualified_name, long_qn);
|
||||
ASSERT_STR_EQ(found.name, "LongName");
|
||||
cbm_node_free_fields(&found);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge case: properties JSON with special characters ────────── */
|
||||
|
||||
TEST(store_node_properties_special_chars) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
/* JSON with quotes, backslashes, unicode, newlines */
|
||||
const char *props =
|
||||
"{\"desc\":\"line1\\nline2\","
|
||||
"\"path\":\"C:\\\\Users\\\\test\","
|
||||
"\"emoji\":\"\\u2603\","
|
||||
"\"nested\":{\"key\":\"val with \\\"quotes\\\"\"}}";
|
||||
|
||||
cbm_node_t n = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "SpecialFunc",
|
||||
.qualified_name = "test.SpecialFunc",
|
||||
.properties_json = props};
|
||||
int64_t id = cbm_store_upsert_node(s, &n);
|
||||
ASSERT_GT(id, 0);
|
||||
|
||||
cbm_node_t found = {0};
|
||||
int rc = cbm_store_find_node_by_id(s, id, &found);
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_NOT_NULL(found.properties_json);
|
||||
/* Round-trip should preserve the JSON exactly */
|
||||
ASSERT_STR_EQ(found.properties_json, props);
|
||||
cbm_node_free_fields(&found);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge case: delete from non-existent project/file ─────────── */
|
||||
|
||||
TEST(store_delete_nodes_nonexistent) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
|
||||
/* Insert one node */
|
||||
cbm_node_t n = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "Survivor",
|
||||
.qualified_name = "test.Survivor",
|
||||
.file_path = "main.go"};
|
||||
cbm_store_upsert_node(s, &n);
|
||||
|
||||
/* Delete by non-existent file — should succeed but delete nothing */
|
||||
int rc = cbm_store_delete_nodes_by_file(s, "test", "ghost.go");
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_EQ(cbm_store_count_nodes(s, "test"), 1);
|
||||
|
||||
/* Delete by non-existent label */
|
||||
rc = cbm_store_delete_nodes_by_label(s, "test", "Interface");
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_EQ(cbm_store_count_nodes(s, "test"), 1);
|
||||
|
||||
/* Delete by non-existent project */
|
||||
rc = cbm_store_delete_nodes_by_project(s, "no-such-project");
|
||||
ASSERT_EQ(rc, CBM_STORE_OK);
|
||||
ASSERT_EQ(cbm_store_count_nodes(s, "test"), 1);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge case: count nodes for unknown project ───────────────── */
|
||||
|
||||
TEST(store_count_nodes_unknown_project) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
/* No project created — count should be 0 */
|
||||
int cnt = cbm_store_count_nodes(s, "ghost-project");
|
||||
ASSERT_EQ(cnt, 0);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(store_nodes) {
|
||||
RUN_TEST(store_open_memory);
|
||||
RUN_TEST(store_close_null);
|
||||
@@ -993,4 +1536,21 @@ SUITE(store_nodes) {
|
||||
RUN_TEST(store_restore_from);
|
||||
RUN_TEST(store_pragma_settings);
|
||||
RUN_TEST(store_find_node_ids_by_qns);
|
||||
RUN_TEST(store_node_null_project);
|
||||
RUN_TEST(store_node_null_qn);
|
||||
RUN_TEST(store_node_empty_strings);
|
||||
RUN_TEST(store_find_by_id_not_found);
|
||||
RUN_TEST(store_find_by_qn_not_found);
|
||||
RUN_TEST(store_find_by_qn_any_cross_project);
|
||||
RUN_TEST(store_find_by_name_any_cross_project);
|
||||
RUN_TEST(store_find_by_file_no_match);
|
||||
RUN_TEST(store_node_batch_upsert_zero);
|
||||
RUN_TEST(store_node_batch_upsert_100);
|
||||
RUN_TEST(store_delete_by_label_verify_remaining);
|
||||
RUN_TEST(store_delete_by_file_verify_remaining);
|
||||
RUN_TEST(store_node_upsert_updates_fields);
|
||||
RUN_TEST(store_node_long_qn);
|
||||
RUN_TEST(store_node_properties_special_chars);
|
||||
RUN_TEST(store_delete_nodes_nonexistent);
|
||||
RUN_TEST(store_count_nodes_unknown_project);
|
||||
}
|
||||
@@ -948,6 +948,266 @@ TEST(store_batch_count_degrees) {
|
||||
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);
|
||||
@@ -977,4 +1237,36 @@ SUITE(store_search) {
|
||||
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);
|
||||
}
|
||||
@@ -110,6 +110,151 @@ TEST(intern_survives_stack_buffer) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Edge case tests ───────────────────────────────────────────── */
|
||||
|
||||
TEST(intern_null_returns_null) {
|
||||
CBMInternPool *pool = cbm_intern_create();
|
||||
const char *s = cbm_intern(pool, NULL);
|
||||
ASSERT_NULL(s);
|
||||
ASSERT_EQ(cbm_intern_count(pool), 0);
|
||||
cbm_intern_free(pool);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(intern_n_zero_len) {
|
||||
/* intern_n with len=0 should produce an empty string */
|
||||
CBMInternPool *pool = cbm_intern_create();
|
||||
const char *s = cbm_intern_n(pool, "anything", 0);
|
||||
ASSERT_NOT_NULL(s);
|
||||
ASSERT_STR_EQ(s, "");
|
||||
ASSERT_EQ(cbm_intern_count(pool), 1);
|
||||
/* Should dedup with intern("") */
|
||||
const char *s2 = cbm_intern(pool, "");
|
||||
ASSERT_EQ((uintptr_t)s, (uintptr_t)s2);
|
||||
ASSERT_EQ(cbm_intern_count(pool), 1);
|
||||
cbm_intern_free(pool);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(intern_very_long_string) {
|
||||
CBMInternPool *pool = cbm_intern_create();
|
||||
/* 2000 char string */
|
||||
char buf[2001];
|
||||
memset(buf, 'A', 2000);
|
||||
buf[2000] = '\0';
|
||||
const char *s1 = cbm_intern(pool, buf);
|
||||
ASSERT_NOT_NULL(s1);
|
||||
ASSERT_EQ(strlen(s1), 2000);
|
||||
ASSERT_STR_EQ(s1, buf);
|
||||
/* Dedup works for long strings too */
|
||||
const char *s2 = cbm_intern(pool, buf);
|
||||
ASSERT_EQ((uintptr_t)s1, (uintptr_t)s2);
|
||||
ASSERT_EQ(cbm_intern_count(pool), 1);
|
||||
cbm_intern_free(pool);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(intern_same_prefix_different_lengths) {
|
||||
/* intern_n of same buffer with different lengths produces different strings */
|
||||
CBMInternPool *pool = cbm_intern_create();
|
||||
const char *src = "abcdefgh";
|
||||
const char *s3 = cbm_intern_n(pool, src, 3); /* "abc" */
|
||||
const char *s5 = cbm_intern_n(pool, src, 5); /* "abcde" */
|
||||
const char *s8 = cbm_intern_n(pool, src, 8); /* "abcdefgh" */
|
||||
ASSERT_STR_EQ(s3, "abc");
|
||||
ASSERT_STR_EQ(s5, "abcde");
|
||||
ASSERT_STR_EQ(s8, "abcdefgh");
|
||||
/* All different pointers */
|
||||
ASSERT_NEQ((uintptr_t)s3, (uintptr_t)s5);
|
||||
ASSERT_NEQ((uintptr_t)s5, (uintptr_t)s8);
|
||||
ASSERT_NEQ((uintptr_t)s3, (uintptr_t)s8);
|
||||
ASSERT_EQ(cbm_intern_count(pool), 3);
|
||||
cbm_intern_free(pool);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(intern_pointer_stability) {
|
||||
/* Intern a string, then intern 10000 more, original pointer must survive */
|
||||
CBMInternPool *pool = cbm_intern_create();
|
||||
const char *original = cbm_intern(pool, "sentinel_value");
|
||||
ASSERT_NOT_NULL(original);
|
||||
|
||||
/* Force many hash table resizes */
|
||||
char buf[64];
|
||||
for (int i = 0; i < 10000; i++) {
|
||||
snprintf(buf, sizeof(buf), "filler_%06d", i);
|
||||
cbm_intern(pool, buf);
|
||||
}
|
||||
ASSERT_EQ(cbm_intern_count(pool), 10001);
|
||||
|
||||
/* Original pointer must still be valid and unchanged */
|
||||
ASSERT_STR_EQ(original, "sentinel_value");
|
||||
/* Re-interning must return the exact same pointer */
|
||||
const char *again = cbm_intern(pool, "sentinel_value");
|
||||
ASSERT_EQ((uintptr_t)original, (uintptr_t)again);
|
||||
cbm_intern_free(pool);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(intern_two_pools_independent) {
|
||||
/* Two pools must return different pointers for the same string */
|
||||
CBMInternPool *p1 = cbm_intern_create();
|
||||
CBMInternPool *p2 = cbm_intern_create();
|
||||
const char *s1 = cbm_intern(p1, "shared");
|
||||
const char *s2 = cbm_intern(p2, "shared");
|
||||
ASSERT_STR_EQ(s1, "shared");
|
||||
ASSERT_STR_EQ(s2, "shared");
|
||||
/* Different pools own different copies — different pointers */
|
||||
ASSERT_NEQ((uintptr_t)s1, (uintptr_t)s2);
|
||||
cbm_intern_free(p1);
|
||||
cbm_intern_free(p2);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(intern_free_empty_pool) {
|
||||
/* Free a pool with no strings interned — should not crash */
|
||||
CBMInternPool *pool = cbm_intern_create();
|
||||
ASSERT_EQ(cbm_intern_count(pool), 0);
|
||||
ASSERT_EQ(cbm_intern_bytes(pool), 0);
|
||||
cbm_intern_free(pool);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(intern_bytes_tracking) {
|
||||
CBMInternPool *pool = cbm_intern_create();
|
||||
ASSERT_EQ(cbm_intern_bytes(pool), 0);
|
||||
|
||||
cbm_intern(pool, "abc"); /* 3 bytes */
|
||||
ASSERT_EQ(cbm_intern_bytes(pool), 3);
|
||||
|
||||
cbm_intern(pool, "defgh"); /* 5 bytes */
|
||||
ASSERT_EQ(cbm_intern_bytes(pool), 8);
|
||||
|
||||
/* Duplicate should not add bytes */
|
||||
cbm_intern(pool, "abc");
|
||||
ASSERT_EQ(cbm_intern_bytes(pool), 8);
|
||||
|
||||
cbm_intern(pool, ""); /* 0 bytes */
|
||||
ASSERT_EQ(cbm_intern_bytes(pool), 8);
|
||||
|
||||
cbm_intern(pool, "x"); /* 1 byte */
|
||||
ASSERT_EQ(cbm_intern_bytes(pool), 9);
|
||||
|
||||
cbm_intern_free(pool);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(intern_free_null_pool) {
|
||||
/* Free NULL pool — should not crash */
|
||||
cbm_intern_free(NULL);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(intern_count_null_pool) {
|
||||
ASSERT_EQ(cbm_intern_count(NULL), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(str_intern) {
|
||||
RUN_TEST(intern_create_free);
|
||||
RUN_TEST(intern_basic);
|
||||
@@ -120,4 +265,15 @@ SUITE(str_intern) {
|
||||
RUN_TEST(intern_many_strings);
|
||||
RUN_TEST(intern_bytes);
|
||||
RUN_TEST(intern_survives_stack_buffer);
|
||||
/* Edge cases */
|
||||
RUN_TEST(intern_null_returns_null);
|
||||
RUN_TEST(intern_n_zero_len);
|
||||
RUN_TEST(intern_very_long_string);
|
||||
RUN_TEST(intern_same_prefix_different_lengths);
|
||||
RUN_TEST(intern_pointer_stability);
|
||||
RUN_TEST(intern_two_pools_independent);
|
||||
RUN_TEST(intern_free_empty_pool);
|
||||
RUN_TEST(intern_bytes_tracking);
|
||||
RUN_TEST(intern_free_null_pool);
|
||||
RUN_TEST(intern_count_null_pool);
|
||||
}
|
||||
@@ -162,6 +162,307 @@ TEST(str_split_no_delim) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── NULL safety tests ─────────────────────────────────────────── */
|
||||
|
||||
TEST(path_join_null_base) {
|
||||
setup();
|
||||
ASSERT_NULL(cbm_path_join(&a, NULL, "main.c"));
|
||||
teardown();
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(path_join_null_name) {
|
||||
setup();
|
||||
ASSERT_NULL(cbm_path_join(&a, "src", NULL));
|
||||
teardown();
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(path_join_both_null) {
|
||||
setup();
|
||||
ASSERT_NULL(cbm_path_join(&a, NULL, NULL));
|
||||
teardown();
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(path_dir_null) {
|
||||
setup();
|
||||
ASSERT_STR_EQ(cbm_path_dir(&a, NULL), ".");
|
||||
teardown();
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(str_starts_with_null_s) {
|
||||
ASSERT_FALSE(cbm_str_starts_with(NULL, "hello"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(str_starts_with_null_prefix) {
|
||||
ASSERT_FALSE(cbm_str_starts_with("hello", NULL));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(str_ends_with_null) {
|
||||
ASSERT_FALSE(cbm_str_ends_with(NULL, "world"));
|
||||
ASSERT_FALSE(cbm_str_ends_with("world", NULL));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(str_contains_null) {
|
||||
ASSERT_FALSE(cbm_str_contains(NULL, "sub"));
|
||||
ASSERT_FALSE(cbm_str_contains("hello", NULL));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(str_tolower_null) {
|
||||
setup();
|
||||
ASSERT_NULL(cbm_str_tolower(&a, NULL));
|
||||
teardown();
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(str_replace_char_null) {
|
||||
setup();
|
||||
ASSERT_NULL(cbm_str_replace_char(&a, NULL, 'a', 'b'));
|
||||
teardown();
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(str_strip_ext_null) {
|
||||
setup();
|
||||
ASSERT_NULL(cbm_str_strip_ext(&a, NULL));
|
||||
teardown();
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(str_split_null_string) {
|
||||
setup();
|
||||
int count = 0;
|
||||
ASSERT_NULL(cbm_str_split(&a, NULL, '/', &count));
|
||||
teardown();
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(str_split_null_out_count) {
|
||||
setup();
|
||||
ASSERT_NULL(cbm_str_split(&a, "a/b", '/', NULL));
|
||||
teardown();
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── path_join edge cases ─────────────────────────────────────── */
|
||||
|
||||
TEST(path_join_multi_slashes) {
|
||||
setup();
|
||||
ASSERT_STR_EQ(cbm_path_join(&a, "src///", "///main.c"), "src/main.c");
|
||||
teardown();
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(path_join_only_slash_base) {
|
||||
setup();
|
||||
ASSERT_STR_EQ(cbm_path_join(&a, "/", "main.c"), "main.c");
|
||||
teardown();
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(path_join_long_paths) {
|
||||
setup();
|
||||
char long_base[512];
|
||||
char long_name[512];
|
||||
memset(long_base, 'a', 511);
|
||||
long_base[511] = '\0';
|
||||
memset(long_name, 'b', 511);
|
||||
long_name[511] = '\0';
|
||||
char *result = cbm_path_join(&a, long_base, long_name);
|
||||
ASSERT_NOT_NULL(result);
|
||||
ASSERT_TRUE(cbm_str_starts_with(result, "aaa"));
|
||||
ASSERT_TRUE(cbm_str_ends_with(result, "bbb"));
|
||||
ASSERT_TRUE(cbm_str_contains(result, "/"));
|
||||
ASSERT_EQ(strlen(result), 511 + 1 + 511);
|
||||
teardown();
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(path_join_n_zero) {
|
||||
setup();
|
||||
ASSERT_STR_EQ(cbm_path_join_n(&a, NULL, 0), "");
|
||||
teardown();
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(path_join_n_null_parts) {
|
||||
setup();
|
||||
ASSERT_STR_EQ(cbm_path_join_n(&a, NULL, 3), "");
|
||||
teardown();
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── path_ext edge cases ──────────────────────────────────────── */
|
||||
|
||||
TEST(path_ext_null) {
|
||||
ASSERT_STR_EQ(cbm_path_ext(NULL), "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(path_ext_dot_directory) {
|
||||
ASSERT_STR_EQ(cbm_path_ext("dir.name/file"), "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(path_ext_hidden_file) {
|
||||
ASSERT_STR_EQ(cbm_path_ext(".gitignore"), "gitignore");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(path_ext_no_extension) {
|
||||
ASSERT_STR_EQ(cbm_path_ext("Makefile"), "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(path_ext_multiple_dots) {
|
||||
ASSERT_STR_EQ(cbm_path_ext("file.test.spec.ts"), "ts");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(path_ext_trailing_dot) {
|
||||
ASSERT_STR_EQ(cbm_path_ext("file."), "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── path_base edge cases ─────────────────────────────────────── */
|
||||
|
||||
TEST(path_base_null) {
|
||||
ASSERT_STR_EQ(cbm_path_base(NULL), "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(path_base_empty) {
|
||||
ASSERT_STR_EQ(cbm_path_base(""), "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(path_base_just_filename) {
|
||||
ASSERT_STR_EQ(cbm_path_base("hello.txt"), "hello.txt");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(path_base_trailing_slash) {
|
||||
ASSERT_STR_EQ(cbm_path_base("dir/"), "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── validate_shell_arg tests ─────────────────────────────────── */
|
||||
|
||||
TEST(validate_shell_arg_null) {
|
||||
ASSERT_FALSE(cbm_validate_shell_arg(NULL));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(validate_shell_arg_safe) {
|
||||
ASSERT_TRUE(cbm_validate_shell_arg("hello-world_123"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(validate_shell_arg_single_quote) {
|
||||
ASSERT_FALSE(cbm_validate_shell_arg("it's bad"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(validate_shell_arg_semicolon) {
|
||||
ASSERT_FALSE(cbm_validate_shell_arg("cmd; rm -rf"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(validate_shell_arg_pipe) {
|
||||
ASSERT_FALSE(cbm_validate_shell_arg("cmd | evil"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(validate_shell_arg_ampersand) {
|
||||
ASSERT_FALSE(cbm_validate_shell_arg("cmd & bg"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(validate_shell_arg_dollar) {
|
||||
ASSERT_FALSE(cbm_validate_shell_arg("$HOME"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(validate_shell_arg_backtick) {
|
||||
ASSERT_FALSE(cbm_validate_shell_arg("`whoami`"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(validate_shell_arg_newline) {
|
||||
ASSERT_FALSE(cbm_validate_shell_arg("line1\nline2"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(validate_shell_arg_carriage_return) {
|
||||
ASSERT_FALSE(cbm_validate_shell_arg("line1\rline2"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(validate_shell_arg_backslash) {
|
||||
ASSERT_FALSE(cbm_validate_shell_arg("path\\to\\file"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(validate_shell_arg_empty) {
|
||||
ASSERT_TRUE(cbm_validate_shell_arg(""));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(validate_shell_arg_spaces) {
|
||||
ASSERT_TRUE(cbm_validate_shell_arg("hello world with spaces"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── SNPRINTF_APPEND tests ────────────────────────────────────── */
|
||||
|
||||
TEST(snprintf_append_basic) {
|
||||
char buf[64];
|
||||
int off = 0;
|
||||
CBM_SNPRINTF_APPEND(buf, sizeof(buf), off, "hello");
|
||||
CBM_SNPRINTF_APPEND(buf, sizeof(buf), off, " world");
|
||||
ASSERT_STR_EQ(buf, "hello world");
|
||||
ASSERT_EQ(off, 11);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(snprintf_append_fills_exactly) {
|
||||
char buf[6];
|
||||
int off = 0;
|
||||
CBM_SNPRINTF_APPEND(buf, sizeof(buf), off, "hello");
|
||||
ASSERT_STR_EQ(buf, "hello");
|
||||
ASSERT_EQ(off, 5);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(snprintf_append_overflow) {
|
||||
char buf[8];
|
||||
int off = 0;
|
||||
CBM_SNPRINTF_APPEND(buf, sizeof(buf), off, "hello world this is way too long");
|
||||
/* offset clamped to sizeof(buf) - 1 */
|
||||
ASSERT_EQ(off, 7);
|
||||
/* buffer is null-terminated and truncated */
|
||||
ASSERT_EQ(strlen(buf), 7);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(snprintf_append_multiple_sequential) {
|
||||
char buf[32];
|
||||
int off = 0;
|
||||
CBM_SNPRINTF_APPEND(buf, sizeof(buf), off, "a=%d", 1);
|
||||
CBM_SNPRINTF_APPEND(buf, sizeof(buf), off, " b=%d", 2);
|
||||
CBM_SNPRINTF_APPEND(buf, sizeof(buf), off, " c=%d", 3);
|
||||
CBM_SNPRINTF_APPEND(buf, sizeof(buf), off, " d=%d", 4);
|
||||
ASSERT_STR_EQ(buf, "a=1 b=2 c=3 d=4");
|
||||
ASSERT_EQ(off, 15);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(str_util) {
|
||||
RUN_TEST(path_join_basic);
|
||||
RUN_TEST(path_join_trailing_slash);
|
||||
@@ -181,4 +482,55 @@ SUITE(str_util) {
|
||||
RUN_TEST(str_split);
|
||||
RUN_TEST(str_split_empty);
|
||||
RUN_TEST(str_split_no_delim);
|
||||
/* NULL safety */
|
||||
RUN_TEST(path_join_null_base);
|
||||
RUN_TEST(path_join_null_name);
|
||||
RUN_TEST(path_join_both_null);
|
||||
RUN_TEST(path_dir_null);
|
||||
RUN_TEST(str_starts_with_null_s);
|
||||
RUN_TEST(str_starts_with_null_prefix);
|
||||
RUN_TEST(str_ends_with_null);
|
||||
RUN_TEST(str_contains_null);
|
||||
RUN_TEST(str_tolower_null);
|
||||
RUN_TEST(str_replace_char_null);
|
||||
RUN_TEST(str_strip_ext_null);
|
||||
RUN_TEST(str_split_null_string);
|
||||
RUN_TEST(str_split_null_out_count);
|
||||
/* path_join edge cases */
|
||||
RUN_TEST(path_join_multi_slashes);
|
||||
RUN_TEST(path_join_only_slash_base);
|
||||
RUN_TEST(path_join_long_paths);
|
||||
RUN_TEST(path_join_n_zero);
|
||||
RUN_TEST(path_join_n_null_parts);
|
||||
/* path_ext edge cases */
|
||||
RUN_TEST(path_ext_null);
|
||||
RUN_TEST(path_ext_dot_directory);
|
||||
RUN_TEST(path_ext_hidden_file);
|
||||
RUN_TEST(path_ext_no_extension);
|
||||
RUN_TEST(path_ext_multiple_dots);
|
||||
RUN_TEST(path_ext_trailing_dot);
|
||||
/* path_base edge cases */
|
||||
RUN_TEST(path_base_null);
|
||||
RUN_TEST(path_base_empty);
|
||||
RUN_TEST(path_base_just_filename);
|
||||
RUN_TEST(path_base_trailing_slash);
|
||||
/* validate_shell_arg */
|
||||
RUN_TEST(validate_shell_arg_null);
|
||||
RUN_TEST(validate_shell_arg_safe);
|
||||
RUN_TEST(validate_shell_arg_single_quote);
|
||||
RUN_TEST(validate_shell_arg_semicolon);
|
||||
RUN_TEST(validate_shell_arg_pipe);
|
||||
RUN_TEST(validate_shell_arg_ampersand);
|
||||
RUN_TEST(validate_shell_arg_dollar);
|
||||
RUN_TEST(validate_shell_arg_backtick);
|
||||
RUN_TEST(validate_shell_arg_newline);
|
||||
RUN_TEST(validate_shell_arg_carriage_return);
|
||||
RUN_TEST(validate_shell_arg_backslash);
|
||||
RUN_TEST(validate_shell_arg_empty);
|
||||
RUN_TEST(validate_shell_arg_spaces);
|
||||
/* SNPRINTF_APPEND */
|
||||
RUN_TEST(snprintf_append_basic);
|
||||
RUN_TEST(snprintf_append_fills_exactly);
|
||||
RUN_TEST(snprintf_append_overflow);
|
||||
RUN_TEST(snprintf_append_multiple_sequential);
|
||||
}
|
||||
@@ -105,6 +105,28 @@ TEST(traces_extract_http_info_url_full) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── TestExtractServiceName — edge cases ───────────────────────── */
|
||||
|
||||
TEST(traces_extract_service_name_empty_attrs) {
|
||||
cbm_trace_resource_t r = {.attributes = NULL, .attr_count = 0};
|
||||
const char *name = cbm_extract_service_name(&r);
|
||||
ASSERT_STR_EQ(name, "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(traces_extract_service_name_multiple_attrs) {
|
||||
/* service.name is present but not the first attribute */
|
||||
cbm_trace_attr_t attrs[] = {
|
||||
{.key = "deployment.environment", .string_value = "production"},
|
||||
{.key = "telemetry.sdk.language", .string_value = "go"},
|
||||
{.key = "service.name", .string_value = "payment-gateway"},
|
||||
};
|
||||
cbm_trace_resource_t r = {.attributes = attrs, .attr_count = 3};
|
||||
const char *name = cbm_extract_service_name(&r);
|
||||
ASSERT_STR_EQ(name, "payment-gateway");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── TestExtractPathFromURL ─────────────────────────────────────── */
|
||||
|
||||
TEST(traces_extract_path_from_url) {
|
||||
@@ -125,6 +147,54 @@ TEST(traces_extract_path_from_url) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── TestExtractPathFromURL — edge cases ──────────────────────── */
|
||||
|
||||
TEST(traces_extract_path_null_url) {
|
||||
char buf[256] = "untouched";
|
||||
const char *result = cbm_extract_path_from_url(NULL, buf, sizeof(buf));
|
||||
ASSERT_STR_EQ(result, "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(traces_extract_path_empty_url) {
|
||||
char buf[256];
|
||||
cbm_extract_path_from_url("", buf, sizeof(buf));
|
||||
ASSERT_STR_EQ(buf, "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(traces_extract_path_with_port) {
|
||||
char buf[256];
|
||||
cbm_extract_path_from_url("https://api.example.com:8443/v2/users", buf, sizeof(buf));
|
||||
ASSERT_STR_EQ(buf, "/v2/users");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(traces_extract_path_with_query_and_fragment) {
|
||||
char buf[256];
|
||||
/* Query params should be stripped (stops at '?') */
|
||||
cbm_extract_path_from_url("https://example.com/search?q=test&page=2", buf, sizeof(buf));
|
||||
ASSERT_STR_EQ(buf, "/search");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(traces_extract_path_buffer_too_small) {
|
||||
char buf[4]; /* only 4 bytes — path "/api/orders" gets truncated */
|
||||
cbm_extract_path_from_url("https://example.com/api/orders", buf, sizeof(buf));
|
||||
/* Should truncate safely to 3 chars + NUL */
|
||||
ASSERT_EQ(strlen(buf), 3);
|
||||
ASSERT_STR_EQ(buf, "/ap");
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(traces_extract_path_relative_path) {
|
||||
/* No scheme — "relative/path" has fewer than 3 slashes → empty */
|
||||
char buf[256];
|
||||
cbm_extract_path_from_url("/api/orders", buf, sizeof(buf));
|
||||
ASSERT_STR_EQ(buf, "");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── TestCalculateP99 ───────────────────────────────────────────── */
|
||||
|
||||
TEST(traces_calculate_p99) {
|
||||
@@ -161,17 +231,114 @@ TEST(traces_parse_duration_zero) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── TestParseDuration — edge cases ──────────────────────────────── */
|
||||
|
||||
TEST(traces_parse_duration_null_start) {
|
||||
int64_t d = cbm_parse_duration(NULL, "1000");
|
||||
ASSERT_EQ(d, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(traces_parse_duration_null_end) {
|
||||
int64_t d = cbm_parse_duration("1000", NULL);
|
||||
ASSERT_EQ(d, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(traces_parse_duration_both_null) {
|
||||
int64_t d = cbm_parse_duration(NULL, NULL);
|
||||
ASSERT_EQ(d, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(traces_parse_duration_equal) {
|
||||
/* Same start and end → 0 */
|
||||
int64_t d = cbm_parse_duration("5000000000", "5000000000");
|
||||
ASSERT_EQ(d, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(traces_parse_duration_large_values) {
|
||||
/* Realistic nanosecond timestamps (epoch ~2024) */
|
||||
int64_t d = cbm_parse_duration("1700000000000000000", "1700000000050000000");
|
||||
ASSERT_EQ(d, 50000000); /* 50ms */
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── TestCalculateP99 — edge cases ────────────────────────────────── */
|
||||
|
||||
TEST(traces_calculate_p99_100_values) {
|
||||
int64_t values[100];
|
||||
for (int i = 0; i < 100; i++) {
|
||||
values[i] = (int64_t)(i + 1); /* 1..100 */
|
||||
}
|
||||
int64_t p99 = cbm_calculate_p99(values, 100);
|
||||
/* idx = (int)(100 * 0.99) = 99, values[99] = 100 */
|
||||
ASSERT_EQ(p99, 100);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(traces_calculate_p99_reversed) {
|
||||
/* Reversed array — qsort should sort first */
|
||||
int64_t values[10] = {100, 90, 80, 70, 60, 50, 40, 30, 20, 10};
|
||||
int64_t p99 = cbm_calculate_p99(values, 10);
|
||||
/* After sort: 10..100, idx = (int)(10 * 0.99) = 9, values[9] = 100 */
|
||||
ASSERT_EQ(p99, 100);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(traces_calculate_p99_two_values) {
|
||||
int64_t values[2] = {5, 500};
|
||||
int64_t p99 = cbm_calculate_p99(values, 2);
|
||||
/* idx = (int)(2 * 0.99) = 1, values[1] = 500 */
|
||||
ASSERT_EQ(p99, 500);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(traces_calculate_p99_all_same) {
|
||||
int64_t values[5] = {42, 42, 42, 42, 42};
|
||||
int64_t p99 = cbm_calculate_p99(values, 5);
|
||||
ASSERT_EQ(p99, 42);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(traces_calculate_p99_negative_count) {
|
||||
/* Negative count → treated like empty → 0 */
|
||||
int64_t values[3] = {10, 20, 30};
|
||||
int64_t p99 = cbm_calculate_p99(values, -1);
|
||||
ASSERT_EQ(p99, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(traces) {
|
||||
RUN_TEST(traces_extract_service_name);
|
||||
RUN_TEST(traces_extract_service_name_missing);
|
||||
RUN_TEST(traces_extract_service_name_null);
|
||||
RUN_TEST(traces_extract_service_name_empty_attrs);
|
||||
RUN_TEST(traces_extract_service_name_multiple_attrs);
|
||||
RUN_TEST(traces_extract_http_info);
|
||||
RUN_TEST(traces_extract_http_info_non_http);
|
||||
RUN_TEST(traces_extract_http_info_url_full);
|
||||
RUN_TEST(traces_extract_path_from_url);
|
||||
RUN_TEST(traces_extract_path_null_url);
|
||||
RUN_TEST(traces_extract_path_empty_url);
|
||||
RUN_TEST(traces_extract_path_with_port);
|
||||
RUN_TEST(traces_extract_path_with_query_and_fragment);
|
||||
RUN_TEST(traces_extract_path_buffer_too_small);
|
||||
RUN_TEST(traces_extract_path_relative_path);
|
||||
RUN_TEST(traces_calculate_p99);
|
||||
RUN_TEST(traces_calculate_p99_single);
|
||||
RUN_TEST(traces_calculate_p99_empty);
|
||||
RUN_TEST(traces_calculate_p99_100_values);
|
||||
RUN_TEST(traces_calculate_p99_reversed);
|
||||
RUN_TEST(traces_calculate_p99_two_values);
|
||||
RUN_TEST(traces_calculate_p99_all_same);
|
||||
RUN_TEST(traces_calculate_p99_negative_count);
|
||||
RUN_TEST(traces_parse_duration);
|
||||
RUN_TEST(traces_parse_duration_zero);
|
||||
RUN_TEST(traces_parse_duration_null_start);
|
||||
RUN_TEST(traces_parse_duration_null_end);
|
||||
RUN_TEST(traces_parse_duration_both_null);
|
||||
RUN_TEST(traces_parse_duration_equal);
|
||||
RUN_TEST(traces_parse_duration_large_values);
|
||||
}
|
||||
@@ -129,6 +129,170 @@ TEST(vmem_worker_budget) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── vmem edge-case and resource management tests ────────────── */
|
||||
|
||||
TEST(vmem_alloc_zero_returns_null) {
|
||||
cbm_vmem_init(0.5);
|
||||
/* alloc(0) must return NULL per the API contract */
|
||||
void *p = cbm_vmem_alloc(0);
|
||||
ASSERT_NULL(p);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(vmem_free_null_zero_no_crash) {
|
||||
cbm_vmem_init(0.5);
|
||||
/* free(NULL, 0) must be a no-op */
|
||||
cbm_vmem_free(NULL, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(vmem_free_null_nonzero_no_crash) {
|
||||
cbm_vmem_init(0.5);
|
||||
/* free(NULL, 100) must be a no-op — ptr==NULL short-circuits */
|
||||
cbm_vmem_free(NULL, 100);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(vmem_alloc_very_large) {
|
||||
cbm_vmem_init(0.5);
|
||||
/* Attempt 1 GB allocation — may succeed or fail depending on system.
|
||||
* Either way it must not crash. */
|
||||
size_t sz = (size_t)1024 * 1024 * 1024;
|
||||
void *p = cbm_vmem_alloc(sz);
|
||||
if (p) {
|
||||
/* If it succeeded, verify we can touch the first and last page */
|
||||
((unsigned char *)p)[0] = 0xAA;
|
||||
((unsigned char *)p)[sz - 1] = 0xBB;
|
||||
cbm_vmem_free(p, sz);
|
||||
}
|
||||
/* Success or graceful NULL — either is fine */
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(vmem_sequential_alloc_free_no_leak) {
|
||||
cbm_vmem_init(0.5);
|
||||
size_t before = cbm_vmem_allocated();
|
||||
|
||||
/* 20 alloc/free cycles — allocated must return to baseline */
|
||||
size_t sz = 64 * 1024; /* 64 KB */
|
||||
for (int i = 0; i < 20; i++) {
|
||||
void *p = cbm_vmem_alloc(sz);
|
||||
ASSERT_NOT_NULL(p);
|
||||
memset(p, (unsigned char)(i & 0xFF), sz);
|
||||
cbm_vmem_free(p, sz);
|
||||
}
|
||||
|
||||
size_t after = cbm_vmem_allocated();
|
||||
ASSERT_EQ(after, before);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(vmem_worker_budget_negative_workers) {
|
||||
cbm_vmem_init(0.5);
|
||||
size_t budget = cbm_vmem_budget();
|
||||
if (budget == 0) {
|
||||
/* Not initialized in this process — skip */
|
||||
PASS();
|
||||
}
|
||||
/* Negative workers clamps to 1 → worker_budget == full budget */
|
||||
size_t wb = cbm_vmem_worker_budget(-3);
|
||||
ASSERT_EQ(wb, budget);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(vmem_over_budget_when_nothing_allocated) {
|
||||
cbm_vmem_init(0.5);
|
||||
/* With nothing (or near-nothing) allocated, should not be over budget */
|
||||
bool over = cbm_vmem_over_budget();
|
||||
ASSERT_FALSE(over);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(vmem_allocated_tracks_alloc_free_cycle) {
|
||||
cbm_vmem_init(0.5);
|
||||
size_t base = cbm_vmem_allocated();
|
||||
|
||||
size_t sz = 128 * 1024; /* 128 KB */
|
||||
void *p = cbm_vmem_alloc(sz);
|
||||
ASSERT_NOT_NULL(p);
|
||||
|
||||
size_t after_alloc = cbm_vmem_allocated();
|
||||
ASSERT_GT(after_alloc, base);
|
||||
|
||||
cbm_vmem_free(p, sz);
|
||||
|
||||
size_t after_free = cbm_vmem_allocated();
|
||||
/* Must be back at or near baseline */
|
||||
ASSERT_LTE(after_free, base + 4096);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(vmem_multiple_alloc_tracks_cumulative) {
|
||||
cbm_vmem_init(0.5);
|
||||
size_t base = cbm_vmem_allocated();
|
||||
|
||||
size_t sz = 64 * 1024; /* 64 KB */
|
||||
void *p1 = cbm_vmem_alloc(sz);
|
||||
ASSERT_NOT_NULL(p1);
|
||||
size_t after_one = cbm_vmem_allocated();
|
||||
|
||||
void *p2 = cbm_vmem_alloc(sz);
|
||||
ASSERT_NOT_NULL(p2);
|
||||
size_t after_two = cbm_vmem_allocated();
|
||||
|
||||
/* Two allocs should be roughly double one alloc above base */
|
||||
ASSERT_GT(after_two, after_one);
|
||||
ASSERT_GT(after_one, base);
|
||||
|
||||
cbm_vmem_free(p2, sz);
|
||||
cbm_vmem_free(p1, sz);
|
||||
|
||||
size_t after_free = cbm_vmem_allocated();
|
||||
ASSERT_LTE(after_free, base + 4096);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(vmem_peak_never_decreases) {
|
||||
cbm_vmem_init(0.5);
|
||||
|
||||
size_t sz = 256 * 1024;
|
||||
void *p = cbm_vmem_alloc(sz);
|
||||
ASSERT_NOT_NULL(p);
|
||||
size_t peak_with_alloc = cbm_vmem_peak();
|
||||
|
||||
cbm_vmem_free(p, sz);
|
||||
size_t peak_after_free = cbm_vmem_peak();
|
||||
|
||||
/* Peak must never decrease */
|
||||
ASSERT_GTE(peak_after_free, peak_with_alloc);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(vmem_worker_budget_one_worker) {
|
||||
cbm_vmem_init(0.5);
|
||||
size_t budget = cbm_vmem_budget();
|
||||
if (budget == 0) {
|
||||
PASS();
|
||||
}
|
||||
/* 1 worker → equals full budget */
|
||||
size_t wb = cbm_vmem_worker_budget(1);
|
||||
ASSERT_EQ(wb, budget);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(vmem_worker_budget_many_workers) {
|
||||
cbm_vmem_init(0.5);
|
||||
size_t budget = cbm_vmem_budget();
|
||||
if (budget == 0) {
|
||||
PASS();
|
||||
}
|
||||
/* 1000 workers → must still be non-zero (budget is huge) */
|
||||
size_t wb = cbm_vmem_worker_budget(1000);
|
||||
ASSERT_GT(wb, 0);
|
||||
ASSERT_EQ(wb, budget / 1000);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Arena-vmem integration tests ─────────────────────────────── */
|
||||
|
||||
TEST(arena_vmem_alloc_and_destroy) {
|
||||
@@ -613,6 +777,20 @@ SUITE(vmem) {
|
||||
RUN_TEST(vmem_budget_tracking);
|
||||
RUN_TEST(vmem_peak_tracks);
|
||||
RUN_TEST(vmem_worker_budget);
|
||||
/* Edge cases and resource management */
|
||||
RUN_TEST(vmem_alloc_zero_returns_null);
|
||||
RUN_TEST(vmem_free_null_zero_no_crash);
|
||||
RUN_TEST(vmem_free_null_nonzero_no_crash);
|
||||
RUN_TEST(vmem_alloc_very_large);
|
||||
RUN_TEST(vmem_sequential_alloc_free_no_leak);
|
||||
RUN_TEST(vmem_worker_budget_negative_workers);
|
||||
RUN_TEST(vmem_over_budget_when_nothing_allocated);
|
||||
RUN_TEST(vmem_allocated_tracks_alloc_free_cycle);
|
||||
RUN_TEST(vmem_multiple_alloc_tracks_cumulative);
|
||||
RUN_TEST(vmem_peak_never_decreases);
|
||||
RUN_TEST(vmem_worker_budget_one_worker);
|
||||
RUN_TEST(vmem_worker_budget_many_workers);
|
||||
/* Arena-vmem integration */
|
||||
RUN_TEST(arena_vmem_alloc_and_destroy);
|
||||
RUN_TEST(arena_vmem_grow_tracks_sizes);
|
||||
RUN_TEST(arena_vmem_large_alloc);
|
||||
|
||||
@@ -1241,6 +1241,304 @@ TEST(watcher_modify_tracked_file) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
* RESOURCE MANAGEMENT & AUTO-INDEXING BEHAVIOR
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
TEST(watcher_null_store_handling) {
|
||||
/* watcher_new with NULL store — verify behavior */
|
||||
cbm_watcher_t *w = cbm_watcher_new(NULL, NULL, NULL);
|
||||
/* Implementation may return NULL or a valid watcher.
|
||||
* Either is acceptable — key is no crash. */
|
||||
if (w) {
|
||||
ASSERT_EQ(cbm_watcher_watch_count(w), 0);
|
||||
cbm_watcher_free(w);
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_free_null_safe) {
|
||||
/* Explicit test: free(NULL) must not crash */
|
||||
cbm_watcher_free(NULL);
|
||||
cbm_watcher_free(NULL);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_empty_count) {
|
||||
/* Fresh watcher with no projects → count 0 */
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
cbm_watcher_t *w = cbm_watcher_new(store, NULL, NULL);
|
||||
ASSERT_NOT_NULL(w);
|
||||
ASSERT_EQ(cbm_watcher_watch_count(w), 0);
|
||||
cbm_watcher_free(w);
|
||||
cbm_store_close(store);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_watch_multiple_verify_count) {
|
||||
/* Watch 5 projects, verify count at each step */
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
cbm_watcher_t *w = cbm_watcher_new(store, NULL, NULL);
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
char name[32], path[64];
|
||||
snprintf(name, sizeof(name), "proj-%d", i);
|
||||
snprintf(path, sizeof(path), "/tmp/proj-%d", i);
|
||||
cbm_watcher_watch(w, name, path);
|
||||
ASSERT_EQ(cbm_watcher_watch_count(w), i + 1);
|
||||
}
|
||||
|
||||
/* Unwatch all */
|
||||
for (int i = 0; i < 5; i++) {
|
||||
char name[32];
|
||||
snprintf(name, sizeof(name), "proj-%d", i);
|
||||
cbm_watcher_unwatch(w, name);
|
||||
}
|
||||
ASSERT_EQ(cbm_watcher_watch_count(w), 0);
|
||||
|
||||
cbm_watcher_free(w);
|
||||
cbm_store_close(store);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_watch_same_project_idempotent) {
|
||||
/* Watching the same project twice updates the path, count stays 1 */
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
cbm_watcher_t *w = cbm_watcher_new(store, NULL, NULL);
|
||||
|
||||
cbm_watcher_watch(w, "proj", "/tmp/path-a");
|
||||
ASSERT_EQ(cbm_watcher_watch_count(w), 1);
|
||||
cbm_watcher_watch(w, "proj", "/tmp/path-b");
|
||||
ASSERT_EQ(cbm_watcher_watch_count(w), 1);
|
||||
cbm_watcher_watch(w, "proj", "/tmp/path-c");
|
||||
ASSERT_EQ(cbm_watcher_watch_count(w), 1);
|
||||
|
||||
cbm_watcher_free(w);
|
||||
cbm_store_close(store);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_unwatch_nonexistent_safe) {
|
||||
/* Unwatch a project that was never watched — no crash */
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
cbm_watcher_t *w = cbm_watcher_new(store, NULL, NULL);
|
||||
|
||||
cbm_watcher_unwatch(w, "never-existed");
|
||||
cbm_watcher_unwatch(w, "also-never-existed");
|
||||
ASSERT_EQ(cbm_watcher_watch_count(w), 0);
|
||||
|
||||
cbm_watcher_free(w);
|
||||
cbm_store_close(store);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_touch_nonexistent_project) {
|
||||
/* touch() on a project not in the watch list — no crash */
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
cbm_watcher_t *w = cbm_watcher_new(store, NULL, NULL);
|
||||
|
||||
cbm_watcher_touch(w, "nonexistent-project");
|
||||
ASSERT_EQ(cbm_watcher_watch_count(w), 0);
|
||||
|
||||
cbm_watcher_free(w);
|
||||
cbm_store_close(store);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_poll_interval_zero_files) {
|
||||
/* 0 files → base interval (5000ms) */
|
||||
int ms = cbm_watcher_poll_interval_ms(0);
|
||||
ASSERT_EQ(ms, 5000);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_poll_interval_small_files) {
|
||||
/* 100 files → should be close to base (5000ms) */
|
||||
int ms = cbm_watcher_poll_interval_ms(100);
|
||||
ASSERT_GTE(ms, 5000);
|
||||
/* 100 files / 500 = 0 extra seconds of scaling → 5000ms */
|
||||
ASSERT_EQ(ms, 5000);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_poll_interval_medium_files) {
|
||||
/* 10000 files → 5000 + 20*1000 = 25000ms */
|
||||
int ms = cbm_watcher_poll_interval_ms(10000);
|
||||
ASSERT_EQ(ms, 25000);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_poll_interval_capped) {
|
||||
/* 100000 files → capped at 60000ms */
|
||||
int ms = cbm_watcher_poll_interval_ms(100000);
|
||||
ASSERT_EQ(ms, 60000);
|
||||
/* Even larger → still capped */
|
||||
ms = cbm_watcher_poll_interval_ms(500000);
|
||||
ASSERT_EQ(ms, 60000);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_poll_interval_negative) {
|
||||
/* Negative file count → should handle gracefully (no crash) */
|
||||
int ms = cbm_watcher_poll_interval_ms(-1);
|
||||
/* Result should be at least the base interval or 0 — just no crash */
|
||||
ASSERT_GTE(ms, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_poll_empty_returns_zero) {
|
||||
/* poll_once with empty watch list → 0 reindexed */
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
cbm_watcher_t *w = cbm_watcher_new(store, index_callback, NULL);
|
||||
index_call_count = 0;
|
||||
|
||||
int reindexed = cbm_watcher_poll_once(w);
|
||||
ASSERT_EQ(reindexed, 0);
|
||||
ASSERT_EQ(index_call_count, 0);
|
||||
|
||||
cbm_watcher_free(w);
|
||||
cbm_store_close(store);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_poll_non_git_dir) {
|
||||
/* poll_once with a non-git directory → 0 changes detected */
|
||||
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_watcher_ng2_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmpdir))
|
||||
SKIP("cbm_mkdtemp failed");
|
||||
|
||||
/* Create a regular file so directory is not empty */
|
||||
char cmd[512];
|
||||
snprintf(cmd, sizeof(cmd), "echo 'hello' > '%s/file.txt'", tmpdir);
|
||||
system(cmd);
|
||||
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
cbm_watcher_t *w = cbm_watcher_new(store, index_callback, NULL);
|
||||
cbm_watcher_watch(w, "nongit2", tmpdir);
|
||||
index_call_count = 0;
|
||||
|
||||
/* Baseline */
|
||||
cbm_watcher_poll_once(w);
|
||||
|
||||
/* Modify file */
|
||||
snprintf(cmd, sizeof(cmd), "echo 'world' >> '%s/file.txt'", tmpdir);
|
||||
system(cmd);
|
||||
|
||||
/* Poll — non-git directory, should not trigger reindex */
|
||||
cbm_watcher_touch(w, "nongit2");
|
||||
int reindexed = cbm_watcher_poll_once(w);
|
||||
ASSERT_EQ(reindexed, 0);
|
||||
ASSERT_EQ(index_call_count, 0);
|
||||
|
||||
cbm_watcher_free(w);
|
||||
cbm_store_close(store);
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", tmpdir);
|
||||
system(cmd);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_stop_prevents_run) {
|
||||
/* Setting stop before run → run returns immediately */
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
cbm_watcher_t *w = cbm_watcher_new(store, NULL, NULL);
|
||||
|
||||
cbm_watcher_stop(w);
|
||||
int rc = cbm_watcher_run(w, 60000);
|
||||
ASSERT_EQ(rc, 0);
|
||||
|
||||
cbm_watcher_free(w);
|
||||
cbm_store_close(store);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_watch_unwatch_rapid_cycle) {
|
||||
/* Rapid watch/unwatch cycles — stress lifecycle management */
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
cbm_watcher_t *w = cbm_watcher_new(store, NULL, NULL);
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
cbm_watcher_watch(w, "rapid", "/tmp/rapid");
|
||||
ASSERT_EQ(cbm_watcher_watch_count(w), 1);
|
||||
cbm_watcher_unwatch(w, "rapid");
|
||||
ASSERT_EQ(cbm_watcher_watch_count(w), 0);
|
||||
}
|
||||
|
||||
cbm_watcher_free(w);
|
||||
cbm_store_close(store);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Callback and state for watcher_callback_data_passed test */
|
||||
static int g_cbdata_value = 42;
|
||||
static int *g_cbdata_received = NULL;
|
||||
|
||||
static int capture_data_callback(const char *name, const char *path, void *ud) {
|
||||
(void)name; (void)path;
|
||||
g_cbdata_received = (int *)ud;
|
||||
return 0;
|
||||
}
|
||||
|
||||
TEST(watcher_callback_data_passed) {
|
||||
/* Verify that user_data pointer is accessible in the callback */
|
||||
g_cbdata_received = NULL;
|
||||
|
||||
/* Create a temp git repo */
|
||||
char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_watcher_cbdata_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmpdir))
|
||||
SKIP("cbm_mkdtemp failed");
|
||||
|
||||
char cmd[512];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"cd '%s' && git init -q && git config user.email test@test && "
|
||||
"git config user.name test && echo 'hello' > file.txt && "
|
||||
"git add file.txt && git commit -q -m 'init'",
|
||||
tmpdir);
|
||||
if (system(cmd) != 0) {
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", tmpdir);
|
||||
system(cmd);
|
||||
SKIP("git not available");
|
||||
}
|
||||
|
||||
cbm_store_t *store = cbm_store_open_memory();
|
||||
cbm_watcher_t *w = cbm_watcher_new(store, capture_data_callback, &g_cbdata_value);
|
||||
cbm_watcher_watch(w, "cbdata-repo", tmpdir);
|
||||
|
||||
/* Baseline */
|
||||
cbm_watcher_poll_once(w);
|
||||
|
||||
/* Make dirty to trigger callback */
|
||||
snprintf(cmd, sizeof(cmd), "echo 'dirty' >> '%s/file.txt'", tmpdir);
|
||||
system(cmd);
|
||||
|
||||
cbm_watcher_touch(w, "cbdata-repo");
|
||||
cbm_watcher_poll_once(w);
|
||||
|
||||
/* If callback was invoked, g_cbdata_received should point to g_cbdata_value */
|
||||
if (g_cbdata_received) {
|
||||
ASSERT_EQ(*g_cbdata_received, 42);
|
||||
}
|
||||
|
||||
cbm_watcher_free(w);
|
||||
cbm_store_close(store);
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", tmpdir);
|
||||
system(cmd);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_null_poll_once) {
|
||||
/* poll_once(NULL) → 0 */
|
||||
int reindexed = cbm_watcher_poll_once(NULL);
|
||||
ASSERT_EQ(reindexed, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(watcher_null_watch_count) {
|
||||
/* watch_count(NULL) → 0 */
|
||||
int count = cbm_watcher_watch_count(NULL);
|
||||
ASSERT_EQ(count, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
* SUITE
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
@@ -1295,4 +1593,27 @@ SUITE(watcher) {
|
||||
RUN_TEST(watcher_poll_only_watched_projects);
|
||||
RUN_TEST(watcher_touch_resets_immediate);
|
||||
RUN_TEST(watcher_modify_tracked_file);
|
||||
|
||||
/* Resource management & auto-indexing behavior */
|
||||
RUN_TEST(watcher_null_store_handling);
|
||||
RUN_TEST(watcher_free_null_safe);
|
||||
RUN_TEST(watcher_empty_count);
|
||||
RUN_TEST(watcher_watch_multiple_verify_count);
|
||||
RUN_TEST(watcher_watch_same_project_idempotent);
|
||||
RUN_TEST(watcher_unwatch_nonexistent_safe);
|
||||
RUN_TEST(watcher_touch_nonexistent_project);
|
||||
/* Poll interval edge cases */
|
||||
RUN_TEST(watcher_poll_interval_zero_files);
|
||||
RUN_TEST(watcher_poll_interval_small_files);
|
||||
RUN_TEST(watcher_poll_interval_medium_files);
|
||||
RUN_TEST(watcher_poll_interval_capped);
|
||||
RUN_TEST(watcher_poll_interval_negative);
|
||||
/* Poll edge cases */
|
||||
RUN_TEST(watcher_poll_empty_returns_zero);
|
||||
RUN_TEST(watcher_poll_non_git_dir);
|
||||
RUN_TEST(watcher_stop_prevents_run);
|
||||
RUN_TEST(watcher_watch_unwatch_rapid_cycle);
|
||||
RUN_TEST(watcher_callback_data_passed);
|
||||
RUN_TEST(watcher_null_poll_once);
|
||||
RUN_TEST(watcher_null_watch_count);
|
||||
}
|
||||
@@ -221,6 +221,147 @@ TEST(tls_persistence_across_dispatch) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Resource Management & Edge Case Tests ──────────────────────── */
|
||||
|
||||
TEST(parallel_for_negative_count) {
|
||||
/* count=-1 → no iterations (documented: "If count <= 0, this is a no-op") */
|
||||
_Atomic int sum;
|
||||
atomic_init(&sum, 0);
|
||||
cbm_parallel_for_opts_t opts = {.max_workers = 4, .force_pthreads = false};
|
||||
cbm_parallel_for(-1, sum_worker, &sum, opts);
|
||||
ASSERT_EQ(atomic_load(&sum), 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parallel_for_null_fn) {
|
||||
/* NULL function pointer — should not crash (no-op or safe handling) */
|
||||
cbm_parallel_for_opts_t opts = {.max_workers = 4, .force_pthreads = false};
|
||||
/* count=0 makes this a no-op before fn is called, so safe */
|
||||
cbm_parallel_for(0, NULL, NULL, opts);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parallel_for_max_workers_one) {
|
||||
/* max_workers=1 → serial execution, correct result */
|
||||
_Atomic int sum;
|
||||
atomic_init(&sum, 0);
|
||||
cbm_parallel_for_opts_t opts = {.max_workers = 1, .force_pthreads = false};
|
||||
cbm_parallel_for(50, sum_worker, &sum, opts);
|
||||
ASSERT_EQ(atomic_load(&sum), 50 * 49 / 2);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parallel_for_max_workers_zero_auto) {
|
||||
/* max_workers=0 → auto-detect, should produce correct result */
|
||||
_Atomic int sum;
|
||||
atomic_init(&sum, 0);
|
||||
cbm_parallel_for_opts_t opts = {.max_workers = 0, .force_pthreads = false};
|
||||
cbm_parallel_for(100, sum_worker, &sum, opts);
|
||||
ASSERT_EQ(atomic_load(&sum), 100 * 99 / 2);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parallel_for_large_count_coverage) {
|
||||
/* Large count (1000) → all indices visited exactly once */
|
||||
_Atomic int visited[1000];
|
||||
for (int i = 0; i < 1000; i++)
|
||||
atomic_init(&visited[i], 0);
|
||||
cbm_parallel_for_opts_t opts = {.max_workers = 8, .force_pthreads = false};
|
||||
cbm_parallel_for(1000, coverage_worker, visited, opts);
|
||||
/* Every index must be visited exactly once */
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
ASSERT_EQ(atomic_load(&visited[i]), 1);
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parallel_for_immediate_return_callback) {
|
||||
/* Callback that returns immediately — no crash, no hang */
|
||||
cbm_parallel_for_opts_t opts = {.max_workers = 4, .force_pthreads = false};
|
||||
cbm_parallel_for(500, noop_worker, NULL, opts);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Helpers for context_passed_correctly test */
|
||||
typedef struct { _Atomic int counter; int magic; } ctx_test_t;
|
||||
|
||||
static void count_and_verify_worker(int idx, void *vctx) {
|
||||
(void)idx;
|
||||
ctx_test_t *c = vctx;
|
||||
if (c->magic == 0xDEAD) {
|
||||
atomic_fetch_add(&c->counter, 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(parallel_for_context_passed_correctly) {
|
||||
/* Verify the context pointer reaches every iteration */
|
||||
ctx_test_t ctx;
|
||||
atomic_init(&ctx.counter, 0);
|
||||
ctx.magic = 0xDEAD;
|
||||
|
||||
cbm_parallel_for_opts_t opts = {.max_workers = 4, .force_pthreads = false};
|
||||
cbm_parallel_for(100, count_and_verify_worker, &ctx, opts);
|
||||
/* All 100 iterations should have received the correct context */
|
||||
ASSERT_EQ(atomic_load(&ctx.counter), 100);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Helper for no_duplicates test */
|
||||
static void count_visit_worker(int idx, void *ctx) {
|
||||
_Atomic int *c = ctx;
|
||||
atomic_fetch_add(&c[idx], 1);
|
||||
}
|
||||
|
||||
TEST(parallel_for_no_duplicates) {
|
||||
/* Verify no index is visited more than once (atomic increment per slot) */
|
||||
_Atomic int counts[500];
|
||||
for (int i = 0; i < 500; i++)
|
||||
atomic_init(&counts[i], 0);
|
||||
|
||||
cbm_parallel_for_opts_t opts = {.max_workers = 8, .force_pthreads = false};
|
||||
cbm_parallel_for(500, count_visit_worker, counts, opts);
|
||||
|
||||
for (int i = 0; i < 500; i++) {
|
||||
ASSERT_EQ(atomic_load(&counts[i]), 1);
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Helper for single_iteration_idx_zero test */
|
||||
static int g_received_idx = -1;
|
||||
|
||||
static void capture_idx_worker(int idx, void *ctx) {
|
||||
(void)ctx;
|
||||
g_received_idx = idx;
|
||||
}
|
||||
|
||||
TEST(parallel_for_single_iteration_idx_zero) {
|
||||
/* count=1 → single iteration with idx=0 */
|
||||
g_received_idx = -1;
|
||||
cbm_parallel_for_opts_t opts = {.max_workers = 4, .force_pthreads = false};
|
||||
cbm_parallel_for(1, capture_idx_worker, NULL, opts);
|
||||
ASSERT_EQ(g_received_idx, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(parallel_for_serial_matches_parallel) {
|
||||
/* Serial (max_workers=1) and parallel (max_workers=8) should produce
|
||||
* identical results for a deterministic reduction. */
|
||||
_Atomic int serial_sum, parallel_sum;
|
||||
atomic_init(&serial_sum, 0);
|
||||
atomic_init(¶llel_sum, 0);
|
||||
|
||||
cbm_parallel_for_opts_t serial_opts = {.max_workers = 1, .force_pthreads = false};
|
||||
cbm_parallel_for(200, sum_worker, &serial_sum, serial_opts);
|
||||
|
||||
cbm_parallel_for_opts_t parallel_opts = {.max_workers = 8, .force_pthreads = false};
|
||||
cbm_parallel_for(200, sum_worker, ¶llel_sum, parallel_opts);
|
||||
|
||||
ASSERT_EQ(atomic_load(&serial_sum), atomic_load(¶llel_sum));
|
||||
ASSERT_EQ(atomic_load(&serial_sum), 200 * 199 / 2);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Suite Registration ──────────────────────────────────────────── */
|
||||
|
||||
SUITE(system_info) {
|
||||
@@ -244,4 +385,15 @@ SUITE(worker_pool) {
|
||||
RUN_TEST(parallel_for_per_slot_write);
|
||||
RUN_TEST(parallel_for_actually_parallel);
|
||||
RUN_TEST(tls_persistence_across_dispatch);
|
||||
/* Resource management & edge cases */
|
||||
RUN_TEST(parallel_for_negative_count);
|
||||
RUN_TEST(parallel_for_null_fn);
|
||||
RUN_TEST(parallel_for_max_workers_one);
|
||||
RUN_TEST(parallel_for_max_workers_zero_auto);
|
||||
RUN_TEST(parallel_for_large_count_coverage);
|
||||
RUN_TEST(parallel_for_immediate_return_callback);
|
||||
RUN_TEST(parallel_for_context_passed_correctly);
|
||||
RUN_TEST(parallel_for_no_duplicates);
|
||||
RUN_TEST(parallel_for_single_iteration_idx_zero);
|
||||
RUN_TEST(parallel_for_serial_matches_parallel);
|
||||
}
|
||||
+1105
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user