@@ -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
2038320582SUITE(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