test(cypher): isolate UNION stack checks from elapsed time

Use a thread-local test clock for the large UNION structural regression
so sanitizer overhead cannot consume its execution deadline. Preserve
all row, ordering, and constrained-stack checks. Add an exact positive
budget expiry case alongside the existing deadline tests.

Request the fixture's exact 20000-row budget to avoid reserving the
default 100000 binding slots for every branch, and assert no truncation.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-12 21:17:13 +02:00
1 parent df6f674ecb
commit 415abe9c2d
3 files changed
+63 -3

No files matched your search

+19 -2
View File
@@ -3138,11 +3138,28 @@ static _Thread_local bool g_cypher_timed_out = false;
static _Thread_local bool g_cypher_truncated = false;
static _Thread_local int64_t g_cypher_deadline_override_ms = -1; /* test hook; <0 = default */
#ifdef CBM_ENABLE_TEST_SEAMS
static _Thread_local cbm_cypher_test_clock_fn g_cypher_deadline_clock = NULL;
void cbm_cypher_test_set_deadline_clock(cbm_cypher_test_clock_fn clock_fn) {
g_cypher_deadline_clock = clock_fn;
}
#endif
static uint64_t cypher_deadline_now(void) {
#ifdef CBM_ENABLE_TEST_SEAMS
if (g_cypher_deadline_clock) {
return g_cypher_deadline_clock();
}
#endif
return cbm_now_ms();
}
static void cypher_deadline_arm(void) {
g_cypher_timed_out = false;
int64_t budget = g_cypher_deadline_override_ms >= 0 ? g_cypher_deadline_override_ms
: CYPHER_DEADLINE_BUDGET_MS;
g_cypher_deadline_ms = cbm_now_ms() + (uint64_t)budget;
g_cypher_deadline_ms = cypher_deadline_now() + (uint64_t)budget;
}
/* True once the query has run past its wall-clock budget. Sticky: after the
@@ -3154,7 +3171,7 @@ static bool cypher_deadline_exceeded(void) {
if (g_cypher_deadline_ms == 0) {
return false;
}
if (cbm_now_ms() >= g_cypher_deadline_ms) {
if (cypher_deadline_now() >= g_cypher_deadline_ms) {
g_cypher_timed_out = true;
return true;
}
+6
View File
@@ -355,6 +355,12 @@ void cbm_query_free(cbm_query_t *q);
* check; a negative value restores the default budget. */
void cbm_cypher_test_set_deadline_ms(int64_t budget_ms);
#ifdef CBM_ENABLE_TEST_SEAMS
/* Override only the deadline clock on this thread; NULL restores real time. */
typedef uint64_t (*cbm_cypher_test_clock_fn)(void);
void cbm_cypher_test_set_deadline_clock(cbm_cypher_test_clock_fn clock_fn);
#endif
/* Worst-case binding slot count for a node cross-join. Computes the count in
* size_t and rejects any that would not fit the int binding counter or would
* overflow the size_t byte size; returns 0 and writes *out_n on success,
+38 -1
View File
@@ -5064,6 +5064,35 @@ TEST(cypher_exec_deadline_aborts_runaway_query_issue601) {
PASS();
}
static uint64_t cypher_frozen_deadline_clock(void) {
return 1000;
}
static _Thread_local unsigned cypher_deadline_clock_reads = 0;
static uint64_t cypher_expiring_deadline_clock(void) {
/* Default budget is 30 seconds; expire exactly at the first checkpoint. */
return cypher_deadline_clock_reads++ == 0 ? 1000 : 31000;
}
TEST(cypher_exec_deadline_expires_at_positive_budget_boundary) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
cypher_deadline_clock_reads = 0;
cbm_cypher_test_set_deadline_clock(cypher_expiring_deadline_clock);
int rc = cbm_cypher_execute(s, "MATCH (n) RETURN n.name", "test", 0, &r);
cbm_cypher_test_set_deadline_clock(NULL);
ASSERT_TRUE(cypher_deadline_clock_reads >= 2);
ASSERT_TRUE(rc != 0);
ASSERT_NOT_NULL(r.error);
ASSERT_NOT_NULL(strstr(r.error, "time limit"));
ASSERT_EQ(r.row_count, 0);
cbm_cypher_result_free(&r);
cbm_store_close(s);
PASS();
}
/* #601 companion: the default (ample) budget must NOT false-positive on a
* normal small query — it still returns its rows. */
TEST(cypher_exec_deadline_allows_normal_query_issue601) {
@@ -5442,13 +5471,20 @@ static int union_many_check(void *arg) {
cbm_store_t *s = setup_cypher_store();
cbm_cypher_result_t r = {0};
int rc = cbm_cypher_execute(s, query, "test", 0, &r);
/* This exercises stack depth and every result, independently of how
* slowly instrumentation runs. Dedicated deadline tests retain expiry. */
cbm_cypher_test_set_deadline_clock(cypher_frozen_deadline_clock);
/* Request exactly the output this fixture needs: the default 100k ceiling
* also reserves 100k binding slots separately for every UNION branch. */
int rc = cbm_cypher_execute(s, query, "test", CYPHER_UNION_MANY_BRANCHES, &r);
cbm_cypher_test_set_deadline_clock(NULL);
free(query);
if (rc != 0) {
printf(" query error: %s\n", r.error ? r.error : "(none)");
}
ASSERT_EQ(rc, 0);
ASSERT_EQ(r.row_count, CYPHER_UNION_MANY_BRANCHES);
ASSERT_TRUE(!r.truncated);
for (int i = 0; i < r.row_count; i++) {
ASSERT_STR_EQ(r.rows[i][0], cypher_fixture_nodes[i % CYPHER_FIXTURE_NODE_COUNT]);
}
@@ -5548,6 +5584,7 @@ SUITE(cypher) {
RUN_TEST(cypher_parse_error_frees_partial_pattern);
/* Execution */
RUN_TEST(cypher_exec_deadline_aborts_runaway_query_issue601);
RUN_TEST(cypher_exec_deadline_expires_at_positive_budget_boundary);
RUN_TEST(cypher_exec_deadline_allows_normal_query_issue601);
RUN_TEST(cypher_deep_nesting_rejected_not_crash);
RUN_TEST(cypher_exec_match_all_functions);