Skip to content

Commit 72a8a0d

Browse files
committed
fix(ensemble): segment-anchored class matching; cbm_fopen; add ROUTES_TO pipeline test
- class_name_matches() replaces strstr for method_belongs_to_production: exact or dot-prefixed suffix match prevents 'Ens' matching 'Ens.BusinessService' - find_entry_point: strcmp instead of strstr for QN matching - read_file: cbm_fopen for Windows UTF-8 path compatibility - pipeline_ensemble_routing_routes_to_edges: full pipeline test with two .cls fixtures asserting >= 2 EnsembleItem nodes and >= 1 ROUTES_TO edge Signed-off-by: Thomas Dyar <tdyar@intersystems.com>
1 parent fd2968b commit 72a8a0d

2 files changed

Lines changed: 96 additions & 3 deletions

File tree

src/pipeline/pass_ensemble_routing.c

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ static ens_prod_def_t *parse_production_xml(const char *xml, const char *class_q
176176
}
177177

178178
static char *read_file(const char *full_path) {
179-
FILE *f = fopen(full_path, "rb");
179+
FILE *f = cbm_fopen(full_path, "rb");
180180
if (!f)
181181
return NULL;
182182
fseek(f, 0, SEEK_END);
@@ -234,7 +234,7 @@ static int64_t find_entry_point(cbm_pipeline_ctx_t *ctx, const char *class_name)
234234
cbm_gbuf_find_by_name(ctx->gbuf, ENTRY_POINTS[ei], (const cbm_gbuf_node_t ***)&nodes,
235235
&count);
236236
for (int ni = 0; ni < count; ni++) {
237-
if (nodes[ni]->qualified_name && strstr(nodes[ni]->qualified_name, suffix))
237+
if (nodes[ni]->qualified_name && strcmp(nodes[ni]->qualified_name, suffix) == 0)
238238
return nodes[ni]->id;
239239
}
240240
}
@@ -422,14 +422,30 @@ static void collect_prod_defs(cbm_pipeline_ctx_t *ctx, ens_prod_def_t ***defs_ou
422422
*count_out = n;
423423
}
424424

425+
/* True when haystack equals needle exactly, or ends with ".needle"
426+
* (segment-anchored). Prevents "Ens" from matching "Ens.BusinessService". */
427+
static bool class_name_matches(const char *haystack, const char *needle) {
428+
if (!haystack || !needle)
429+
return false;
430+
size_t hlen = strlen(haystack);
431+
size_t nlen = strlen(needle);
432+
if (nlen == 0)
433+
return false;
434+
if (hlen == nlen)
435+
return strcmp(haystack, needle) == 0;
436+
if (hlen > nlen && haystack[hlen - nlen - 1] == '.')
437+
return strcmp(haystack + hlen - nlen, needle) == 0;
438+
return false;
439+
}
440+
425441
static bool method_belongs_to_production(const cbm_gbuf_node_t *method, const ens_prod_def_t *def) {
426442
if (!method->properties_json)
427443
return false;
428444
char parent_class[CBM_SZ_512];
429445
if (!jstr(method->properties_json, "parent_class", parent_class, sizeof(parent_class)))
430446
return false;
431447
for (int i = 0; i < def->n_items; i++) {
432-
if (strstr(parent_class, def->items[i].class_name))
448+
if (class_name_matches(parent_class, def->items[i].class_name))
433449
return true;
434450
}
435451
return false;

tests/test_pipeline.c

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6881,6 +6881,81 @@ TEST(pipeline_seq_ts_cross_uses_shared_registry) {
68816881
PASS();
68826882
}
68836883

6884+
TEST(pipeline_ensemble_routing_routes_to_edges) {
6885+
char tmpdir[256];
6886+
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_ens_XXXXXX");
6887+
if (!cbm_mkdtemp(tmpdir))
6888+
FAIL("failed to create temp dir");
6889+
6890+
char path[512];
6891+
6892+
/* Production class with two items */
6893+
snprintf(path, sizeof(path), "%s/MyProduction.cls", tmpdir);
6894+
FILE *f = fopen(path, "w");
6895+
if (!f) {
6896+
th_rmtree(tmpdir);
6897+
FAIL("fopen production");
6898+
}
6899+
fprintf(f, "Class MyApp.MyProduction Extends Ens.Production\n"
6900+
"{\n"
6901+
"XData ProductionDefinition\n"
6902+
"{\n"
6903+
"<Production Name=\"MyApp.MyProduction\">\n"
6904+
" <Item Name=\"MyService\" ClassName=\"MyApp.MyService\" Enabled=\"true\">\n"
6905+
" </Item>\n"
6906+
" <Item Name=\"MyOperation\" ClassName=\"MyApp.MyOperation\" Enabled=\"true\">\n"
6907+
" </Item>\n"
6908+
"</Production>\n"
6909+
"}\n"
6910+
"}\n");
6911+
fclose(f);
6912+
6913+
/* Service class with OnProcessInput that routes to MyOperation via literal */
6914+
snprintf(path, sizeof(path), "%s/MyService.cls", tmpdir);
6915+
f = fopen(path, "w");
6916+
if (!f) {
6917+
th_rmtree(tmpdir);
6918+
FAIL("fopen service");
6919+
}
6920+
fprintf(f, "Class MyApp.MyService Extends Ens.BusinessService\n"
6921+
"{\n"
6922+
"Method OnProcessInput(pRequest As %%Library.Object,"
6923+
" Output pResponse As %%Library.Object) As %%Status\n"
6924+
"{\n"
6925+
" Do ..SendRequestSync(\"MyOperation\", pRequest, .pResponse)\n"
6926+
" Quit $$$OK\n"
6927+
"}\n"
6928+
"}\n");
6929+
fclose(f);
6930+
6931+
char db_path[512];
6932+
snprintf(db_path, sizeof(db_path), "%s/ens.db", tmpdir);
6933+
6934+
cbm_pipeline_t *p = cbm_pipeline_new(tmpdir, db_path, CBM_MODE_FULL);
6935+
ASSERT_NOT_NULL(p);
6936+
ASSERT_EQ(cbm_pipeline_run(p), 0);
6937+
6938+
cbm_store_t *s = cbm_store_open_path(db_path);
6939+
ASSERT_NOT_NULL(s);
6940+
const char *project = cbm_pipeline_project_name(p);
6941+
6942+
/* EnsembleItem nodes emitted for both production items */
6943+
cbm_node_t *items = NULL;
6944+
int item_count = 0;
6945+
cbm_store_find_nodes_by_label(s, project, "EnsembleItem", &items, &item_count);
6946+
ASSERT_GTE(item_count, 2);
6947+
cbm_store_free_nodes(items, item_count);
6948+
6949+
/* At least one ROUTES_TO edge from SendRequestSync literal match */
6950+
int routes = cbm_store_count_edges_by_type(s, project, "ROUTES_TO");
6951+
ASSERT_GTE(routes, 1);
6952+
6953+
cbm_store_close(s);
6954+
cbm_pipeline_free(p);
6955+
th_rmtree(tmpdir);
6956+
PASS();
6957+
}
6958+
68846959
SUITE(pipeline) {
68856960
/* Index lock */
68866961
RUN_TEST(pipeline_lock_try_acquire);
@@ -7157,4 +7232,6 @@ SUITE(pipeline) {
71577232
/* Project name edge cases */
71587233
RUN_TEST(project_name_special_chars);
71597234
RUN_TEST(project_name_trailing_slash);
7235+
/* Ensemble routing pass */
7236+
RUN_TEST(pipeline_ensemble_routing_routes_to_edges);
71607237
}

0 commit comments

Comments
 (0)