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:
Martin Vogel committed 2026-03-24 23:48:23 +01:00
1 parent cbac7be1e0
commit 7f579f91ad
23 files changed
+6481 -5

No files matched your search

+4 -2
View File
@@ -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 ────────────────────────────────────────────
+5
View File
@@ -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) {
+27 -3
View File
@@ -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) */
+36
View File
@@ -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;
}
+32
View File
@@ -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);
}
+326
View File
@@ -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);
}
+592
View File
@@ -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);
}
+631
View File
@@ -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);
}
+224
View File
@@ -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);
}
+6
View File
@@ -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);
+310
View File
@@ -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);
+218
View File
@@ -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);
+490
View File
@@ -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);
}
+297
View File
@@ -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);
}
+560
View File
@@ -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);
}
+292
View File
@@ -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);
}
+156
View File
@@ -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);
}
+352
View File
@@ -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);
}
+167
View File
@@ -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);
}
+178
View File
@@ -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);
+321
View File
@@ -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);
}
+152
View File
@@ -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(&parallel_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, &parallel_sum, parallel_opts);
ASSERT_EQ(atomic_load(&serial_sum), atomic_load(&parallel_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
View File
File diff suppressed because it is too large. Load diff