Skip to content

Commit 08587cd

Browse files
authored
Break-glass merge PR #2525
2 parents 742f09b + bf5ea5a commit 08587cd

3 files changed

Lines changed: 423 additions & 1 deletion

File tree

‎internal/cbm/lsp/py_lsp.c‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4933,7 +4933,9 @@ void py_lsp_process_file(PyLSPContext *ctx, TSNode root) {
49334933

49344934
uint32_t nc = rk.n;
49354935
const char *prev_func = ctx->enclosing_func_qn;
4936-
ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.__module__", ctx->module_qn);
4936+
/* Extraction uses the file's Module QN at top level. Keep the caller
4937+
* arena-owned for cross-file APIs whose module_qn input may be temporary. */
4938+
ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s", ctx->module_qn);
49374939
// Pass 1: execute top-level binding effects in source order. Function
49384940
// bodies remain deferred until the final module scope has been assembled.
49394941
for (uint32_t i = 0; i < nc && !ctx->eval_failure; i++) {

‎tests/test_pipeline.c‎

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10693,6 +10693,205 @@ TEST(pipeline_issue1527_cross_memo_failure_fallback) {
1069310693
PASS();
1069410694
}
1069510695

10696+
static bool python_module_write_fixture_file(const char *root, const char *name, const char *text) {
10697+
char path[768];
10698+
int n = snprintf(path, sizeof(path), "%s/%s", root, name);
10699+
return n > 0 && (size_t)n < sizeof(path) && th_write_file(path, text) == 0;
10700+
}
10701+
10702+
/* An absent node, failed edge query, or heuristic edge must not satisfy this
10703+
* integration check. Exact source/target identities precede strategy checks. */
10704+
static bool python_module_exact_lsp_edge(cbm_store_t *store, const char *project,
10705+
const char *caller, const char *caller_label,
10706+
const char *target, const char *target_label) {
10707+
char source_qn[768], target_qn[768];
10708+
int sn = snprintf(source_qn, sizeof(source_qn), "%s.%s", project, caller);
10709+
int tn = snprintf(target_qn, sizeof(target_qn), "%s.%s", project, target);
10710+
if (sn <= 0 || (size_t)sn >= sizeof(source_qn) || tn <= 0 || (size_t)tn >= sizeof(target_qn))
10711+
return false;
10712+
cbm_node_t source = {0}, destination = {0};
10713+
bool correct =
10714+
cbm_store_find_node_by_qn(store, project, source_qn, &source) == CBM_STORE_OK &&
10715+
cbm_store_find_node_by_qn(store, project, target_qn, &destination) == CBM_STORE_OK &&
10716+
source.id > 0 && destination.id > 0 && source.label && destination.label &&
10717+
strcmp(source.label, caller_label) == 0 && strcmp(destination.label, target_label) == 0;
10718+
cbm_edge_t *edges = NULL;
10719+
int edge_count = 0, hits = 0;
10720+
if (correct) {
10721+
correct = cbm_store_find_edges_by_source_type(store, source.id, "CALLS", &edges,
10722+
&edge_count) == CBM_STORE_OK;
10723+
for (int i = 0; correct && i < edge_count; i++) {
10724+
if (edges[i].source_id != source.id || edges[i].target_id != destination.id)
10725+
continue;
10726+
hits++;
10727+
const char *properties = edges[i].properties_json;
10728+
yyjson_doc *doc = properties ? yyjson_read(properties, strlen(properties), 0) : NULL;
10729+
const char *strategy =
10730+
doc ? yyjson_get_str(yyjson_obj_get(yyjson_doc_get_root(doc), "strategy")) : NULL;
10731+
correct = strategy && strncmp(strategy, "lsp_", 4) == 0;
10732+
if (doc)
10733+
yyjson_doc_free(doc);
10734+
}
10735+
}
10736+
cbm_store_free_edges(edges, edge_count);
10737+
cbm_node_free_fields(&source);
10738+
cbm_node_free_fields(&destination);
10739+
return correct && hits == 1;
10740+
}
10741+
10742+
static int python_module_callers_pipeline_case(bool parallel) {
10743+
static const char helpers[] = "def helper():\n"
10744+
" return 1\n"
10745+
"class Service:\n"
10746+
" def run(self):\n"
10747+
" return 2\n";
10748+
static const char app[] = "from helpers import helper, Service\n"
10749+
"def local_helper():\n"
10750+
" return 3\n"
10751+
"class Local:\n"
10752+
" def run(self):\n"
10753+
" return 4\n"
10754+
"class Controller:\n"
10755+
" def run(self):\n"
10756+
" helper()\n"
10757+
" method_service = Service()\n"
10758+
" method_service.run()\n"
10759+
"def control():\n"
10760+
" helper()\n"
10761+
" function_service = Service()\n"
10762+
" function_service.run()\n"
10763+
" local_helper()\n"
10764+
" function_local = Local()\n"
10765+
" function_local.run()\n"
10766+
"helper()\n"
10767+
"module_service = Service()\n"
10768+
"module_service.run()\n"
10769+
"local_helper()\n"
10770+
"module_local = Local()\n"
10771+
"module_local.run()\n";
10772+
static const struct {
10773+
const char *caller;
10774+
const char *caller_label;
10775+
const char *target;
10776+
const char *target_label;
10777+
} expected[] = {
10778+
{"app", "Module", "helpers.helper", "Function"},
10779+
{"app", "Module", "helpers.Service.run", "Method"},
10780+
{"app", "Module", "app.local_helper", "Function"},
10781+
{"app", "Module", "app.Local.run", "Method"},
10782+
{"app.control", "Function", "helpers.helper", "Function"},
10783+
{"app.control", "Function", "helpers.Service.run", "Method"},
10784+
{"app.control", "Function", "app.local_helper", "Function"},
10785+
{"app.control", "Function", "app.Local.run", "Method"},
10786+
{"app.Controller.run", "Method", "helpers.helper", "Function"},
10787+
{"app.Controller.run", "Method", "helpers.Service.run", "Method"},
10788+
{"pkg.__init__", "Module", "helpers.helper", "Function"},
10789+
};
10790+
char root[512];
10791+
int n = snprintf(root, sizeof(root), "%s/cbm-python-module-XXXXXX", cbm_tmpdir());
10792+
if (n <= 0 || (size_t)n >= sizeof(root) || !cbm_mkdtemp(root))
10793+
FAIL("failed to create Python module fixture");
10794+
char path[768];
10795+
n = snprintf(path, sizeof(path), "%s/pkg", root);
10796+
bool written = n > 0 && (size_t)n < sizeof(path);
10797+
if (written) {
10798+
cbm_mkdir_p(path, 0755);
10799+
written = python_module_write_fixture_file(root, "helpers.py", helpers) &&
10800+
python_module_write_fixture_file(root, "app.py", app) &&
10801+
python_module_write_fixture_file(root, "pkg/__init__.py",
10802+
"from helpers import helper\n"
10803+
"PACKAGE_VALUE = helper()\n");
10804+
}
10805+
for (int i = 0; written && parallel && i < 52; i++) {
10806+
char name[64];
10807+
snprintf(name, sizeof(name), "module_pad_%02d.py", i);
10808+
written = python_module_write_fixture_file(root, name, "padding = 0\n");
10809+
}
10810+
if (!written) {
10811+
th_rmtree(root);
10812+
FAIL("failed to write Python module fixture");
10813+
}
10814+
10815+
const char *keys[] = {"CBM_WORKERS", "CBM_INDEX_SINGLE_THREAD"};
10816+
char *saved[2] = {0};
10817+
bool saved_ok = true;
10818+
for (int i = 0; i < 2; i++) {
10819+
const char *old = getenv(keys[i]);
10820+
saved[i] = old ? strdup(old) : NULL;
10821+
saved_ok = saved_ok && (!old || saved[i]);
10822+
}
10823+
if (!saved_ok) {
10824+
free(saved[0]);
10825+
free(saved[1]);
10826+
th_rmtree(root);
10827+
FAIL("failed to save Python module fixture environment");
10828+
}
10829+
int set_rc = cbm_setenv(keys[0], parallel ? "4" : "1", 1);
10830+
set_rc |= parallel ? cbm_unsetenv(keys[1]) : cbm_setenv(keys[1], "1", 1);
10831+
n = snprintf(path, sizeof(path), "%s/module.db", root);
10832+
cbm_pipeline_t *pipeline = set_rc == 0 && n > 0 && (size_t)n < sizeof(path)
10833+
? cbm_pipeline_new(root, path, CBM_MODE_FULL)
10834+
: NULL;
10835+
int run_rc = pipeline ? cbm_pipeline_run(pipeline) : -1;
10836+
cbm_file_error_t *errors = NULL;
10837+
int error_count = 0;
10838+
cbm_pipeline_get_file_errors(pipeline, &errors, &error_count);
10839+
cbm_store_t *store = run_rc == 0 ? cbm_store_open_path(path) : NULL;
10840+
bool correct = store != NULL;
10841+
int file_count = 0;
10842+
if (store) {
10843+
const char *project = cbm_pipeline_project_name(pipeline);
10844+
cbm_node_t *files = NULL;
10845+
correct = project &&
10846+
cbm_store_find_nodes_by_label(store, project, "File", &files, &file_count) ==
10847+
CBM_STORE_OK &&
10848+
file_count == (parallel ? 55 : 3);
10849+
cbm_store_free_nodes(files, file_count);
10850+
for (size_t i = 0; i < sizeof(expected) / sizeof(expected[0]); i++) {
10851+
bool edge = project && python_module_exact_lsp_edge(
10852+
store, project, expected[i].caller, expected[i].caller_label,
10853+
expected[i].target, expected[i].target_label);
10854+
correct = correct && edge;
10855+
}
10856+
/* Neither the ordinary nor package Module should acquire a fake
10857+
* __module__ owner node. A failed lookup query is not absence proof. */
10858+
const char *phantoms[] = {"app.__module__", "pkg.__init__.__module__"};
10859+
for (int i = 0; project && i < 2; i++) {
10860+
char qn[768];
10861+
n = snprintf(qn, sizeof(qn), "%s.%s", project, phantoms[i]);
10862+
cbm_node_t node = {0};
10863+
bool absent =
10864+
n > 0 && (size_t)n < sizeof(qn) &&
10865+
cbm_store_find_node_by_qn(store, project, qn, &node) == CBM_STORE_NOT_FOUND;
10866+
correct = correct && absent;
10867+
cbm_node_free_fields(&node);
10868+
}
10869+
cbm_store_close(store);
10870+
}
10871+
cbm_pipeline_free(pipeline);
10872+
int restore_rc = 0;
10873+
for (int i = 0; i < 2; i++) {
10874+
restore_rc |= saved[i] ? cbm_setenv(keys[i], saved[i], 1) : cbm_unsetenv(keys[i]);
10875+
free(saved[i]);
10876+
}
10877+
int cleanup_rc = th_rmtree(root);
10878+
ASSERT_EQ(set_rc, 0);
10879+
ASSERT_EQ(restore_rc, 0);
10880+
ASSERT_EQ(cleanup_rc, 0);
10881+
ASSERT_EQ(run_rc, 0);
10882+
ASSERT_EQ(error_count, 0);
10883+
ASSERT_TRUE(correct);
10884+
PASS();
10885+
}
10886+
10887+
TEST(pipeline_python_module_callers_sequential) {
10888+
return python_module_callers_pipeline_case(false);
10889+
}
10890+
10891+
TEST(pipeline_python_module_callers_parallel) {
10892+
return python_module_callers_pipeline_case(true);
10893+
}
10894+
1069610895
/* #1277: typed instance fields of a Python class imported from another file.
1069710896
* Counts CALLS edges caller -> callee (QN suffixes) whose properties name an
1069810897
* LSP strategy. */
@@ -20381,6 +20580,8 @@ TEST(pipeline_calls_args_overlong_argument_stops_cleanly) {
2038120580
}
2038220581

2038320582
SUITE(pipeline) {
20583+
RUN_TEST(pipeline_python_module_callers_sequential);
20584+
RUN_TEST(pipeline_python_module_callers_parallel);
2038420585
RUN_TEST(pipeline_issue1527_python_raw_memo_failure_sequential);
2038520586
RUN_TEST(pipeline_issue1527_python_raw_memo_failure_parallel);
2038620587
RUN_TEST(pipeline_issue1527_python_cross_memo_failure_sequential);

0 commit comments

Comments
 (0)