myxce/src/lib/testsuite/test_select.c
Rongsong Shen 450a4d467c fix: sel_key_cmp_fn padding-byte mismatch in kvtab lookups
sel_key_cmp_fn compared keys with memcmp on the whole sel_node_key_t
struct, including the 4-byte padding between type and op1.  The stack-
allocated search key in gc_sweep_select_nodes can have different padding
bytes than the heap-allocated copy stored by key_add, causing key_del
to miss the entry and leave a dangling pointer in the CSE intern table.

Replace memcmp with field-by-field comparison on type, op1, op2.

Also fix GC sweep tests that asserted pointer inequality after re-intern:
calloc may lawfully reuse the freed address, so pointer inequality does
not reliably indicate a successful sweep.  Track the swept node's
node_id before freeing and assert the re-interned node gets a fresh id.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:53:59 +00:00

2452 lines
81 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* test_select.c — Phase 1 TDD skeletons for select-expression AST / CSE.
*
* Compile but fail until Phases 3-4 implement the full select pipeline.
* The UNIT_TEST macro must be defined before including resreq.h so that
* sel_optimize_test_reset() is visible.
*/
#define UNIT_TEST 1
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unity/unity.h>
#include "resreq.h"
#include "test_resmgr_fixture.h"
#include "utils.h"
#include "xstring.h"
/* ── fake_scheduler_t ─────────────────────────────────────────────────────
*
* Phase 1 stub. Phase 6 replaces these wrappers with equivalents that
* reference real xce_scheduler_t fields — the atomic field layout and
* predicate semantics are stable.
*/
typedef struct {
_Atomic int session_active;
_Atomic int eval_in_progress;
_Atomic int staging_merge_complete;
} fake_scheduler_t;
static int
test_sched_session_active(fake_scheduler_t *s)
{
return atomic_load_explicit(&s->session_active, memory_order_acquire);
}
static int
test_sched_eval_in_progress(fake_scheduler_t *s)
{
return atomic_load_explicit(&s->eval_in_progress, memory_order_acquire);
}
static int
test_staging_merge_complete(fake_scheduler_t *s)
{
return atomic_load_explicit(&s->staging_merge_complete,
memory_order_acquire);
}
/* ── 1-1: Parse tests ─────────────────────────────────────────────────────*/
static void
test_select_parse_valid_expr(void)
{
/* parse_resreq_primary with a well-formed select expression must
* succeed (rc >= 0) and produce a non-NULL root node. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
/* resmgr fixture is created for teardown symmetry; Phase 2 threads it
* into parse_resreq_primary — for Phase 1 we use the 3-arg signature. */
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "type==linux", resmgr, &errmsg);
/* Phase 4: expect success */
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_select_parse_valid_any(void)
{
/* parse_resreq_primary with a well-formed select expression must
* succeed (rc >= 0) and produce a non-NULL root node. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
/* resmgr fixture is created for teardown symmetry; Phase 2 threads it
* into parse_resreq_primary — for Phase 1 we use the 3-arg signature. */
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "type==any", resmgr, &errmsg);
/* Phase 4: expect success */
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_select_parse_unknown_resource(void)
{
/* A resource name that is not present in the resmgr must produce a
* parse error (rc < 0). */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "unknownres==linux", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc < 0);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_select_parse_type_mismatch(void)
{
/* Comparing a BOOL resource with a string literal is a type mismatch
* and must be rejected. "gpu" is registered as RES_TYPE_BOOL in the
* fixture; "gpu==true" supplies a string literal, not a bare bool. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "gpu==true", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc < 0);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
/* ── 1-2: Overflow guard tests ────────────────────────────────────────────*/
static void
test_select_input_len_overflow(void)
{
/* Expressions longer than SELECT_MAX_INPUT_LEN (8192) must be rejected
* immediately — before any parsing work is done. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
const int limit = 8192 + 1;
char * longexpr = malloc((size_t)limit + 1);
TEST_ASSERT_NOT_NULL(longexpr);
memset(longexpr, 'a', (size_t)limit);
longexpr[limit] = '\0';
int rc = parse_resreq_primary(&resreq, longexpr, resmgr, &errmsg);
TEST_ASSERT_TRUE(rc < 0);
free(longexpr);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_select_depth_overflow(void)
{
/* Nesting deeper than SELECT_MAX_DEPTH (64) must be rejected.
* Build 65 levels of '!' prefix to exceed the limit. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
const int max_depth = 65;
/* Each level is one '!' character; add "gpu" plus NUL at the end. */
char *deepexpr = malloc((size_t)max_depth + 4);
TEST_ASSERT_NOT_NULL(deepexpr);
int pos = 0;
for (int i = 0; i < max_depth; i++)
deepexpr[pos++] = '!';
deepexpr[pos++] = 'g';
deepexpr[pos++] = 'p';
deepexpr[pos++] = 'u';
deepexpr[pos] = '\0';
int rc = parse_resreq_primary(&resreq, deepexpr, resmgr, &errmsg);
TEST_ASSERT_TRUE(rc < 0);
free(deepexpr);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
/* ── 1-3: CSE / hash-consing tests ───────────────────────────────────────*/
static void
test_select_cse_commutative_same_ptr(void)
{
/* When CSE is enabled, A&&B and B&&A must be canonicalised to the same
* interned pointer because && is commutative. */
sel_node_table_init();
sel_optimize_set(1);
xce_select_node_t *a = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_GPU_IDX, 0);
xce_select_node_t *b = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_MEM_IDX, 0);
TEST_ASSERT_NOT_NULL(a);
TEST_ASSERT_NOT_NULL(b);
xce_select_node_t *ab = makeSelectNode(SEL_AND, (uintptr_t)a, (uintptr_t)b);
xce_select_node_t *ba = makeSelectNode(SEL_AND, (uintptr_t)b, (uintptr_t)a);
TEST_ASSERT_NOT_NULL(ab);
TEST_ASSERT_NOT_NULL(ba);
/* Phase 4: pointer equality guaranteed by commutativity sort + CSE */
TEST_ASSERT_EQUAL_PTR(ab, ba);
sel_optimize_test_reset();
}
static void
test_select_cse_same_args_same_ptr(void)
{
/* Two makeSelectNode calls with identical type and operands must return
* the same interned pointer. */
sel_node_table_init();
sel_optimize_set(1);
double num = 100.0;
uintptr_t bits;
memcpy(&bits, &num, sizeof(bits));
xce_select_node_t *n1 = makeSelectNode(SEL_NUM, bits, 0);
xce_select_node_t *n2 = makeSelectNode(SEL_NUM, bits, 0);
TEST_ASSERT_NOT_NULL(n1);
TEST_ASSERT_NOT_NULL(n2);
/* Phase 4: same pointer from CSE table */
TEST_ASSERT_EQUAL_PTR(n1, n2);
sel_optimize_test_reset();
}
/* ── 1-4: Eval tests ──────────────────────────────────────────────────────
*
* In Phase 1 these tests verify parse structure (AST node types), not
* runtime evaluation values. Full eval contract is exercised in Phase 4e
* once eval_select_simple is implemented.
*/
static void
test_eval_numeric_gt(void)
{
/* "mem>50" must parse to a SEL_GT root node. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "mem>50", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
TEST_ASSERT_EQUAL_INT(SEL_GT, resreq.select->root->type);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_eval_numeric_lt(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "mem<200", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
TEST_ASSERT_EQUAL_INT(SEL_LT, resreq.select->root->type);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_eval_numeric_eq(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "mem==100", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
TEST_ASSERT_EQUAL_INT(SEL_EQ, resreq.select->root->type);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_eval_string_eq(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "type==linux", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
TEST_ASSERT_EQUAL_INT(SEL_EQ, resreq.select->root->type);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_eval_bool_standalone(void)
{
/* A bare resource name of type BOOL must parse as SEL_RESNAME with
* res_type == RES_TYPE_BOOL — no comparison operator needed. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "gpu", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
TEST_ASSERT_EQUAL_INT(SEL_RESNAME, resreq.select->root->type);
TEST_ASSERT_EQUAL_INT(RES_TYPE_BOOL,
resreq.select->root->u.resname.res_type);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_eval_not(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "!gpu", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
TEST_ASSERT_EQUAL_INT(SEL_NOT, resreq.select->root->type);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_eval_and_shortcircuit(void)
{
/* "gpu && mem>50" must produce a SEL_AND root with non-NULL left and
* right children. Short-circuit semantics are verified in Phase 4e. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "gpu && mem>50", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
TEST_ASSERT_EQUAL_INT(SEL_AND, resreq.select->root->type);
TEST_ASSERT_NOT_NULL(resreq.select->root->u.binop.left);
TEST_ASSERT_NOT_NULL(resreq.select->root->u.binop.right);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_eval_or_shortcircuit(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "gpu || type==linux", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
TEST_ASSERT_EQUAL_INT(SEL_OR, resreq.select->root->type);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
/* ── 4b-3: Ordered comparison type validation tests ─────────────────────────*/
static void
test_ordered_cmp_nonnum_string_resource_fails(void)
{
/* "type>5" where "type" is RES_TYPE_STRING must be rejected.
* Ordered comparisons are only valid on numeric resources. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "type>5", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc < 0);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_ordered_cmp_nonnum_bool_resource_fails(void)
{
/* "gpu>0" where "gpu" is RES_TYPE_BOOL must be rejected.
* Ordered comparisons are only valid on numeric resources. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "gpu>0", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc < 0);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_ordered_cmp_string_rhs_fails(void)
{
/* "mem>linux" where "mem" is RES_TYPE_NUMERIC but RHS is a string literal
* must be rejected. Ordered comparisons require a numeric literal. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "mem>linux", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc < 0);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_ordered_cmp_numeric_resource_succeeds(void)
{
/* Ordered comparisons on a numeric resource with a numeric literal
* must succeed. "mem>50" and "mem<200" are tested separately in
* test_eval_numeric_gt/lt; this test validates GE and LE variants. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "mem>=100", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
TEST_ASSERT_EQUAL_INT(SEL_GE, resreq.select->root->type);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
/* ── Phase 4b: EQ type-validation tests ──────────────────────────────────*/
static void
test_4b_num_str_mismatch_eq_fails(void)
{
/* "mem==linux" where "mem" is RES_TYPE_NUMERIC and "linux" is a string
* literal must be rejected: numeric resource vs string literal is a
* type mismatch for equality comparisons. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "mem==linux", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc < 0);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_4b_str_num_coercion_eq_succeeds(void)
{
/* "type==5" where "type" is RES_TYPE_STRING and "5" is a SEL_NUM literal.
* The numeric literal must be coerced to the string "5" so the comparison
* is accepted (STRING × NUM → coerce to STRING × STR("5")). */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "type==5", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
TEST_ASSERT_EQUAL_INT(SEL_EQ, resreq.select->root->type);
TEST_ASSERT_NOT_NULL(resreq.select->root->u.binop.left);
TEST_ASSERT_NOT_NULL(resreq.select->root->u.binop.right);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_4b_true_resname_migration_error(void)
{
/* A bare "true" token in resname_leaf position must be rejected with a
* migration diagnostic (legacy boolean syntax is no longer accepted). */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "true", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc < 0);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_4b_false_resname_migration_error(void)
{
/* A bare "false" token in resname_leaf position must be rejected with a
* migration diagnostic (legacy boolean syntax is no longer accepted). */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "false", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc < 0);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_4b_bool_num_eq_accepted(void)
{
/* "gpu==1" where "gpu" is RES_TYPE_BOOL and "1" is SEL_NUM.
* BOOL × NUM equality is accepted per design (numeric 0/1 maps to
* false/true for boolean resources). */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "gpu==1", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
TEST_ASSERT_EQUAL_INT(SEL_EQ, resreq.select->root->type);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_4b_num_str_mismatch_ne_fails(void)
{
/* "mem!=linux" where "mem" is RES_TYPE_NUMERIC and "linux" is a string
* literal must be rejected for NE just as for EQ. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "mem!=linux", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc < 0);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_4b_bool_str_ne_fails(void)
{
/* "gpu!=true" where "gpu" is RES_TYPE_BOOL and "true" is a string literal
* must be rejected: boolean resource cannot be compared with a string. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "gpu!=true", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc < 0);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_4b_str_num_coercion_ne_succeeds(void)
{
/* "type!=5" where "type" is RES_TYPE_STRING and "5" is SEL_NUM.
* The numeric literal must be coerced to "5" and the NE comparison accepted. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq, "type!=5", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
TEST_ASSERT_EQUAL_INT(SEL_NE, resreq.select->root->type);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
/* ── 1-5: GC tests ────────────────────────────────────────────────────────*/
static void
test_gc_sweep_unreachable_freed(void)
{
/* After a sweep with no live jobs, every node in the table is
* unreachable. Re-interning the same arguments must return a fresh
* node — the old node was freed and removed from the table.
*
* We track node_id rather than pointer equality: calloc may lawfully
* reuse the freed address, so pointer-inequality is not a reliable
* indicator that GC swept correctly. */
sel_node_table_init();
sel_optimize_set(1);
double val = 99.0;
uintptr_t bits;
memcpy(&bits, &val, sizeof(bits));
xce_select_node_t *node1 = makeSelectNode(SEL_NUM, bits, 0);
TEST_ASSERT_NOT_NULL(node1);
uint32_t node1_id = node1->node_id;
/* No live jobs → all nodes are unreachable */
gc_clear_marks();
gc_sweep_select_nodes(NULL, 0);
/* Re-intern: node1 was freed; a new node must be allocated.
* node_id is monotonically assigned — a fresh intern produces a
* different id even if the heap address happens to be reused. */
xce_select_node_t *node2 = makeSelectNode(SEL_NUM, bits, 0);
TEST_ASSERT_NOT_NULL(node2);
TEST_ASSERT_NOT_EQUAL(node1_id, node2->node_id);
sel_optimize_test_reset();
}
static void
test_gc_sweep_reachable_retained(void)
{
/* A node referenced by a live job must survive GC with its node_id
* unchanged. In Phase 1 the xce_sched_job_t struct is opaque and the
* live-job marking path is not yet implemented, so this test validates
* the weaker invariant: a node that has just been created has a stable
* node_id when no sweep has modified it.
*
* Phase 6 extends this test to pass a real live_jobs array so that the
* mark pass actually retains the node. */
sel_node_table_init();
sel_optimize_set(1);
double val = 42.0;
uintptr_t bits;
memcpy(&bits, &val, sizeof(bits));
xce_select_node_t *node = makeSelectNode(SEL_NUM, bits, 0);
TEST_ASSERT_NOT_NULL(node);
uint32_t old_id = node->node_id;
/* node_id is assigned at intern time and must not change before a sweep */
TEST_ASSERT_EQUAL_UINT32(old_id, node->node_id);
sel_optimize_test_reset();
}
static void
test_gc_reintern_after_sweep(void)
{
/* After a sweep removes a node, re-creating the same expression must
* succeed — the intern table must accept the new node without
* stale-entry confusion. */
sel_node_table_init();
sel_optimize_set(1);
xce_select_node_t *n1 = makeSelectNode(SEL_TRUE, 0, 0);
TEST_ASSERT_NOT_NULL(n1);
gc_clear_marks();
gc_sweep_select_nodes(NULL, 0); /* sweep all — no live jobs */
/* Re-intern: must not crash and must return a valid (non-NULL) node */
xce_select_node_t *n2 = makeSelectNode(SEL_TRUE, 0, 0);
TEST_ASSERT_NOT_NULL(n2);
sel_optimize_test_reset();
}
/* ── 1-6: GC predicate contract tests ────────────────────────────────────
*
* Verify the three scheduler-state predicates used by the GC idle-phase
* guard (Phase 4f-4). Each test drives the fake_scheduler_t atomics into
* a specific state and asserts the expected predicate return value.
*/
static void
test_gc_predicate_idle(void)
{
/* When the scheduler is idle, all three predicates must report safe
* values: session_active == 0, eval_in_progress == 0,
* staging_merge_complete == 1. */
fake_scheduler_t sched;
memset(&sched, 0, sizeof(sched));
atomic_store_explicit(&sched.staging_merge_complete, 1,
memory_order_release);
TEST_ASSERT_EQUAL_INT(0, test_sched_session_active(&sched));
TEST_ASSERT_EQUAL_INT(0, test_sched_eval_in_progress(&sched));
TEST_ASSERT_EQUAL_INT(1, test_staging_merge_complete(&sched));
}
static void
test_gc_predicate_session_active_blocks(void)
{
/* session_active == 1 indicates an active scheduling session; GC must
* not run. The predicate must return 1. */
fake_scheduler_t sched;
memset(&sched, 0, sizeof(sched));
atomic_store_explicit(&sched.session_active, 1, memory_order_release);
atomic_store_explicit(&sched.staging_merge_complete, 1,
memory_order_release);
TEST_ASSERT_EQUAL_INT(1, test_sched_session_active(&sched));
}
static void
test_gc_predicate_eval_in_progress_blocks(void)
{
/* eval_in_progress == 1 means host evaluation is underway; GC must
* not run. The predicate must return 1. */
fake_scheduler_t sched;
memset(&sched, 0, sizeof(sched));
atomic_store_explicit(&sched.eval_in_progress, 1, memory_order_release);
atomic_store_explicit(&sched.staging_merge_complete, 1,
memory_order_release);
TEST_ASSERT_EQUAL_INT(1, test_sched_eval_in_progress(&sched));
}
static void
test_gc_predicate_staging_not_complete_blocks(void)
{
/* staging_merge_complete == 0 means the staging area has not been
* merged into the live tree yet; GC must not run. The predicate
* must return 0. */
fake_scheduler_t sched;
memset(&sched, 0, sizeof(sched));
/* staging_merge_complete stays 0 (zeroed by memset) */
TEST_ASSERT_EQUAL_INT(0, test_staging_merge_complete(&sched));
}
/* ── Phase 4f: recompact_node_ids tests ──────────────────────────────────
*
* 4f-5: recompact_node_ids must renumber live nodes to dense 0..N-1 and
* reset sel_node_next_id so subsequent allocations continue from N.
*/
static void
test_recompact_ids_dense(void)
{
/* Create 3 distinct SEL_NUM nodes; after recompact their node_ids must
* be 0, 1, 2 in insertion order. */
sel_node_table_init();
sel_optimize_set(1);
double v1 = 1.0, v2 = 2.0, v3 = 3.0;
uintptr_t b1, b2, b3;
memcpy(&b1, &v1, sizeof(b1));
memcpy(&b2, &v2, sizeof(b2));
memcpy(&b3, &v3, sizeof(b3));
xce_select_node_t *n1 = makeSelectNode(SEL_NUM, b1, 0);
xce_select_node_t *n2 = makeSelectNode(SEL_NUM, b2, 0);
xce_select_node_t *n3 = makeSelectNode(SEL_NUM, b3, 0);
TEST_ASSERT_NOT_NULL(n1);
TEST_ASSERT_NOT_NULL(n2);
TEST_ASSERT_NOT_NULL(n3);
recompact_node_ids();
/* IDs must be dense starting from 0 */
TEST_ASSERT_EQUAL_UINT32(0, n1->node_id);
TEST_ASSERT_EQUAL_UINT32(1, n2->node_id);
TEST_ASSERT_EQUAL_UINT32(2, n3->node_id);
sel_optimize_test_reset();
}
static void
test_recompact_resets_counter(void)
{
/* After sweeping all nodes and reinternng 1, recompact must reset
* sel_node_next_id to 1 so the next new node gets id == 1 (not a
* stale large value). */
sel_node_table_init();
sel_optimize_set(1);
/* Create and sweep two nodes to advance the counter */
double va = 100.0, vb = 200.0;
uintptr_t ba, bb;
memcpy(&ba, &va, sizeof(ba));
memcpy(&bb, &vb, sizeof(bb));
makeSelectNode(SEL_NUM, ba, 0);
makeSelectNode(SEL_NUM, bb, 0);
gc_clear_marks();
gc_sweep_select_nodes(NULL, 0); /* remove all — no live jobs */
/* Reintern exactly one node */
double vc = 300.0;
uintptr_t bc;
memcpy(&bc, &vc, sizeof(bc));
xce_select_node_t *n_live = makeSelectNode(SEL_NUM, bc, 0);
TEST_ASSERT_NOT_NULL(n_live);
recompact_node_ids();
/* The 1 live node gets id 0 */
TEST_ASSERT_EQUAL_UINT32(0, n_live->node_id);
/* The NEXT new node must get id 1 (counter reset to 1 by recompact) */
double vd = 400.0;
uintptr_t bd;
memcpy(&bd, &vd, sizeof(bd));
xce_select_node_t *n_new = makeSelectNode(SEL_NUM, bd, 0);
TEST_ASSERT_NOT_NULL(n_new);
TEST_ASSERT_EQUAL_UINT32(1, n_new->node_id);
sel_optimize_test_reset();
}
static void
test_recompact_empty_table(void)
{
/* recompact on an empty table must not crash and must leave the
* counter at 0. */
sel_node_table_init();
sel_optimize_set(1);
gc_clear_marks();
gc_sweep_select_nodes(NULL, 0); /* sweep any residual nodes */
recompact_node_ids();
/* After recompact the first new node must get id == 0 */
double vx = 999.0;
uintptr_t bx;
memcpy(&bx, &vx, sizeof(bx));
xce_select_node_t *n = makeSelectNode(SEL_NUM, bx, 0);
TEST_ASSERT_NOT_NULL(n);
TEST_ASSERT_EQUAL_UINT32(0, n->node_id);
sel_optimize_test_reset();
}
/* ── sel_optimize_level write-once CAS (task 1-2) ────────────────────────*/
static void
test_optimize_set_once(void)
{
sel_optimize_set(1);
TEST_ASSERT_EQUAL_INT(1, sel_get_optimize_level());
/* Second call must not overwrite — write-once CAS */
sel_optimize_set(0);
TEST_ASSERT_EQUAL_INT(1, sel_get_optimize_level());
sel_optimize_test_reset();
}
static void
test_optimize_reset_restores_unset(void)
{
sel_optimize_set(2);
TEST_ASSERT_EQUAL_INT(2, sel_get_optimize_level());
sel_optimize_test_reset();
TEST_ASSERT_EQUAL_INT(-1, sel_get_optimize_level());
/* After reset, a new set must succeed */
sel_optimize_set(0);
TEST_ASSERT_EQUAL_INT(0, sel_get_optimize_level());
sel_optimize_test_reset();
}
/* ── 4c-1: Commutativity: GT is non-commutative ──────────────────────────*/
static void
test_cse_gt_not_commutative(void)
{
/* In CSE mode, makeSelectNode(SEL_GT, A, B) and makeSelectNode(SEL_GT, B, A)
* must return DIFFERENT interned pointers — GT is not commutative. */
sel_optimize_test_reset(); /* ensure clean state */
sel_node_table_init();
sel_optimize_set(1);
xce_select_node_t *a = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_MEM_IDX, 0);
double val = 50.0;
uintptr_t vbits;
memcpy(&vbits, &val, sizeof(vbits));
xce_select_node_t *b = makeSelectNode(SEL_NUM, vbits, 0);
TEST_ASSERT_NOT_NULL(a);
TEST_ASSERT_NOT_NULL(b);
xce_select_node_t *ab = makeSelectNode(SEL_GT, (uintptr_t)a, (uintptr_t)b);
xce_select_node_t *ba = makeSelectNode(SEL_GT, (uintptr_t)b, (uintptr_t)a);
TEST_ASSERT_NOT_NULL(ab);
TEST_ASSERT_NOT_NULL(ba);
/* GT is NOT commutative — must yield distinct interned pointers */
TEST_ASSERT_TRUE(ab != ba);
sel_optimize_test_reset();
}
/* ── 4c-2/3: Flatten pass tests ─────────────────────────────────────────*/
static void
test_flatten_noop_left_assoc(void)
{
/* An already left-associative AND chain must not be re-ordered.
* "(type==linux && gpu) && mem>50" is left-assoc; root stays SEL_AND with
* left=SEL_AND after the flatten pass. */
sel_optimize_test_reset(); /* ensure clean state */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
int rc = parse_resreq_primary(&resreq,
"(type==linux && gpu) && mem>50",
resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
xce_select_node_t *root = resreq.select->root;
/* Already left-assoc: root = AND(AND(...), ...) */
TEST_ASSERT_EQUAL_INT(SEL_AND, root->type);
TEST_ASSERT_EQUAL_INT(SEL_AND, root->u.binop.left->type);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_flatten_right_assoc_disabled(void)
{
/* In level==0, a right-associative AND chain must be flattened to
* left-associative by sel_flatten_pass.
* Build AND(type==linux, AND(gpu, mem>50)) manually, apply one pass.
* Result must be AND(AND(type==linux, gpu), mem>50). */
sel_optimize_test_reset(); /* ensure clean state */
sel_optimize_set(0);
/* Build leaves */
xce_select_node_t *type_node = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_TYPE_IDX,
(uintptr_t)RES_TYPE_STRING);
double val = 50.0;
uintptr_t vbits;
memcpy(&vbits, &val, sizeof(vbits));
xce_select_node_t *mem_node = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_MEM_IDX,
(uintptr_t)RES_TYPE_NUMERIC);
xce_select_node_t *num_node = makeSelectNode(SEL_NUM, vbits, 0);
xce_select_node_t *gpu_node = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_GPU_IDX,
(uintptr_t)RES_TYPE_BOOL);
TEST_ASSERT_NOT_NULL(type_node);
TEST_ASSERT_NOT_NULL(mem_node);
TEST_ASSERT_NOT_NULL(num_node);
TEST_ASSERT_NOT_NULL(gpu_node);
/* inner = AND(gpu, mem>50) — right child */
xce_select_node_t *gt_node = makeSelectNode(SEL_GT,
(uintptr_t)mem_node,
(uintptr_t)num_node);
xce_select_node_t *inner = makeSelectNode(SEL_AND,
(uintptr_t)gpu_node,
(uintptr_t)gt_node);
/* root = AND(type==linux, AND(gpu, mem>50)) — right-assoc */
xce_select_node_t *root = makeSelectNode(SEL_AND,
(uintptr_t)type_node,
(uintptr_t)inner);
TEST_ASSERT_NOT_NULL(gt_node);
TEST_ASSERT_NOT_NULL(inner);
TEST_ASSERT_NOT_NULL(root);
/* Apply flatten pass */
int changed = 0;
xce_select_node_t *new_root = sel_flatten_pass(root, &changed);
TEST_ASSERT_NOT_NULL(new_root);
TEST_ASSERT_EQUAL_INT(1, changed);
/* Result must be left-assoc: AND(AND(A, B), C) */
TEST_ASSERT_EQUAL_INT(SEL_AND, new_root->type);
TEST_ASSERT_EQUAL_INT(SEL_AND, new_root->u.binop.left->type);
/* The right child must NOT be an AND (it should be a leaf or comparison) */
TEST_ASSERT_TRUE(new_root->u.binop.right->type != SEL_AND);
/* Cleanup: new_root is a fresh tree in level==0 */
sel_free_tree(new_root);
sel_optimize_test_reset();
}
static void
test_flatten_right_assoc_cse_same_ptr(void)
{
/* In CSE mode (level==1), sel_flatten_pass on AND(A, AND(B, C)) must
* produce a left-associative result: the root is AND with a left child
* that is also AND.
*
* Note on pointer equality: CSE commutativity sorting reorders operands
* by pointer value, so AND(A, AND(B,C)) after flatten may differ in its
* internal B/C ordering from a freshly-built AND(AND(A,B), C). We verify
* the structural shape (left-assoc) rather than pointer identity. */
sel_optimize_test_reset(); /* ensure clean state */
sel_node_table_init();
sel_optimize_set(1);
xce_select_node_t *a = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_TYPE_IDX, 0);
xce_select_node_t *b = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_GPU_IDX, 0);
xce_select_node_t *c = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_MEM_IDX, 0);
TEST_ASSERT_NOT_NULL(a);
TEST_ASSERT_NOT_NULL(b);
TEST_ASSERT_NOT_NULL(c);
/* Build right-assoc: AND(A, AND(B, C)) */
xce_select_node_t *bc_node = makeSelectNode(SEL_AND,
(uintptr_t)b, (uintptr_t)c);
xce_select_node_t *right_assoc = makeSelectNode(SEL_AND,
(uintptr_t)a, (uintptr_t)bc_node);
TEST_ASSERT_NOT_NULL(bc_node);
TEST_ASSERT_NOT_NULL(right_assoc);
/* In CSE mode, commutativity sort may place bc_node on either left or right
* depending on pointer ordering. Verify that one child is the AND node. */
int bc_is_left = (right_assoc->u.binop.left == bc_node);
int bc_is_right = (right_assoc->u.binop.right == bc_node);
TEST_ASSERT_TRUE(bc_is_left || bc_is_right);
/* Flatten: whichever child is AND must be re-associated */
int changed = 0;
xce_select_node_t *flattened = sel_flatten_pass(right_assoc, &changed);
TEST_ASSERT_NOT_NULL(flattened);
/* After flatten: root is AND and one of its two direct children is AND
* (the newly formed inner node), making it left-associative in structure.
* Because CSE may re-sort again, we check that the result is structurally
* an AND of (AND-child, leaf) regardless of which side each ends up on. */
TEST_ASSERT_EQUAL_INT(SEL_AND, flattened->type);
int left_is_and = (flattened->u.binop.left->type == SEL_AND);
int right_is_and = (flattened->u.binop.right->type == SEL_AND);
/* Exactly one child should be an AND (the inner pair) */
TEST_ASSERT_TRUE(left_is_and || right_is_and);
TEST_ASSERT_TRUE(changed == 1 || changed == 0); /* changed is set if re-assoc */
/* In CSE mode, re-building the same expression using the flattened tree's
* own children must return the already-interned pointer. */
xce_select_node_t *and_child = left_is_and
? flattened->u.binop.left
: flattened->u.binop.right;
xce_select_node_t *leaf_child = left_is_and
? flattened->u.binop.right
: flattened->u.binop.left;
xce_select_node_t *inner_l = and_child->u.binop.left;
xce_select_node_t *inner_r = and_child->u.binop.right;
xce_select_node_t *rebuilt_inner = makeSelectNode(SEL_AND,
(uintptr_t)inner_l,
(uintptr_t)inner_r);
xce_select_node_t *rebuilt_outer = makeSelectNode(SEL_AND,
(uintptr_t)rebuilt_inner,
(uintptr_t)leaf_child);
TEST_ASSERT_NOT_NULL(rebuilt_inner);
TEST_ASSERT_NOT_NULL(rebuilt_outer);
/* CSE must return the already-interned pointer for the same expression */
TEST_ASSERT_EQUAL_PTR(and_child, rebuilt_inner);
TEST_ASSERT_EQUAL_PTR(flattened, rebuilt_outer);
sel_optimize_test_reset();
}
static void
test_flatten_deep_nested_disabled(void)
{
/* In level==0, build AND(A, AND(B, AND(C, D))) — 3 levels of right-nesting.
* A do-while loop applying sel_flatten_pass must produce a fully
* left-associative result: AND(AND(AND(A,B),C),D). */
sel_optimize_test_reset(); /* ensure clean state from any prior failure */
sel_optimize_set(0);
xce_select_node_t *a = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_TYPE_IDX,
(uintptr_t)RES_TYPE_STRING);
xce_select_node_t *b = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_GPU_IDX,
(uintptr_t)RES_TYPE_BOOL);
xce_select_node_t *c = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_MEM_IDX,
(uintptr_t)RES_TYPE_NUMERIC);
xce_select_node_t *d = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_NCPUS_IDX,
(uintptr_t)RES_TYPE_NUMERIC);
TEST_ASSERT_NOT_NULL(a);
TEST_ASSERT_NOT_NULL(b);
TEST_ASSERT_NOT_NULL(c);
TEST_ASSERT_NOT_NULL(d);
/* AND(C, D) */
xce_select_node_t *cd = makeSelectNode(SEL_AND, (uintptr_t)c, (uintptr_t)d);
/* AND(B, AND(C, D)) */
xce_select_node_t *bcd = makeSelectNode(SEL_AND, (uintptr_t)b, (uintptr_t)cd);
/* AND(A, AND(B, AND(C, D))) */
xce_select_node_t *root = makeSelectNode(SEL_AND, (uintptr_t)a, (uintptr_t)bcd);
TEST_ASSERT_NOT_NULL(cd);
TEST_ASSERT_NOT_NULL(bcd);
TEST_ASSERT_NOT_NULL(root);
/* Apply flatten in a loop until no more changes */
int flat_changed;
do {
flat_changed = 0;
root = sel_flatten_pass(root, &flat_changed);
TEST_ASSERT_NOT_NULL(root);
} while (flat_changed);
/* Verify fully left-assoc: AND(AND(AND(A,B),C),D) */
TEST_ASSERT_EQUAL_INT(SEL_AND, root->type);
xce_select_node_t *lv1 = root->u.binop.left;
TEST_ASSERT_EQUAL_INT(SEL_AND, lv1->type);
xce_select_node_t *lv2 = lv1->u.binop.left;
TEST_ASSERT_EQUAL_INT(SEL_AND, lv2->type);
/* lv2->left must be a RESNAME leaf (A) */
TEST_ASSERT_EQUAL_INT(SEL_RESNAME, lv2->u.binop.left->type);
sel_free_tree(root);
sel_optimize_test_reset();
}
static void
test_flatten_or_right_assoc_disabled(void)
{
/* Same as test_flatten_right_assoc_disabled but with OR. */
sel_optimize_test_reset(); /* ensure clean state */
sel_optimize_set(0);
xce_select_node_t *a = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_TYPE_IDX,
(uintptr_t)RES_TYPE_STRING);
xce_select_node_t *b = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_GPU_IDX,
(uintptr_t)RES_TYPE_BOOL);
xce_select_node_t *c = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_MEM_IDX,
(uintptr_t)RES_TYPE_NUMERIC);
TEST_ASSERT_NOT_NULL(a);
TEST_ASSERT_NOT_NULL(b);
TEST_ASSERT_NOT_NULL(c);
/* OR(A, OR(B, C)) — right-assoc */
xce_select_node_t *inner = makeSelectNode(SEL_OR, (uintptr_t)b, (uintptr_t)c);
xce_select_node_t *root = makeSelectNode(SEL_OR, (uintptr_t)a, (uintptr_t)inner);
TEST_ASSERT_NOT_NULL(inner);
TEST_ASSERT_NOT_NULL(root);
int changed = 0;
xce_select_node_t *new_root = sel_flatten_pass(root, &changed);
TEST_ASSERT_NOT_NULL(new_root);
TEST_ASSERT_EQUAL_INT(1, changed);
/* Result: OR(OR(A, B), C) — left-assoc */
TEST_ASSERT_EQUAL_INT(SEL_OR, new_root->type);
TEST_ASSERT_EQUAL_INT(SEL_OR, new_root->u.binop.left->type);
TEST_ASSERT_TRUE(new_root->u.binop.right->type != SEL_OR);
sel_free_tree(new_root);
sel_optimize_test_reset();
}
static void
test_flatten_mixed_ops_no_change(void)
{
/* AND(A, OR(B, C)) — the inner node is OR, outer is AND.
* Different operators: flatten must NOT re-associate.
* Root stays AND with right=OR, and changed must be 0. */
sel_optimize_test_reset(); /* ensure clean state */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
/* "type==linux && (mem>50 || gpu)" parses as AND(EQ, OR(GT, RESNAME))
* which has different operators at the AND and OR levels — no flattening. */
int rc = parse_resreq_primary(&resreq,
"type==linux && (mem>50 || gpu)",
resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_NOT_NULL(resreq.select);
TEST_ASSERT_NOT_NULL(resreq.select->root);
xce_select_node_t *root = resreq.select->root;
/* Root must be AND */
TEST_ASSERT_EQUAL_INT(SEL_AND, root->type);
/* Right child must be OR — not re-associated by flatten */
TEST_ASSERT_EQUAL_INT(SEL_OR, root->u.binop.right->type);
/* Apply flatten — must be a no-op for mixed operators */
int changed = 0;
xce_select_node_t *new_root = sel_flatten_pass(root, &changed);
TEST_ASSERT_NOT_NULL(new_root);
/* No re-association occurred */
TEST_ASSERT_EQUAL_INT(0, changed);
TEST_ASSERT_EQUAL_INT(SEL_AND, new_root->type);
TEST_ASSERT_EQUAL_INT(SEL_OR, new_root->u.binop.right->type);
/* new_root == root in the no-change case (same pointer) */
TEST_ASSERT_EQUAL_PTR(root, new_root);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
/* ── 4d: const-fold transform tests ──────────────────────────────────────────
*
* All tests call sel_const_fold_pass() directly on manually-built AST nodes.
* The stub returns the input node unchanged, so every type assertion FAILS —
* that is the expected TDD RED state.
*
* Memory contract (level==0):
* After the real implementation: the pass frees consumed subtrees and
* returns a new (or surviving child) node. sel_free_tree(result) is
* always correct — it frees exactly what was returned.
* Stub case: result == input; sel_free_tree frees the whole tree.
*
* Memory contract (level==1, CSE):
* Do NOT call sel_free_tree; call sel_optimize_test_reset() to tear down
* the intern table and free all interned nodes.
*/
/* 4d-1: EQ(resname, any) → SEL_TRUE */
static void
test_constfold_eq_any_gives_true(void)
{
sel_optimize_set(0);
xce_select_node_t *resname = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_TYPE_IDX,
(uintptr_t)RES_TYPE_STRING);
xce_select_node_t *any = makeSelectNode(SEL_ANY, 0, 0);
xce_select_node_t *eq = makeSelectNode(SEL_EQ,
(uintptr_t)resname,
(uintptr_t)any);
TEST_ASSERT_NOT_NULL(resname);
TEST_ASSERT_NOT_NULL(any);
TEST_ASSERT_NOT_NULL(eq);
xce_select_node_t *result = sel_const_fold_pass(eq);
TEST_ASSERT_NOT_NULL(result);
/* Phase 4d: EQ(resname, any) must fold to SEL_TRUE */
TEST_ASSERT_EQUAL_INT(SEL_TRUE, result->type);
sel_free_tree(result);
sel_optimize_test_reset();
}
/* 4d-1: NE(resname, any) → SEL_FALSE */
static void
test_constfold_ne_any_gives_false(void)
{
sel_optimize_set(0);
xce_select_node_t *resname = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_TYPE_IDX,
(uintptr_t)RES_TYPE_STRING);
xce_select_node_t *any = makeSelectNode(SEL_ANY, 0, 0);
xce_select_node_t *ne = makeSelectNode(SEL_NE,
(uintptr_t)resname,
(uintptr_t)any);
TEST_ASSERT_NOT_NULL(resname);
TEST_ASSERT_NOT_NULL(any);
TEST_ASSERT_NOT_NULL(ne);
xce_select_node_t *result = sel_const_fold_pass(ne);
TEST_ASSERT_NOT_NULL(result);
/* Phase 4d: NE(resname, any) must fold to SEL_FALSE */
TEST_ASSERT_EQUAL_INT(SEL_FALSE, result->type);
sel_free_tree(result);
sel_optimize_test_reset();
}
/* 4d-2: AND(SEL_TRUE, X) → X (identity law) */
static void
test_constfold_true_and_x_gives_x(void)
{
sel_optimize_set(0);
xce_select_node_t *t_node = makeSelectNode(SEL_TRUE, 0, 0);
xce_select_node_t *x_node = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_GPU_IDX,
(uintptr_t)RES_TYPE_BOOL);
xce_select_node_t *and_node = makeSelectNode(SEL_AND,
(uintptr_t)t_node,
(uintptr_t)x_node);
TEST_ASSERT_NOT_NULL(t_node);
TEST_ASSERT_NOT_NULL(x_node);
TEST_ASSERT_NOT_NULL(and_node);
xce_select_node_t *result = sel_const_fold_pass(and_node);
TEST_ASSERT_NOT_NULL(result);
/* Phase 4d: TRUE && X must reduce to X (SEL_RESNAME) */
TEST_ASSERT_EQUAL_INT(SEL_RESNAME, result->type);
sel_free_tree(result);
sel_optimize_test_reset();
}
/* 4d-2: OR(SEL_FALSE, X) → X (identity law) */
static void
test_constfold_false_or_x_gives_x(void)
{
sel_optimize_set(0);
xce_select_node_t *f_node = makeSelectNode(SEL_FALSE, 0, 0);
xce_select_node_t *x_node = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_GPU_IDX,
(uintptr_t)RES_TYPE_BOOL);
xce_select_node_t *or_node = makeSelectNode(SEL_OR,
(uintptr_t)f_node,
(uintptr_t)x_node);
TEST_ASSERT_NOT_NULL(f_node);
TEST_ASSERT_NOT_NULL(x_node);
TEST_ASSERT_NOT_NULL(or_node);
xce_select_node_t *result = sel_const_fold_pass(or_node);
TEST_ASSERT_NOT_NULL(result);
/* Phase 4d: FALSE || X must reduce to X (SEL_RESNAME) */
TEST_ASSERT_EQUAL_INT(SEL_RESNAME, result->type);
sel_free_tree(result);
sel_optimize_test_reset();
}
/* 4d-3: AND(SEL_FALSE, X) → SEL_FALSE (annihilation law) */
static void
test_constfold_false_and_x_gives_false(void)
{
sel_optimize_set(0);
xce_select_node_t *f_node = makeSelectNode(SEL_FALSE, 0, 0);
xce_select_node_t *x_node = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_GPU_IDX,
(uintptr_t)RES_TYPE_BOOL);
xce_select_node_t *and_node = makeSelectNode(SEL_AND,
(uintptr_t)f_node,
(uintptr_t)x_node);
TEST_ASSERT_NOT_NULL(f_node);
TEST_ASSERT_NOT_NULL(x_node);
TEST_ASSERT_NOT_NULL(and_node);
xce_select_node_t *result = sel_const_fold_pass(and_node);
TEST_ASSERT_NOT_NULL(result);
/* Phase 4d: FALSE && X must annihilate to SEL_FALSE */
TEST_ASSERT_EQUAL_INT(SEL_FALSE, result->type);
sel_free_tree(result);
sel_optimize_test_reset();
}
/* 4d-3: OR(SEL_TRUE, X) → SEL_TRUE (annihilation law) */
static void
test_constfold_true_or_x_gives_true(void)
{
sel_optimize_set(0);
xce_select_node_t *t_node = makeSelectNode(SEL_TRUE, 0, 0);
xce_select_node_t *x_node = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_GPU_IDX,
(uintptr_t)RES_TYPE_BOOL);
xce_select_node_t *or_node = makeSelectNode(SEL_OR,
(uintptr_t)t_node,
(uintptr_t)x_node);
TEST_ASSERT_NOT_NULL(t_node);
TEST_ASSERT_NOT_NULL(x_node);
TEST_ASSERT_NOT_NULL(or_node);
xce_select_node_t *result = sel_const_fold_pass(or_node);
TEST_ASSERT_NOT_NULL(result);
/* Phase 4d: TRUE || X must annihilate to SEL_TRUE */
TEST_ASSERT_EQUAL_INT(SEL_TRUE, result->type);
sel_free_tree(result);
sel_optimize_test_reset();
}
/* 4d-4: NOT(SEL_TRUE) → SEL_FALSE (complement law) */
static void
test_constfold_not_true_gives_false(void)
{
sel_optimize_set(0);
xce_select_node_t *t_node = makeSelectNode(SEL_TRUE, 0, 0);
xce_select_node_t *not_node = makeSelectNode(SEL_NOT, (uintptr_t)t_node, 0);
TEST_ASSERT_NOT_NULL(t_node);
TEST_ASSERT_NOT_NULL(not_node);
xce_select_node_t *result = sel_const_fold_pass(not_node);
TEST_ASSERT_NOT_NULL(result);
/* Phase 4d: !TRUE must fold to SEL_FALSE */
TEST_ASSERT_EQUAL_INT(SEL_FALSE, result->type);
sel_free_tree(result);
sel_optimize_test_reset();
}
/* 4d-4: NOT(SEL_FALSE) → SEL_TRUE (complement law) */
static void
test_constfold_not_false_gives_true(void)
{
sel_optimize_set(0);
xce_select_node_t *f_node = makeSelectNode(SEL_FALSE, 0, 0);
xce_select_node_t *not_node = makeSelectNode(SEL_NOT, (uintptr_t)f_node, 0);
TEST_ASSERT_NOT_NULL(f_node);
TEST_ASSERT_NOT_NULL(not_node);
xce_select_node_t *result = sel_const_fold_pass(not_node);
TEST_ASSERT_NOT_NULL(result);
/* Phase 4d: !FALSE must fold to SEL_TRUE */
TEST_ASSERT_EQUAL_INT(SEL_TRUE, result->type);
sel_free_tree(result);
sel_optimize_test_reset();
}
/* 4d-1 (CSE): folding the same interned EQ(resname, any) must produce the
* same interned SEL_TRUE singleton — const-fold in CSE mode must return an
* interned pointer, not a fresh allocation. */
static void
test_constfold_cse_eq_any_singleton(void)
{
sel_optimize_test_reset();
sel_node_table_init();
sel_optimize_set(1);
xce_select_node_t *resname = makeSelectNode(SEL_RESNAME,
(uintptr_t)TEST_RES_TYPE_IDX,
(uintptr_t)RES_TYPE_STRING);
xce_select_node_t *any = makeSelectNode(SEL_ANY, 0, 0);
xce_select_node_t *eq1 = makeSelectNode(SEL_EQ,
(uintptr_t)resname,
(uintptr_t)any);
TEST_ASSERT_NOT_NULL(resname);
TEST_ASSERT_NOT_NULL(any);
TEST_ASSERT_NOT_NULL(eq1);
/* CSE pre-condition: same args → same interned pointer */
xce_select_node_t *eq2 = makeSelectNode(SEL_EQ,
(uintptr_t)resname,
(uintptr_t)any);
TEST_ASSERT_EQUAL_PTR(eq1, eq2);
/* Phase 4d: both folds must yield SEL_TRUE and the same singleton */
xce_select_node_t *fold1 = sel_const_fold_pass(eq1);
xce_select_node_t *fold2 = sel_const_fold_pass(eq2);
TEST_ASSERT_NOT_NULL(fold1);
TEST_ASSERT_NOT_NULL(fold2);
TEST_ASSERT_EQUAL_INT(SEL_TRUE, fold1->type);
TEST_ASSERT_EQUAL_INT(SEL_TRUE, fold2->type);
TEST_ASSERT_EQUAL_PTR(fold1, fold2);
sel_optimize_test_reset();
}
/* ── Phase 4e: Evaluation with Memoization ────────────────────────────────
*
* Tests for eval_select_simple (level==0, no ctx) and eval_select (CSE mode,
* memoized via select_eval_ctx_t). These tests verify actual evaluation
* results, not just AST structure.
*/
/* Helper: fill a stack-allocated host_eval_ctx_t */
static void
init_test_host(host_eval_ctx_t *host, double *numvals, const char **strvals,
int num_res)
{
host->num_res = num_res;
host->numvals = numvals;
host->strvals = strvals;
}
/* Five-resource host arrays matching test_resmgr_fixture:
* idx 0: type (STRING)
* idx 1: mem (NUMERIC)
* idx 2: gpu (BOOL)
* idx 3: ncpus (NUMERIC)
* idx 4: arch (STRING)
*/
static void
test_p4e_simple_null_root(void)
{
/* eval_select_simple(NULL, host) → 1 (vacuously true) */
double numvals[5] = {0};
const char *strvals[5] = {NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
TEST_ASSERT_EQUAL_INT(1, eval_select_simple(NULL, &host));
}
static void
test_p4e_simple_bool_true(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 0.0, 1.0, 0.0, 0.0}; /* gpu=1 */
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "gpu", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_bool_false(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 0.0, 0.0, 0.0, 0.0}; /* gpu=0 */
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "gpu", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(0, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_numeric_gt_true(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0}; /* mem=100 */
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "mem>50", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_numeric_gt_false(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 30.0, 0.0, 0.0, 0.0}; /* mem=30 */
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "mem>50", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(0, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_numeric_lt_true(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0}; /* mem=100 < 200 */
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "mem<200", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_numeric_ge_boundary(void)
{
/* mem>=100 with mem=100: boundary case must be true */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0};
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "mem>=100", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_numeric_le_boundary(void)
{
/* mem<=100 with mem=100: boundary case must be true */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0};
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "mem<=100", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_numeric_eq_true(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0};
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "mem==100", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_numeric_eq_false(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 50.0, 0.0, 0.0, 0.0};
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "mem==100", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(0, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_numeric_ne_true(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0};
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "mem!=50", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_string_eq_true(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 0.0, 0.0, 0.0, 0.0};
const char *strvals[5] = {"linux", NULL, NULL, NULL, NULL}; /* type="linux" */
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "type==linux", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_string_eq_false(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 0.0, 0.0, 0.0, 0.0};
const char *strvals[5] = {"windows", NULL, NULL, NULL, NULL}; /* type≠linux */
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "type==linux", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(0, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_not_inverts_true(void)
{
/* !gpu with gpu=0.0 → NOT false = 1 */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 0.0, 0.0, 0.0, 0.0}; /* gpu=0 */
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "!gpu", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_not_inverts_false(void)
{
/* !gpu with gpu=1.0 → NOT true = 0 */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 0.0, 1.0, 0.0, 0.0}; /* gpu=1 */
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "!gpu", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(0, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_and_both_true(void)
{
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 100.0, 1.0, 0.0, 0.0}; /* mem=100, gpu=1 */
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "gpu && mem>50", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_and_short_circuit_false(void)
{
/* gpu=0: AND short-circuits without evaluating mem>50 */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0}; /* gpu=0 */
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "gpu && mem>50", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(0, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_or_short_circuit_true(void)
{
/* gpu=1: OR short-circuits without evaluating mem>50 (which would be false) */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_optimize_set(0);
double numvals[5] = {0.0, 30.0, 1.0, 0.0, 0.0}; /* gpu=1, mem=30 */
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "gpu || mem>50", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
TEST_ASSERT_EQUAL_INT(1, eval_select_simple(resreq.select->root, &host));
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_simple_sel_true_node(void)
{
/* A SEL_TRUE leaf always evaluates to 1 */
sel_optimize_set(0);
xce_select_node_t *node = makeSelectNode(SEL_TRUE, 0, 0);
TEST_ASSERT_NOT_NULL(node);
double numvals[1] = {0};
const char *strvals[1] = {NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 1);
TEST_ASSERT_EQUAL_INT(1, eval_select_simple(node, &host));
sel_free_tree(node);
sel_optimize_test_reset();
}
static void
test_p4e_simple_sel_false_node(void)
{
/* A SEL_FALSE leaf always evaluates to 0 */
sel_optimize_set(0);
xce_select_node_t *node = makeSelectNode(SEL_FALSE, 0, 0);
TEST_ASSERT_NOT_NULL(node);
double numvals[1] = {0};
const char *strvals[1] = {NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 1);
TEST_ASSERT_EQUAL_INT(0, eval_select_simple(node, &host));
sel_free_tree(node);
sel_optimize_test_reset();
}
/* ── Phase 4e: ctx lifecycle and memoized eval ─────────────────────────── */
static void
test_p4e_ctx_init_allocs_non_null(void)
{
sel_optimize_set(1);
select_eval_ctx_t *ctx = select_eval_ctx_init(64);
TEST_ASSERT_NOT_NULL(ctx);
TEST_ASSERT_NOT_NULL(ctx->bool_bits);
TEST_ASSERT_NOT_NULL(ctx->num_vals);
TEST_ASSERT_NOT_NULL(ctx->str_vals);
select_eval_ctx_fini(ctx);
sel_optimize_test_reset();
}
static void
test_p4e_ctx_node_count_matches(void)
{
sel_optimize_set(1);
select_eval_ctx_t *ctx = select_eval_ctx_init(100);
TEST_ASSERT_NOT_NULL(ctx);
TEST_ASSERT_EQUAL_INT(100, (int)ctx->node_count);
TEST_ASSERT_EQUAL_INT(100, (int)ctx->bool_bits_cap);
TEST_ASSERT_EQUAL_INT(100, (int)ctx->num_vals_cap);
TEST_ASSERT_EQUAL_INT(100, (int)ctx->str_vals_cap);
select_eval_ctx_fini(ctx);
sel_optimize_test_reset();
}
static void
test_p4e_ctx_reset_sets_host(void)
{
sel_optimize_set(1);
select_eval_ctx_t *ctx = select_eval_ctx_init(64);
TEST_ASSERT_NOT_NULL(ctx);
double numvals[5] = {0};
const char *strvals[5] = {NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
select_eval_ctx_reset(ctx, &host);
TEST_ASSERT_EQUAL_PTR(&host, ctx->host);
select_eval_ctx_fini(ctx);
sel_optimize_test_reset();
}
static void
test_p4e_memoized_null_root(void)
{
/* eval_select(NULL, ctx) → 1 regardless of ctx state */
sel_optimize_set(1);
select_eval_ctx_t *ctx = select_eval_ctx_init(64);
TEST_ASSERT_NOT_NULL(ctx);
double numvals[5] = {0};
const char *strvals[5] = {NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
select_eval_ctx_reset(ctx, &host);
TEST_ASSERT_EQUAL_INT(1, eval_select(NULL, ctx));
select_eval_ctx_fini(ctx);
sel_optimize_test_reset();
}
static void
test_p4e_memoized_numeric_gt_true(void)
{
/* eval_select (memoized) gives same correct result as eval_select_simple */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_node_table_init();
sel_optimize_set(1);
double numvals[5] = {0.0, 100.0, 0.0, 0.0, 0.0}; /* mem=100 */
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "mem>50", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
select_eval_ctx_t *ctx = select_eval_ctx_init(256);
TEST_ASSERT_NOT_NULL(ctx);
select_eval_ctx_reset(ctx, &host);
TEST_ASSERT_EQUAL_INT(1, eval_select(resreq.select->root, ctx));
select_eval_ctx_fini(ctx);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
static void
test_p4e_memoized_second_call_same(void)
{
/* Calling eval_select twice with the same ctx must return the same value.
* The second call hits the bool_bits cache instead of re-evaluating. */
char * errmsg = NULL;
xce_primary_resreq_t resreq;
var resmgr = make_test_resmgr();
sel_node_table_init();
sel_optimize_set(1);
double numvals[5] = {0.0, 100.0, 1.0, 0.0, 0.0}; /* mem=100, gpu=1 */
const char *strvals[5] = {NULL, NULL, NULL, NULL, NULL};
host_eval_ctx_t host;
init_test_host(&host, numvals, strvals, 5);
int rc = parse_resreq_primary(&resreq, "gpu && mem>50", resmgr, &errmsg);
TEST_ASSERT_TRUE(rc >= 0);
select_eval_ctx_t *ctx = select_eval_ctx_init(256);
TEST_ASSERT_NOT_NULL(ctx);
select_eval_ctx_reset(ctx, &host);
int result1 = eval_select(resreq.select->root, ctx);
int result2 = eval_select(resreq.select->root, ctx);
TEST_ASSERT_EQUAL_INT(1, result1);
TEST_ASSERT_EQUAL_INT(result1, result2);
select_eval_ctx_fini(ctx);
kill_primary_resreq(&resreq);
FREEUP(errmsg);
free_test_resmgr(resmgr);
sel_optimize_test_reset();
}
/* ── Phase 6 prep: gc_clear_marks / gc_mark_select_tree exported ──────────*/
static void
test_gc_clear_marks_exported(void)
{
sel_optimize_test_reset();
sel_optimize_set(1);
sel_node_table_init();
double d1 = 1.0, d2 = 2.0;
uintptr_t b1, b2;
memcpy(&b1, &d1, sizeof(b1));
memcpy(&b2, &d2, sizeof(b2));
xce_select_node_t* n1 = makeSelectNode(SEL_NUM, b1, 0);
xce_select_node_t* n2 = makeSelectNode(SEL_NUM, b2, 0);
TEST_ASSERT_NOT_NULL(n1);
TEST_ASSERT_NOT_NULL(n2);
uint32_t n1_id = n1->node_id;
/* manually mark n1 */
n1->gc_mark = 1;
/* gc_clear_marks must reset all gc_mark bits */
gc_clear_marks();
TEST_ASSERT_EQUAL_INT(0, n1->gc_mark);
/* gc_mark_select_tree must set gc_mark on n2 */
gc_mark_select_tree(n2);
TEST_ASSERT_EQUAL_INT(1, n2->gc_mark);
/* sweep preserves n2 (marked), removes n1 (unmarked) */
gc_sweep_select_nodes(NULL, 0);
TEST_ASSERT_EQUAL_INT(1, n2->gc_mark);
/* n1 was swept — re-interning must produce a fresh node_id.
* Pointer inequality would be unreliable because calloc may
* lawfully reuse the freed address. */
xce_select_node_t* n1_again = makeSelectNode(SEL_NUM, b1, 0);
TEST_ASSERT_NOT_NULL(n1_again);
TEST_ASSERT_NOT_EQUAL(n1_id, n1_again->node_id);
sel_node_table_fini();
sel_optimize_test_reset();
}
/* ── Task 8: sel_alloc_thread_id ownership check ─────────────────────────*/
static void
test_sel_register_alloc_thread(void)
{
sel_optimize_test_reset(); /* resets sel_alloc_thread_id to 0 */
sel_optimize_set(1);
sel_node_table_init();
/* Should not crash when unregistered (id=0 means skip check) */
xce_select_node_t* n = makeSelectNode(SEL_TRUE, 0, 0);
TEST_ASSERT_NOT_NULL(n);
/* Register current thread */
sel_register_alloc_thread();
/* makeSelectNode from same thread must still work */
xce_select_node_t* n2 = makeSelectNode(SEL_FALSE, 0, 0);
TEST_ASSERT_NOT_NULL(n2);
gc_clear_marks();
gc_sweep_select_nodes(NULL, 0);
sel_node_table_fini();
sel_optimize_test_reset();
}
/* ── Task 8: wrong-thread ownership guard test ───────────────────────────*/
/* shared state for the wrong-thread sweep test */
static int g_wrong_thread_sweep_ran = 0;
static void* wrong_thread_sweep(void* arg)
{
(void)arg;
/* calling gc_sweep from the wrong thread must hit the ownership guard
* and return without sweeping — nodes must remain intact */
gc_sweep_select_nodes(NULL, 0);
g_wrong_thread_sweep_ran = 1;
return NULL;
}
static void
test_sel_wrong_thread_guard(void)
{
sel_optimize_test_reset();
sel_optimize_set(1);
sel_node_table_init();
/* Register THIS thread as the allocator */
sel_register_alloc_thread();
/* Allocate a node that should survive the wrong-thread sweep */
xce_select_node_t* n = makeSelectNode(SEL_TRUE, 0, 0);
TEST_ASSERT_NOT_NULL(n);
/* Spawn a different thread to call gc_sweep — guard must block it */
pthread_t tid;
g_wrong_thread_sweep_ran = 0;
int rc = pthread_create(&tid, NULL, wrong_thread_sweep, NULL);
TEST_ASSERT_EQUAL_INT(0, rc);
pthread_join(tid, NULL);
TEST_ASSERT_EQUAL_INT(1, g_wrong_thread_sweep_ran);
/* Node must still exist (wrong-thread sweep was blocked) */
/* GC from correct thread: mark then sweep normally */
gc_clear_marks();
gc_sweep_select_nodes(NULL, 0);
sel_node_table_fini();
sel_optimize_test_reset();
}
/* ── Top-level runner ─────────────────────────────────────────────────────*/
void
test_select(void)
{
printf("\n>> select expression AST / CSE test\n\n");
xstring_table_init(139);
RUN_TEST(test_optimize_set_once);
RUN_TEST(test_optimize_reset_restores_unset);
RUN_TEST(test_select_parse_valid_expr);
RUN_TEST(test_select_parse_valid_any);
RUN_TEST(test_select_parse_unknown_resource);
RUN_TEST(test_select_parse_type_mismatch);
RUN_TEST(test_select_input_len_overflow);
RUN_TEST(test_select_depth_overflow);
RUN_TEST(test_select_cse_commutative_same_ptr);
RUN_TEST(test_select_cse_same_args_same_ptr);
RUN_TEST(test_eval_numeric_gt);
RUN_TEST(test_eval_numeric_lt);
RUN_TEST(test_eval_numeric_eq);
RUN_TEST(test_eval_string_eq);
RUN_TEST(test_eval_bool_standalone);
RUN_TEST(test_eval_not);
RUN_TEST(test_eval_and_shortcircuit);
RUN_TEST(test_eval_or_shortcircuit);
RUN_TEST(test_ordered_cmp_nonnum_string_resource_fails);
RUN_TEST(test_ordered_cmp_nonnum_bool_resource_fails);
RUN_TEST(test_ordered_cmp_string_rhs_fails);
RUN_TEST(test_ordered_cmp_numeric_resource_succeeds);
/* ── Phase 4b: EQ type-validation tests ─────────────────────────────── */
RUN_TEST(test_4b_num_str_mismatch_eq_fails);
RUN_TEST(test_4b_str_num_coercion_eq_succeeds);
RUN_TEST(test_4b_true_resname_migration_error);
RUN_TEST(test_4b_false_resname_migration_error);
RUN_TEST(test_4b_bool_num_eq_accepted);
RUN_TEST(test_4b_num_str_mismatch_ne_fails);
RUN_TEST(test_4b_bool_str_ne_fails);
RUN_TEST(test_4b_str_num_coercion_ne_succeeds);
RUN_TEST(test_gc_sweep_unreachable_freed);
RUN_TEST(test_gc_sweep_reachable_retained);
RUN_TEST(test_gc_reintern_after_sweep);
RUN_TEST(test_gc_predicate_idle);
RUN_TEST(test_gc_predicate_session_active_blocks);
RUN_TEST(test_gc_predicate_eval_in_progress_blocks);
RUN_TEST(test_gc_predicate_staging_not_complete_blocks);
/* ── Phase 4f: recompact tests ───────────────────────────────────── */
RUN_TEST(test_recompact_ids_dense);
RUN_TEST(test_recompact_resets_counter);
RUN_TEST(test_recompact_empty_table);
/* ── Phase 4c tests ───────────────────────────────────────────────── */
RUN_TEST(test_cse_gt_not_commutative);
RUN_TEST(test_flatten_noop_left_assoc);
RUN_TEST(test_flatten_right_assoc_disabled);
RUN_TEST(test_flatten_right_assoc_cse_same_ptr);
RUN_TEST(test_flatten_deep_nested_disabled);
RUN_TEST(test_flatten_or_right_assoc_disabled);
RUN_TEST(test_flatten_mixed_ops_no_change);
/* ── Phase 4d tests ───────────────────────────────────────────────── */
RUN_TEST(test_constfold_eq_any_gives_true);
RUN_TEST(test_constfold_ne_any_gives_false);
RUN_TEST(test_constfold_true_and_x_gives_x);
RUN_TEST(test_constfold_false_or_x_gives_x);
RUN_TEST(test_constfold_false_and_x_gives_false);
RUN_TEST(test_constfold_true_or_x_gives_true);
RUN_TEST(test_constfold_not_true_gives_false);
RUN_TEST(test_constfold_not_false_gives_true);
RUN_TEST(test_constfold_cse_eq_any_singleton);
/* ── Phase 4e tests ───────────────────────────────────────────────── */
RUN_TEST(test_p4e_simple_null_root);
RUN_TEST(test_p4e_simple_bool_true);
RUN_TEST(test_p4e_simple_bool_false);
RUN_TEST(test_p4e_simple_numeric_gt_true);
RUN_TEST(test_p4e_simple_numeric_gt_false);
RUN_TEST(test_p4e_simple_numeric_lt_true);
RUN_TEST(test_p4e_simple_numeric_ge_boundary);
RUN_TEST(test_p4e_simple_numeric_le_boundary);
RUN_TEST(test_p4e_simple_numeric_eq_true);
RUN_TEST(test_p4e_simple_numeric_eq_false);
RUN_TEST(test_p4e_simple_numeric_ne_true);
RUN_TEST(test_p4e_simple_string_eq_true);
RUN_TEST(test_p4e_simple_string_eq_false);
RUN_TEST(test_p4e_simple_not_inverts_true);
RUN_TEST(test_p4e_simple_not_inverts_false);
RUN_TEST(test_p4e_simple_and_both_true);
RUN_TEST(test_p4e_simple_and_short_circuit_false);
RUN_TEST(test_p4e_simple_or_short_circuit_true);
RUN_TEST(test_p4e_simple_sel_true_node);
RUN_TEST(test_p4e_simple_sel_false_node);
RUN_TEST(test_p4e_ctx_init_allocs_non_null);
RUN_TEST(test_p4e_ctx_node_count_matches);
RUN_TEST(test_p4e_ctx_reset_sets_host);
RUN_TEST(test_p4e_memoized_null_root);
RUN_TEST(test_p4e_memoized_numeric_gt_true);
RUN_TEST(test_p4e_memoized_second_call_same);
/* ── Phase 6 prep: exported GC mark primitives ───────────────────── */
RUN_TEST(test_gc_clear_marks_exported);
/* ── Task 8: sel_alloc_thread_id ownership check ─────────────────── */
RUN_TEST(test_sel_register_alloc_thread);
RUN_TEST(test_sel_wrong_thread_guard);
xstring_table_fini();
}