mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-07 08:43:35 +08:00
Port FastAPI Depends() + DLL resolve edge tracking (PR #66)
- FastAPI Depends(func_ref): scans Python function signatures for
Depends() patterns and creates CALLS edges to the dependency function.
Without this, auth/DI functions appear as dead code (in_degree=0).
- DLL resolve: scans C/C++ source for GetProcAddress/dlsym/Resolve
patterns and creates CALLS edges to synthetic stub nodes, enabling
call graph tracking across DLL boundaries.
- Extension scoping (#28) was already ported in 6a2b1f5.
- SQL size guard (#62) not needed: our C workers use 8MB stacks.
Tests: 3 new (httplink_laravel_path_filter, pipeline_fastapi_depends_edges,
pipeline_dll_resolve_edges). Total: 2041.
Co-Authored-By: kingchenc <kingchenc@users.noreply.github.com>
Co-Authored-By: mariomeyer <mariomeyer@users.noreply.github.com>
This commit is contained in:
co-authored by
kingchenc
mariomeyer
parent
93d332d1e0
commit
f92fd666b5
@@ -16,6 +16,8 @@
|
||||
#include "foundation/compat.h"
|
||||
#include "cbm.h"
|
||||
|
||||
#include "foundation/compat_regex.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -289,5 +291,312 @@ int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file
|
||||
cbm_log_info("pass.done", "pass", "calls", "total", itoa_log(total_calls), "resolved",
|
||||
itoa_log(resolved), "unresolved", itoa_log(unresolved), "errors",
|
||||
itoa_log(errors));
|
||||
|
||||
/* Additional pattern-based edge passes run after normal call resolution */
|
||||
cbm_pipeline_pass_fastapi_depends(ctx, files, file_count);
|
||||
cbm_pipeline_pass_dll_resolve(ctx, files, file_count);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── FastAPI Depends() tracking ──────────────────────────────────── */
|
||||
/* Scans Python function signatures for Depends(func_ref) patterns and
|
||||
* creates CALLS edges from the endpoint to the dependency function.
|
||||
* Without this, FastAPI auth/DI functions appear as dead code (in_degree=0). */
|
||||
|
||||
// NOLINTNEXTLINE(misc-include-cleaner) — cbm_file_info_t provided by standard header
|
||||
void cbm_pipeline_pass_fastapi_depends(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
|
||||
int file_count) {
|
||||
cbm_regex_t depends_re;
|
||||
if (cbm_regcomp(&depends_re, "Depends\\(([A-Za-z_][A-Za-z0-9_.]*)", CBM_REG_EXTENDED) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int edge_count = 0;
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
if (files[i].language != CBM_LANG_PYTHON) {
|
||||
continue;
|
||||
}
|
||||
if (cbm_pipeline_check_cancel(ctx)) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* Check if file has Depends call in cached extraction */
|
||||
CBMFileResult *result = ctx->result_cache ? ctx->result_cache[i] : NULL;
|
||||
if (!result) {
|
||||
continue;
|
||||
}
|
||||
bool has_depends = false;
|
||||
for (int c = 0; c < result->calls.count; c++) {
|
||||
if (result->calls.items[c].callee_name &&
|
||||
strcmp(result->calls.items[c].callee_name, "Depends") == 0) {
|
||||
has_depends = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!has_depends) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Read source and scan for Depends(func_ref) in function signatures */
|
||||
int source_len = 0;
|
||||
char *source = read_file(files[i].path, &source_len);
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char *module_qn = cbm_pipeline_fqn_module(ctx->project_name, files[i].rel_path);
|
||||
|
||||
/* Build import map for alias resolution */
|
||||
const char **imp_keys = NULL;
|
||||
const char **imp_vals = NULL;
|
||||
int imp_count = 0;
|
||||
build_import_map(ctx, files[i].rel_path, result, &imp_keys, &imp_vals, &imp_count);
|
||||
|
||||
for (int d = 0; d < result->defs.count; d++) {
|
||||
CBMDefinition *def = &result->defs.items[d];
|
||||
if (!def->qualified_name || def->start_line == 0) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(def->label, "Function") != 0 && strcmp(def->label, "Method") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Extract function signature (def line through ~15 lines for multi-line sigs) */
|
||||
int sig_end_line = (int)def->start_line + 15;
|
||||
if (def->end_line > 0 && sig_end_line > (int)def->end_line) {
|
||||
sig_end_line = (int)def->end_line;
|
||||
}
|
||||
|
||||
/* Find signature region in source */
|
||||
const char *p = source;
|
||||
int line = 1;
|
||||
while (*p && line < def->start_line) {
|
||||
if (*p == '\n') {
|
||||
line++;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
const char *sig_start = p;
|
||||
while (*p && line < sig_end_line) {
|
||||
if (*p == '\n') {
|
||||
line++;
|
||||
}
|
||||
p++;
|
||||
/* Stop at closing paren + colon (end of Python signature) */
|
||||
if (p > sig_start + 1 && p[-1] == ':' && p[-2] == ')') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
size_t sig_len = (size_t)(p - sig_start);
|
||||
char *sig = malloc(sig_len + 1);
|
||||
if (!sig) {
|
||||
continue;
|
||||
}
|
||||
memcpy(sig, sig_start, sig_len);
|
||||
sig[sig_len] = '\0';
|
||||
|
||||
/* Match Depends(func_ref) patterns */
|
||||
cbm_regmatch_t match[2];
|
||||
const char *scan = sig;
|
||||
while (cbm_regexec(&depends_re, scan, 2, match, 0) == 0) {
|
||||
int ref_len = match[1].rm_eo - match[1].rm_so;
|
||||
char func_ref[256];
|
||||
if (ref_len >= (int)sizeof(func_ref)) {
|
||||
ref_len = (int)sizeof(func_ref) - 1;
|
||||
}
|
||||
memcpy(func_ref, scan + match[1].rm_so, (size_t)ref_len);
|
||||
func_ref[ref_len] = '\0';
|
||||
|
||||
/* Resolve through registry */
|
||||
cbm_resolution_t res = cbm_registry_resolve(ctx->registry, func_ref, module_qn,
|
||||
imp_keys, imp_vals, imp_count);
|
||||
if (res.qualified_name && res.qualified_name[0] != '\0') {
|
||||
const cbm_gbuf_node_t *src_node =
|
||||
cbm_gbuf_find_by_qn(ctx->gbuf, def->qualified_name);
|
||||
const cbm_gbuf_node_t *tgt_node =
|
||||
cbm_gbuf_find_by_qn(ctx->gbuf, res.qualified_name);
|
||||
if (src_node && tgt_node && src_node->id != tgt_node->id) {
|
||||
cbm_gbuf_insert_edge(ctx->gbuf, src_node->id, tgt_node->id, "CALLS",
|
||||
"{\"confidence\":0.95,\"strategy\":\"fastapi_depends\""
|
||||
"}");
|
||||
edge_count++;
|
||||
}
|
||||
}
|
||||
scan += match[0].rm_eo;
|
||||
}
|
||||
free(sig);
|
||||
}
|
||||
|
||||
free(module_qn);
|
||||
free_import_map(imp_keys, imp_vals, imp_count);
|
||||
free(source);
|
||||
}
|
||||
|
||||
cbm_regfree(&depends_re);
|
||||
if (edge_count > 0) {
|
||||
cbm_log_info("pass.fastapi_depends", "edges", itoa_log(edge_count));
|
||||
}
|
||||
}
|
||||
|
||||
/* ── DLL resolve tracking ────────────────────────────────────────── */
|
||||
/* Scans C/C++ function source for dynamic DLL resolution patterns
|
||||
* (GetProcAddress, dlsym, Resolve) and creates CALLS edges to synthetic
|
||||
* stub nodes, enabling call graph tracking across DLL boundaries. */
|
||||
|
||||
// NOLINTNEXTLINE(misc-include-cleaner) — cbm_file_info_t provided by standard header
|
||||
void cbm_pipeline_pass_dll_resolve(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
|
||||
int file_count) {
|
||||
cbm_regex_t getproc_re;
|
||||
cbm_regex_t dlsym_re;
|
||||
cbm_regex_t resolve_re;
|
||||
|
||||
if (cbm_regcomp(&getproc_re,
|
||||
"GetProcAddress[AW]?\\([^,]+,[[:space:]]*\"([A-Za-z_][A-Za-z0-9_]*)\"",
|
||||
CBM_REG_EXTENDED) != 0) {
|
||||
return;
|
||||
}
|
||||
if (cbm_regcomp(&dlsym_re, "dlsym\\([^,]+,[[:space:]]*\"([A-Za-z_][A-Za-z0-9_]*)\"",
|
||||
CBM_REG_EXTENDED) != 0) {
|
||||
cbm_regfree(&getproc_re);
|
||||
return;
|
||||
}
|
||||
if (cbm_regcomp(&resolve_re, "[.>]Resolve\\([[:space:]]*\"([A-Za-z_][A-Za-z0-9_]*)\"",
|
||||
CBM_REG_EXTENDED) != 0) {
|
||||
cbm_regfree(&getproc_re);
|
||||
cbm_regfree(&dlsym_re);
|
||||
return;
|
||||
}
|
||||
|
||||
cbm_regex_t *patterns[] = {&getproc_re, &dlsym_re, &resolve_re};
|
||||
|
||||
int edge_count = 0;
|
||||
for (int i = 0; i < file_count; i++) {
|
||||
if (files[i].language != CBM_LANG_C && files[i].language != CBM_LANG_CPP) {
|
||||
continue;
|
||||
}
|
||||
if (cbm_pipeline_check_cancel(ctx)) {
|
||||
break;
|
||||
}
|
||||
|
||||
CBMFileResult *result = ctx->result_cache ? ctx->result_cache[i] : NULL;
|
||||
if (!result) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Early bail: check if any call targets a DLL resolution function */
|
||||
bool has_dll_call = false;
|
||||
for (int c = 0; c < result->calls.count; c++) {
|
||||
const char *name = result->calls.items[c].callee_name;
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(name, "GetProcAddress") == 0 || strcmp(name, "GetProcAddressA") == 0 ||
|
||||
strcmp(name, "GetProcAddressW") == 0 || strcmp(name, "dlsym") == 0 ||
|
||||
strstr(name, "Resolve") != NULL) {
|
||||
has_dll_call = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!has_dll_call) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int source_len = 0;
|
||||
char *source = read_file(files[i].path, &source_len);
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
|
||||
char *module_qn = cbm_pipeline_fqn_module(ctx->project_name, files[i].rel_path);
|
||||
|
||||
for (int d = 0; d < result->defs.count; d++) {
|
||||
CBMDefinition *def = &result->defs.items[d];
|
||||
if (!def->qualified_name || def->start_line == 0 || def->end_line == 0) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(def->label, "Function") != 0 && strcmp(def->label, "Method") != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Extract function body from source */
|
||||
const char *p = source;
|
||||
int line = 1;
|
||||
while (*p && line < def->start_line) {
|
||||
if (*p == '\n') {
|
||||
line++;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
const char *body_start = p;
|
||||
while (*p && line < def->end_line) {
|
||||
if (*p == '\n') {
|
||||
line++;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
size_t body_len = (size_t)(p - body_start);
|
||||
char *body = malloc(body_len + 1);
|
||||
if (!body) {
|
||||
continue;
|
||||
}
|
||||
memcpy(body, body_start, body_len);
|
||||
body[body_len] = '\0';
|
||||
|
||||
/* Match each DLL resolution pattern */
|
||||
for (int pi = 0; pi < 3; pi++) {
|
||||
cbm_regmatch_t match[2];
|
||||
const char *scan = body;
|
||||
while (cbm_regexec(patterns[pi], scan, 2, match, 0) == 0) {
|
||||
int fn_len = match[1].rm_eo - match[1].rm_so;
|
||||
char func_name[256];
|
||||
if (fn_len >= (int)sizeof(func_name)) {
|
||||
fn_len = (int)sizeof(func_name) - 1;
|
||||
}
|
||||
memcpy(func_name, scan + match[1].rm_so, (size_t)fn_len);
|
||||
func_name[fn_len] = '\0';
|
||||
|
||||
/* Create edge to synthetic DLL stub node */
|
||||
char target_qn[512];
|
||||
snprintf(target_qn, sizeof(target_qn), "%s.dll.external.%s", module_qn,
|
||||
func_name);
|
||||
|
||||
const cbm_gbuf_node_t *src_node =
|
||||
cbm_gbuf_find_by_qn(ctx->gbuf, def->qualified_name);
|
||||
if (src_node) {
|
||||
/* Create stub node if it doesn't exist */
|
||||
if (!cbm_gbuf_find_by_qn(ctx->gbuf, target_qn)) {
|
||||
char stub_props[256];
|
||||
snprintf(stub_props, sizeof(stub_props),
|
||||
"{\"stub\":true,\"source\":\"dll_resolve\","
|
||||
"\"dll_function\":\"%s\"}",
|
||||
func_name);
|
||||
cbm_gbuf_upsert_node(ctx->gbuf, "Function", func_name, target_qn,
|
||||
files[i].rel_path, (int)def->start_line,
|
||||
(int)def->start_line, stub_props);
|
||||
}
|
||||
const cbm_gbuf_node_t *tgt_node = cbm_gbuf_find_by_qn(ctx->gbuf, target_qn);
|
||||
if (tgt_node) {
|
||||
cbm_gbuf_insert_edge(ctx->gbuf, src_node->id, tgt_node->id, "CALLS",
|
||||
"{\"confidence\":0.85,\"strategy\":\"dll_resolve\""
|
||||
"}");
|
||||
edge_count++;
|
||||
}
|
||||
}
|
||||
scan += match[0].rm_eo;
|
||||
}
|
||||
}
|
||||
free(body);
|
||||
}
|
||||
|
||||
free(module_qn);
|
||||
free(source);
|
||||
}
|
||||
|
||||
cbm_regfree(&getproc_re);
|
||||
cbm_regfree(&dlsym_re);
|
||||
cbm_regfree(&resolve_re);
|
||||
if (edge_count > 0) {
|
||||
cbm_log_info("pass.dll_resolve", "edges", itoa_log(edge_count));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,6 +355,12 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
|
||||
|
||||
int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count);
|
||||
|
||||
/* Sub-passes called from pass_calls: pattern-based edge extraction */
|
||||
void cbm_pipeline_pass_fastapi_depends(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
|
||||
int file_count);
|
||||
void cbm_pipeline_pass_dll_resolve(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
|
||||
int file_count);
|
||||
|
||||
int cbm_pipeline_pass_usages(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, int file_count);
|
||||
|
||||
int cbm_pipeline_pass_semantic(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files,
|
||||
|
||||
@@ -780,6 +780,37 @@ TEST(httplink_linker_same_service_skip) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Laravel path filter: reject $ and : in matched paths ──────── */
|
||||
|
||||
TEST(httplink_laravel_path_filter) {
|
||||
cbm_route_handler_t routes[8];
|
||||
int n;
|
||||
|
||||
/* Cache key patterns should be filtered (contain $ or :) */
|
||||
n = cbm_extract_laravel_routes("fn", "proj.fn",
|
||||
"Cache::get('article:{$this->id}:image')", routes, 8);
|
||||
ASSERT_EQ(n, 0);
|
||||
|
||||
n = cbm_extract_laravel_routes("fn", "proj.fn",
|
||||
"Route::get(\"cache:$key\", fn() => null)", routes, 8);
|
||||
ASSERT_EQ(n, 0);
|
||||
|
||||
/* Valid routes should still pass (Laravel uses {param} not $param) */
|
||||
n = cbm_extract_laravel_routes("fn", "proj.fn",
|
||||
"Route::get('/api/users/{id}', 'UserController@show')", routes,
|
||||
8);
|
||||
ASSERT_EQ(n, 1);
|
||||
ASSERT_STR_EQ(routes[0].path, "/api/users/{id}");
|
||||
|
||||
/* Route with no special chars also passes */
|
||||
n = cbm_extract_laravel_routes("fn", "proj.fn",
|
||||
"Route::post('/api/login', 'AuthController@login')", routes, 8);
|
||||
ASSERT_EQ(n, 1);
|
||||
ASSERT_STR_EQ(routes[0].path, "/api/login");
|
||||
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Laravel module-level route extraction ─────────────────────── */
|
||||
|
||||
TEST(httplink_laravel_module_level_routes) {
|
||||
@@ -863,6 +894,9 @@ SUITE(httplink) {
|
||||
RUN_TEST(httplink_read_source_lines);
|
||||
RUN_TEST(httplink_read_source_lines_missing_file);
|
||||
|
||||
/* Laravel path filter (1 test) */
|
||||
RUN_TEST(httplink_laravel_path_filter);
|
||||
|
||||
/* Integration (3 tests) */
|
||||
RUN_TEST(httplink_linker_route_nodes);
|
||||
RUN_TEST(httplink_linker_same_service_skip);
|
||||
|
||||
@@ -4741,6 +4741,132 @@ static void cleanup_incremental_repo(void) {
|
||||
(void)system(cmd);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
* FastAPI Depends() edge tracking (PR #66, fix #27)
|
||||
* ═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
TEST(pipeline_fastapi_depends_edges) {
|
||||
/* Depends(get_current_user) should produce a CALLS edge from the
|
||||
* endpoint to the dependency function. */
|
||||
const char *files[] = {"auth.py", "routes.py"};
|
||||
const char *contents[] = {
|
||||
/* auth.py: defines get_current_user */
|
||||
"def get_current_user(token: str):\n"
|
||||
" return decode_token(token)\n",
|
||||
/* routes.py: endpoint depends on get_current_user */
|
||||
"from fastapi import Depends\n"
|
||||
"from auth import get_current_user\n\n"
|
||||
"def get_profile(user = Depends(get_current_user)):\n"
|
||||
" return {\"user\": user}\n"};
|
||||
if (setup_lang_repo(files, contents, 2) != 0) {
|
||||
SKIP("tmpdir");
|
||||
}
|
||||
char db[512];
|
||||
snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
|
||||
cbm_store_t *s = cbm_store_open_path(db);
|
||||
ASSERT_NOT_NULL(s);
|
||||
const char *proj = cbm_pipeline_project_name(p);
|
||||
|
||||
/* Check CALLS edges for fastapi_depends strategy */
|
||||
cbm_edge_t *edges = NULL;
|
||||
int edge_count = 0;
|
||||
cbm_store_find_edges_by_type(s, proj, "CALLS", &edges, &edge_count);
|
||||
|
||||
bool found_depends_edge = false;
|
||||
for (int i = 0; i < edge_count; i++) {
|
||||
if (edges[i].properties_json &&
|
||||
strstr(edges[i].properties_json, "fastapi_depends")) {
|
||||
found_depends_edge = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (edges) {
|
||||
cbm_store_free_edges(edges, edge_count);
|
||||
}
|
||||
ASSERT_TRUE(found_depends_edge);
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
teardown_lang_repo();
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
* DLL resolve edge tracking (PR #66, fix #29)
|
||||
* ═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
TEST(pipeline_dll_resolve_edges) {
|
||||
/* GetProcAddress(handle, "Func") should produce a CALLS edge to a
|
||||
* synthetic DLL stub node. */
|
||||
const char *files[] = {"loader.c"};
|
||||
const char *contents[] = {
|
||||
"#include <windows.h>\n\n"
|
||||
"void load_plugin(HMODULE hDll) {\n"
|
||||
" void *fn = GetProcAddress(hDll, \"PluginInit\");\n"
|
||||
" if (fn) ((void(*)())fn)();\n"
|
||||
"}\n"};
|
||||
if (setup_lang_repo(files, contents, 1) != 0) {
|
||||
SKIP("tmpdir");
|
||||
}
|
||||
char db[512];
|
||||
snprintf(db, sizeof(db), "%s/test.db", g_lang_tmpdir);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(g_lang_tmpdir, db, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
|
||||
cbm_store_t *s = cbm_store_open_path(db);
|
||||
ASSERT_NOT_NULL(s);
|
||||
const char *proj = cbm_pipeline_project_name(p);
|
||||
|
||||
/* Check for dll_resolve CALLS edge */
|
||||
cbm_edge_t *edges = NULL;
|
||||
int edge_count = 0;
|
||||
cbm_store_find_edges_by_type(s, proj, "CALLS", &edges, &edge_count);
|
||||
|
||||
bool found_dll_edge = false;
|
||||
for (int i = 0; i < edge_count; i++) {
|
||||
if (edges[i].properties_json && strstr(edges[i].properties_json, "dll_resolve")) {
|
||||
found_dll_edge = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (edges) {
|
||||
cbm_store_free_edges(edges, edge_count);
|
||||
}
|
||||
ASSERT_TRUE(found_dll_edge);
|
||||
|
||||
/* Check for stub node with dll_function property */
|
||||
cbm_node_t *nodes = NULL;
|
||||
int node_count = 0;
|
||||
cbm_store_find_nodes_by_label(s, proj, "Function", &nodes, &node_count);
|
||||
|
||||
bool found_stub = false;
|
||||
for (int i = 0; i < node_count; i++) {
|
||||
if (nodes[i].properties_json && strstr(nodes[i].properties_json, "PluginInit") &&
|
||||
strstr(nodes[i].properties_json, "dll_resolve")) {
|
||||
found_stub = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (nodes) {
|
||||
cbm_store_free_nodes(nodes, node_count);
|
||||
}
|
||||
ASSERT_TRUE(found_stub);
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
teardown_lang_repo();
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
* Incremental reindex
|
||||
* ═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
TEST(incremental_full_then_noop) {
|
||||
/* Full index, then re-run → should detect no changes and skip */
|
||||
if (setup_incremental_repo() != 0) { SKIP("setup failed"); }
|
||||
@@ -5062,6 +5188,10 @@ SUITE(pipeline) {
|
||||
RUN_TEST(githistory_coupling_skips_large_commits);
|
||||
RUN_TEST(githistory_coupling_limits_output);
|
||||
/* Incremental reindex */
|
||||
/* FastAPI Depends + DLL resolve edge tracking (PR #66 port) */
|
||||
RUN_TEST(pipeline_fastapi_depends_edges);
|
||||
RUN_TEST(pipeline_dll_resolve_edges);
|
||||
/* Incremental */
|
||||
RUN_TEST(incremental_full_then_noop);
|
||||
RUN_TEST(incremental_detects_changed_file);
|
||||
RUN_TEST(incremental_detects_deleted_file);
|
||||
|
||||
Reference in New Issue
Block a user