Merge branch 'persistent_ha' into scheduler
This commit is contained in:
commit
a07a74a943
106 changed files with 18528 additions and 637 deletions
|
|
@ -29,6 +29,11 @@ extern void test_jobobj(void);
|
|||
extern void test_qobj(void);
|
||||
extern void test_wobj(void);
|
||||
extern void test_request(void);
|
||||
extern void test_zarray_cow(void);
|
||||
extern void test_kvmgr_capture(void);
|
||||
extern void test_hooks(void);
|
||||
extern void test_cluster(void);
|
||||
extern void test_cluster_parse(void);
|
||||
|
||||
void setUp(void) { return; }
|
||||
|
||||
|
|
@ -127,6 +132,11 @@ int main(int argc, char* argv[])
|
|||
TEST(test_wobj);
|
||||
TEST(test_qobj);
|
||||
TEST(test_request);
|
||||
TEST(test_zarray_cow);
|
||||
TEST(test_kvmgr_capture);
|
||||
TEST(test_hooks);
|
||||
TEST(test_cluster);
|
||||
TEST(test_cluster_parse);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
181
src/lib/testsuite/test_cluster.c
Normal file
181
src/lib/testsuite/test_cluster.c
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "xce/cluster.h"
|
||||
|
||||
/* ── Test seam: mock connect function ───────────────────────────── */
|
||||
static int s_connect_call_count = 0;
|
||||
static int s_connect_fail_n = 0; /* fail first N attempts, then succeed */
|
||||
static char s_last_host[256] = "";
|
||||
static int s_last_port = 0;
|
||||
|
||||
static void* mock_connect(const char* host, int port, const char* ca_cert)
|
||||
{
|
||||
(void)ca_cert;
|
||||
strncpy(s_last_host, host, sizeof(s_last_host) - 1);
|
||||
s_last_port = port;
|
||||
s_connect_call_count++;
|
||||
if (s_connect_call_count <= s_connect_fail_n)
|
||||
return NULL;
|
||||
return (void*)0xDEADBEEF; /* non-NULL = success */
|
||||
}
|
||||
|
||||
static xce_cluster_config_t make_valid_config(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = {0};
|
||||
strncpy(cfg.candidates[0].hostname, "10.0.1.1",
|
||||
XCE_MAXHOSTNAME_LEN - 1);
|
||||
cfg.candidates[0].port = 7000;
|
||||
strncpy(cfg.candidates[1].hostname, "10.0.1.2",
|
||||
XCE_MAXHOSTNAME_LEN - 1);
|
||||
cfg.candidates[1].port = 7001;
|
||||
cfg.candidates_count = 2;
|
||||
strncpy(cfg.ca_cert_path, "/etc/xce/ca.pem",
|
||||
sizeof(cfg.ca_cert_path) - 1);
|
||||
strncpy(cfg.identity, "worker-01", sizeof(cfg.identity) - 1);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
/* ── Test cases ─────────────────────────────────────────────────── */
|
||||
|
||||
/* Test 1: validate rejects empty config */
|
||||
static void test_validate_rejects_zero_candidates(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = {0};
|
||||
cfg.candidates_count = 0;
|
||||
TEST_ASSERT_NOT_EQUAL(0, xce_cluster_config_validate(&cfg));
|
||||
printf("test_validate_rejects_zero_candidates: PASS\n");
|
||||
}
|
||||
|
||||
/* Test 2: validate rejects too many candidates */
|
||||
static void test_validate_rejects_over_max_candidates(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = {0};
|
||||
cfg.candidates_count = MAX_HA_CANDIDATES + 1;
|
||||
TEST_ASSERT_NOT_EQUAL(0, xce_cluster_config_validate(&cfg));
|
||||
printf("test_validate_rejects_over_max_candidates: PASS\n");
|
||||
}
|
||||
|
||||
/* Test 3: validate rejects missing identity */
|
||||
static void test_validate_rejects_empty_identity(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = make_valid_config();
|
||||
cfg.identity[0] = '\0';
|
||||
TEST_ASSERT_NOT_EQUAL(0, xce_cluster_config_validate(&cfg));
|
||||
printf("test_validate_rejects_empty_identity: PASS\n");
|
||||
}
|
||||
|
||||
/* Test 4: validate accepts valid config */
|
||||
static void test_validate_accepts_valid_config(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = make_valid_config();
|
||||
TEST_ASSERT_EQUAL_INT(0, xce_cluster_config_validate(&cfg));
|
||||
printf("test_validate_accepts_valid_config: PASS\n");
|
||||
}
|
||||
|
||||
/* Test 5: connect succeeds on first candidate */
|
||||
static void test_connect_succeeds_first_candidate(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = make_valid_config();
|
||||
s_connect_call_count = 0;
|
||||
s_connect_fail_n = 0;
|
||||
xce_cluster_set_connect_fn(mock_connect);
|
||||
void* ep = xce_connect_cluster(&cfg);
|
||||
TEST_ASSERT_NOT_NULL(ep);
|
||||
TEST_ASSERT_EQUAL_INT(1, s_connect_call_count);
|
||||
TEST_ASSERT_EQUAL_STRING("10.0.1.1", s_last_host);
|
||||
TEST_ASSERT_EQUAL_INT(7000, s_last_port);
|
||||
xce_cluster_set_connect_fn(NULL);
|
||||
printf("test_connect_succeeds_first_candidate: PASS\n");
|
||||
}
|
||||
|
||||
/* Test 6: connect falls back to second candidate when first fails */
|
||||
static void test_connect_fallback_to_second_candidate(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = make_valid_config();
|
||||
s_connect_call_count = 0;
|
||||
s_connect_fail_n = 1; /* first attempt fails */
|
||||
xce_cluster_set_connect_fn(mock_connect);
|
||||
void* ep = xce_connect_cluster(&cfg);
|
||||
TEST_ASSERT_NOT_NULL(ep);
|
||||
TEST_ASSERT_EQUAL_INT(2, s_connect_call_count);
|
||||
TEST_ASSERT_EQUAL_STRING("10.0.1.2", s_last_host);
|
||||
xce_cluster_set_connect_fn(NULL);
|
||||
printf("test_connect_fallback_to_second_candidate: PASS\n");
|
||||
}
|
||||
|
||||
/* Test 7: connect returns NULL when all candidates fail */
|
||||
static void test_connect_returns_null_all_fail(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = make_valid_config();
|
||||
s_connect_call_count = 0;
|
||||
s_connect_fail_n = 999; /* all fail */
|
||||
xce_cluster_set_connect_fn(mock_connect);
|
||||
void* ep = xce_connect_cluster(&cfg);
|
||||
TEST_ASSERT_NULL(ep);
|
||||
xce_cluster_set_connect_fn(NULL);
|
||||
printf("test_connect_returns_null_all_fail: PASS\n");
|
||||
}
|
||||
|
||||
/* Test 8: reconnect state machine — IDLE → CONNECTING → CONNECTED */
|
||||
static void test_reconnect_step_reaches_connected(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = make_valid_config();
|
||||
xce_reconnect_ctx_t rctx;
|
||||
s_connect_call_count = 0;
|
||||
s_connect_fail_n = 0;
|
||||
xce_cluster_set_connect_fn(mock_connect);
|
||||
xce_reconnect_init(&rctx, &cfg);
|
||||
TEST_ASSERT_EQUAL_INT(XCE_RECONNECT_IDLE, (int)rctx.state);
|
||||
|
||||
int steps = 0;
|
||||
while (rctx.state != XCE_RECONNECT_CONNECTED &&
|
||||
rctx.state != XCE_RECONNECT_FAILED && steps < 100) {
|
||||
xce_reconnect_step(&rctx);
|
||||
steps++;
|
||||
}
|
||||
TEST_ASSERT_EQUAL_INT(XCE_RECONNECT_CONNECTED, (int)rctx.state);
|
||||
TEST_ASSERT_NOT_NULL(rctx.endpoint);
|
||||
|
||||
xce_reconnect_fini(&rctx);
|
||||
xce_cluster_set_connect_fn(NULL);
|
||||
printf("test_reconnect_step_reaches_connected: PASS\n");
|
||||
}
|
||||
|
||||
/* Test 9: reconnect state machine backs off on failure */
|
||||
static void test_reconnect_step_backs_off_on_failure(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = make_valid_config();
|
||||
cfg.candidates_count = 1; /* only one candidate so it hits backoff */
|
||||
xce_reconnect_ctx_t rctx;
|
||||
s_connect_call_count = 0;
|
||||
s_connect_fail_n = 999; /* always fail */
|
||||
xce_cluster_set_connect_fn(mock_connect);
|
||||
xce_reconnect_init(&rctx, &cfg);
|
||||
|
||||
xce_reconnect_step(&rctx); /* IDLE → CONNECTING (attempt connect) */
|
||||
xce_reconnect_step(&rctx); /* CONNECTING → BACKOFF (all candidates exhausted) */
|
||||
/* When mock is set, sleep is skipped — state must be BACKOFF */
|
||||
TEST_ASSERT_EQUAL_INT(XCE_RECONNECT_BACKOFF, (int)rctx.state);
|
||||
TEST_ASSERT_TRUE(rctx.backoff_ms > 0);
|
||||
|
||||
xce_reconnect_fini(&rctx);
|
||||
xce_cluster_set_connect_fn(NULL);
|
||||
printf("test_reconnect_step_backs_off_on_failure: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test group entry point ─────────────────────────────────────── */
|
||||
void test_cluster(void)
|
||||
{
|
||||
RUN_TEST(test_validate_rejects_zero_candidates);
|
||||
RUN_TEST(test_validate_rejects_over_max_candidates);
|
||||
RUN_TEST(test_validate_rejects_empty_identity);
|
||||
RUN_TEST(test_validate_accepts_valid_config);
|
||||
RUN_TEST(test_connect_succeeds_first_candidate);
|
||||
RUN_TEST(test_connect_fallback_to_second_candidate);
|
||||
RUN_TEST(test_connect_returns_null_all_fail);
|
||||
RUN_TEST(test_reconnect_step_reaches_connected);
|
||||
RUN_TEST(test_reconnect_step_backs_off_on_failure);
|
||||
}
|
||||
160
src/lib/testsuite/test_cluster_parse.c
Normal file
160
src/lib/testsuite/test_cluster_parse.c
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
/* test_cluster_parse.c — unit tests for the parse_peers() helper used by
|
||||
* worker and scheduler main.c (P2.5-5 / P2.5-6).
|
||||
*
|
||||
* The parser is a static function in the respective main.c files. To keep
|
||||
* the test self-contained we duplicate a simplified reference implementation
|
||||
* here and test the same contract. Integration with the real binaries is
|
||||
* covered by the build (both must compile).
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "xce/cluster.h"
|
||||
|
||||
/* ── Reference implementation of parse_peers (mirrors worker/main.c) ─── */
|
||||
|
||||
static int parse_peers_local(const char* peers_str, xce_cluster_config_t* cfg)
|
||||
{
|
||||
cfg->candidates_count = 0;
|
||||
if (!peers_str || peers_str[0] == '\0')
|
||||
return -1;
|
||||
|
||||
char buf[2048];
|
||||
strncpy(buf, peers_str, sizeof(buf) - 1);
|
||||
buf[sizeof(buf) - 1] = '\0';
|
||||
|
||||
char* saveptr;
|
||||
char* tok = strtok_r(buf, ",", &saveptr);
|
||||
|
||||
while (tok && cfg->candidates_count < MAX_HA_CANDIDATES) {
|
||||
/* Format: name=host:port OR host:port */
|
||||
char* eq = strchr(tok, '=');
|
||||
char* addr = eq ? eq + 1 : tok;
|
||||
char* colon = strrchr(addr, ':');
|
||||
if (!colon)
|
||||
return -1;
|
||||
*colon = '\0';
|
||||
strncpy(cfg->candidates[cfg->candidates_count].hostname, addr,
|
||||
XCE_MAXHOSTNAME_LEN - 1);
|
||||
cfg->candidates[cfg->candidates_count].hostname[XCE_MAXHOSTNAME_LEN - 1]
|
||||
= '\0';
|
||||
cfg->candidates[cfg->candidates_count].port = atoi(colon + 1);
|
||||
cfg->candidates_count++;
|
||||
tok = strtok_r(NULL, ",", &saveptr);
|
||||
}
|
||||
return cfg->candidates_count > 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
/* ── Test cases ─────────────────────────────────────────────────────── */
|
||||
|
||||
static void test_parse_peers_single(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = {0};
|
||||
int rc = parse_peers_local("ctrl-1=10.0.1.1:7000", &cfg);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(1, cfg.candidates_count);
|
||||
TEST_ASSERT_EQUAL_STRING("10.0.1.1", cfg.candidates[0].hostname);
|
||||
TEST_ASSERT_EQUAL_INT(7000, cfg.candidates[0].port);
|
||||
printf("test_parse_peers_single: PASS\n");
|
||||
}
|
||||
|
||||
static void test_parse_peers_multiple(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = {0};
|
||||
int rc = parse_peers_local(
|
||||
"ctrl-1=10.0.1.1:7000,ctrl-2=10.0.1.2:7001,ctrl-3=10.0.1.3:7002",
|
||||
&cfg);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(3, cfg.candidates_count);
|
||||
TEST_ASSERT_EQUAL_STRING("10.0.1.2", cfg.candidates[1].hostname);
|
||||
TEST_ASSERT_EQUAL_INT(7001, cfg.candidates[1].port);
|
||||
TEST_ASSERT_EQUAL_STRING("10.0.1.3", cfg.candidates[2].hostname);
|
||||
TEST_ASSERT_EQUAL_INT(7002, cfg.candidates[2].port);
|
||||
printf("test_parse_peers_multiple: PASS\n");
|
||||
}
|
||||
|
||||
static void test_parse_peers_no_name(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = {0};
|
||||
int rc = parse_peers_local("10.0.1.1:7000,10.0.1.2:7001", &cfg);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(2, cfg.candidates_count);
|
||||
TEST_ASSERT_EQUAL_STRING("10.0.1.1", cfg.candidates[0].hostname);
|
||||
TEST_ASSERT_EQUAL_INT(7000, cfg.candidates[0].port);
|
||||
TEST_ASSERT_EQUAL_STRING("10.0.1.2", cfg.candidates[1].hostname);
|
||||
TEST_ASSERT_EQUAL_INT(7001, cfg.candidates[1].port);
|
||||
printf("test_parse_peers_no_name: PASS\n");
|
||||
}
|
||||
|
||||
static void test_parse_peers_empty_returns_error(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = {0};
|
||||
int rc = parse_peers_local("", &cfg);
|
||||
TEST_ASSERT_NOT_EQUAL(0, rc);
|
||||
printf("test_parse_peers_empty_returns_error: PASS\n");
|
||||
}
|
||||
|
||||
static void test_parse_peers_null_returns_error(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = {0};
|
||||
int rc = parse_peers_local(NULL, &cfg);
|
||||
TEST_ASSERT_NOT_EQUAL(0, rc);
|
||||
printf("test_parse_peers_null_returns_error: PASS\n");
|
||||
}
|
||||
|
||||
static void test_parse_peers_missing_port_returns_error(void)
|
||||
{
|
||||
xce_cluster_config_t cfg = {0};
|
||||
int rc = parse_peers_local("ctrl-1=10.0.1.1", &cfg); /* no colon:port */
|
||||
TEST_ASSERT_NOT_EQUAL(0, rc);
|
||||
printf("test_parse_peers_missing_port_returns_error: PASS\n");
|
||||
}
|
||||
|
||||
static void test_parse_peers_first_entry_parsed_correctly(void)
|
||||
{
|
||||
/* Verify the name= prefix is stripped and only addr:port is stored */
|
||||
xce_cluster_config_t cfg = {0};
|
||||
int rc = parse_peers_local("primary=192.168.10.1:9000", &cfg);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_STRING("192.168.10.1", cfg.candidates[0].hostname);
|
||||
TEST_ASSERT_EQUAL_INT(9000, cfg.candidates[0].port);
|
||||
printf("test_parse_peers_first_entry_parsed_correctly: PASS\n");
|
||||
}
|
||||
|
||||
static void test_parse_peers_respects_max_candidates(void)
|
||||
{
|
||||
/* Build a string with MAX_HA_CANDIDATES + 2 entries; only MAX accepted */
|
||||
char big[2048] = "";
|
||||
char entry[128] = "";
|
||||
int n = MAX_HA_CANDIDATES + 2;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
snprintf(entry, sizeof(entry), "10.0.1.%d:%d", i + 1, 7000 + i);
|
||||
if (i > 0)
|
||||
strncat(big, ",", sizeof(big) - strlen(big) - 1);
|
||||
strncat(big, entry, sizeof(big) - strlen(big) - 1);
|
||||
}
|
||||
|
||||
xce_cluster_config_t cfg = {0};
|
||||
int rc = parse_peers_local(big, &cfg);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(MAX_HA_CANDIDATES, cfg.candidates_count);
|
||||
printf("test_parse_peers_respects_max_candidates: PASS\n");
|
||||
}
|
||||
|
||||
/* ── Test group entry point ─────────────────────────────────────────── */
|
||||
|
||||
void test_cluster_parse(void)
|
||||
{
|
||||
RUN_TEST(test_parse_peers_single);
|
||||
RUN_TEST(test_parse_peers_multiple);
|
||||
RUN_TEST(test_parse_peers_no_name);
|
||||
RUN_TEST(test_parse_peers_empty_returns_error);
|
||||
RUN_TEST(test_parse_peers_null_returns_error);
|
||||
RUN_TEST(test_parse_peers_missing_port_returns_error);
|
||||
RUN_TEST(test_parse_peers_first_entry_parsed_correctly);
|
||||
RUN_TEST(test_parse_peers_respects_max_candidates);
|
||||
}
|
||||
|
|
@ -27,6 +27,27 @@ static void test_array(void)
|
|||
TEST_ASSERT_TRUE(MAX_ARRAY_IDX == 32767);
|
||||
}
|
||||
|
||||
static void test_persistence_constants(void)
|
||||
{
|
||||
TEST_ASSERT_TRUE(MAX_STANDBYS == 3);
|
||||
TEST_ASSERT_TRUE(MAX_HA_CANDIDATES == 4);
|
||||
TEST_ASSERT_TRUE(WAL_FORMAT_VERSION == 1);
|
||||
TEST_ASSERT_TRUE(WAL_ENTRY_TYPE_MAX == 100);
|
||||
TEST_ASSERT_TRUE(MAX_WAL_PAYLOAD_BYTES == (1 << 20));
|
||||
TEST_ASSERT_TRUE(MAX_SNAPSHOT_BYTES == (256 * 1024 * 1024));
|
||||
TEST_ASSERT_TRUE(DEFAULT_WAL_SYNC_MS == 10);
|
||||
TEST_ASSERT_TRUE(DEFAULT_SNAP_INTERVAL_S == 60);
|
||||
TEST_ASSERT_TRUE(DEFAULT_SNAP_WAL_SIZE_MB == 64);
|
||||
TEST_ASSERT_TRUE(DEFAULT_SNAP_KEEP_N == 2);
|
||||
TEST_ASSERT_TRUE(DEFAULT_FINISHED_RETAIN_S == 3600);
|
||||
TEST_ASSERT_TRUE(DEFAULT_WORKER_RETAIN_S == 86400);
|
||||
TEST_ASSERT_TRUE(DEFAULT_SYNC_TIMEOUT_MS == 50);
|
||||
TEST_ASSERT_TRUE(DEFAULT_HB_INTERVAL_MS == 3000);
|
||||
TEST_ASSERT_TRUE(DEFAULT_TRANSIENT_JOB_TIMEOUT_S == 300);
|
||||
TEST_ASSERT_TRUE(WAL_HDR_CRC_SIZE == 32);
|
||||
TEST_ASSERT_TRUE(WAL_CRC_NONE == 0xFFFFFFFFu);
|
||||
}
|
||||
|
||||
void test_config(void)
|
||||
{
|
||||
printf("\n>> config test\n\n");
|
||||
|
|
@ -36,4 +57,5 @@ void test_config(void)
|
|||
RUN_TEST(test_timestamp);
|
||||
RUN_TEST(test_tag);
|
||||
RUN_TEST(test_array);
|
||||
RUN_TEST(test_persistence_constants);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ static void test_delay1()
|
|||
int delay_period = 2;
|
||||
time_t t_start = time(NULL);
|
||||
|
||||
delaymgr_init(&mgr, 1, NULL);
|
||||
delaymgr_init(&mgr, 1000 /* precision_ms */, NULL);
|
||||
delaymgr_start(&mgr);
|
||||
delay_task(&mgr, delay_period, runcount, (delay_task_entry_fn_t)delay_work,
|
||||
&count);
|
||||
|
|
@ -48,9 +48,22 @@ static void test_delay1()
|
|||
return;
|
||||
}
|
||||
|
||||
static void test_delaymgr_precision_ms_field(void)
|
||||
{
|
||||
delaymgr_t mgr;
|
||||
memset(&mgr, 0, sizeof(mgr));
|
||||
/* precision_ms field must exist and be stored correctly */
|
||||
int rc = delaymgr_init(&mgr, 200 /* precision_ms */, NULL);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(200, mgr.precision_ms);
|
||||
delaymgr_fini(&mgr);
|
||||
printf("test_delaymgr_precision_ms_field: PASS\n");
|
||||
}
|
||||
|
||||
void test_delay(void)
|
||||
{
|
||||
printf("\n>> delay test\n\n");
|
||||
|
||||
RUN_TEST(test_delay1);
|
||||
RUN_TEST(test_delaymgr_precision_ms_field);
|
||||
}
|
||||
|
|
|
|||
112
src/lib/testsuite/test_hooks.c
Normal file
112
src/lib/testsuite/test_hooks.c
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "hook.h"
|
||||
#include <unity/unity.h>
|
||||
|
||||
/* Shared call counter for hook tests */
|
||||
static int s_call_count = 0;
|
||||
|
||||
static int counting_fn(int type, int sit, int det, void* data, va_list ap)
|
||||
{
|
||||
(void)type;
|
||||
(void)sit;
|
||||
(void)det;
|
||||
(void)ap;
|
||||
(*(int*)data)++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* --- test cases --- */
|
||||
|
||||
static void test_disable_hook_stops_invocation(void)
|
||||
{
|
||||
hooksys_t hs;
|
||||
int rc;
|
||||
|
||||
s_call_count = 0;
|
||||
|
||||
rc = xce_hook_init(&hs, 2 /* maxtypes */);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
rc = xce_add_hook(&hs, HOOK_AFTER_EVENT, counting_fn, &s_call_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* Before disable: hook fires once */
|
||||
xce_run_hooks(&hs, HOOK_AFTER_EVENT, 0, 0);
|
||||
TEST_ASSERT_EQUAL_INT(1, s_call_count);
|
||||
|
||||
/* Disable the hook */
|
||||
rc = xce_disable_hook(&hs, HOOK_AFTER_EVENT, counting_fn);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* After disable: hook must not fire */
|
||||
xce_run_hooks(&hs, HOOK_AFTER_EVENT, 0, 0);
|
||||
TEST_ASSERT_EQUAL_INT(1, s_call_count); /* unchanged */
|
||||
|
||||
xce_hook_fini(&hs);
|
||||
printf("test_disable_hook_stops_invocation: PASS\n");
|
||||
}
|
||||
|
||||
static void test_disable_hook_returns_enoent_if_not_found(void)
|
||||
{
|
||||
hooksys_t hs;
|
||||
int rc;
|
||||
|
||||
rc = xce_hook_init(&hs, 2);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* Disabling a hook never added must return non-zero (ENOENT) */
|
||||
rc = xce_disable_hook(&hs, HOOK_AFTER_EVENT, counting_fn);
|
||||
TEST_ASSERT_NOT_EQUAL(0, rc);
|
||||
|
||||
xce_hook_fini(&hs);
|
||||
printf("test_disable_hook_returns_enoent_if_not_found: PASS\n");
|
||||
}
|
||||
|
||||
static void test_add_hook_reuses_null_slot(void)
|
||||
{
|
||||
hooksys_t hs;
|
||||
int rc;
|
||||
int count_before_reuse;
|
||||
|
||||
s_call_count = 0;
|
||||
|
||||
rc = xce_hook_init(&hs, 2);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
rc = xce_add_hook(&hs, HOOK_AFTER_EVENT, counting_fn, &s_call_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* Record list length after first add */
|
||||
count_before_reuse = hs.hooks[HOOK_AFTER_EVENT].count;
|
||||
TEST_ASSERT_EQUAL_INT(1, count_before_reuse);
|
||||
|
||||
/* Disable the slot — fn becomes NULL atomically */
|
||||
rc = xce_disable_hook(&hs, HOOK_AFTER_EVENT, counting_fn);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
/* Re-add the same fn — must reuse the NULL slot, not grow the list */
|
||||
rc = xce_add_hook(&hs, HOOK_AFTER_EVENT, counting_fn, &s_call_count);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(count_before_reuse, hs.hooks[HOOK_AFTER_EVENT].count);
|
||||
|
||||
/* Fire: must get exactly 1 call (not 2 if slot was duplicated) */
|
||||
xce_run_hooks(&hs, HOOK_AFTER_EVENT, 0, 0);
|
||||
TEST_ASSERT_EQUAL_INT(1, s_call_count);
|
||||
|
||||
xce_hook_fini(&hs);
|
||||
printf("test_add_hook_reuses_null_slot: PASS\n");
|
||||
}
|
||||
|
||||
/* --- suite entry point --- */
|
||||
|
||||
void test_hooks(void)
|
||||
{
|
||||
printf("\n>> hook system tests\n\n");
|
||||
|
||||
RUN_TEST(test_disable_hook_stops_invocation);
|
||||
RUN_TEST(test_disable_hook_returns_enoent_if_not_found);
|
||||
RUN_TEST(test_add_hook_reuses_null_slot);
|
||||
}
|
||||
244
src/lib/testsuite/test_kvmgr_capture.c
Normal file
244
src/lib/testsuite/test_kvmgr_capture.c
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
/*
|
||||
* Test suite for kvmgr_capture_keys() — P0-14a.
|
||||
*
|
||||
* Tests follow TDD: written BEFORE implementation.
|
||||
* All tests exercise the public API declared in kvmgr.h.
|
||||
*/
|
||||
|
||||
#include <rtti.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "kv.h"
|
||||
#include "kvmgr.h"
|
||||
#include "persist.h"
|
||||
#include "utils.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static int find_key_in_captured(slot_key_t* keys, size_t n,
|
||||
const char* target_key, int* index_out)
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; i < n; i++) {
|
||||
if (strcmp(keys[i].key, target_key) == 0) {
|
||||
if (index_out != NULL) {
|
||||
*index_out = keys[i].index;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* test 1: capture from a string-keyed kvmgr with 3 entries */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void test_capture_keys_string_kvmgr(void)
|
||||
{
|
||||
kvmgr_params_t params = { .key_type = KVMGR_KEY_STRING,
|
||||
.nslots = 8,
|
||||
.delta = 4,
|
||||
.datakiller = NULL };
|
||||
var kvmgr = makeInstance(&xkvmgr_type, ¶ms);
|
||||
TEST_ASSERT_NOT_NULL(kvmgr);
|
||||
|
||||
/* Add 3 string-keyed entries whose values are arbitrary pointers;
|
||||
the index returned by key_add is the slot index we capture. */
|
||||
int idx_a = key_add(kvmgr, (void*)"alpha", (void*)"data_alpha");
|
||||
int idx_b = key_add(kvmgr, (void*)"beta", (void*)"data_beta");
|
||||
int idx_c = key_add(kvmgr, (void*)"gamma", (void*)"data_gamma");
|
||||
TEST_ASSERT_TRUE(idx_a >= 0);
|
||||
TEST_ASSERT_TRUE(idx_b >= 0);
|
||||
TEST_ASSERT_TRUE(idx_c >= 0);
|
||||
|
||||
slot_key_t* keys = NULL;
|
||||
size_t n_keys = 0;
|
||||
int rc = kvmgr_capture_keys(kvmgr, &keys, &n_keys);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_size_t(3, n_keys);
|
||||
TEST_ASSERT_NOT_NULL(keys);
|
||||
|
||||
/* Every inserted key must appear exactly once with the correct index */
|
||||
int found_idx = -1;
|
||||
TEST_ASSERT_TRUE(find_key_in_captured(keys, n_keys, "alpha", &found_idx));
|
||||
TEST_ASSERT_EQUAL_INT(idx_a, found_idx);
|
||||
|
||||
TEST_ASSERT_TRUE(find_key_in_captured(keys, n_keys, "beta", &found_idx));
|
||||
TEST_ASSERT_EQUAL_INT(idx_b, found_idx);
|
||||
|
||||
TEST_ASSERT_TRUE(find_key_in_captured(keys, n_keys, "gamma", &found_idx));
|
||||
TEST_ASSERT_EQUAL_INT(idx_c, found_idx);
|
||||
|
||||
/* key_len must equal strlen(key)+1 */
|
||||
size_t i;
|
||||
for (i = 0; i < n_keys; i++) {
|
||||
TEST_ASSERT_EQUAL_size_t(strlen(keys[i].key) + 1, keys[i].key_len);
|
||||
}
|
||||
|
||||
free(keys);
|
||||
destroy(kvmgr);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* test 2: empty kvmgr returns 0 entries and NULL keys_out */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void test_capture_keys_empty_kvmgr(void)
|
||||
{
|
||||
kvmgr_params_t params = { .key_type = KVMGR_KEY_STRING,
|
||||
.nslots = 4,
|
||||
.delta = 4,
|
||||
.datakiller = NULL };
|
||||
var kvmgr = makeInstance(&xkvmgr_type, ¶ms);
|
||||
TEST_ASSERT_NOT_NULL(kvmgr);
|
||||
|
||||
slot_key_t* keys = (slot_key_t*)(uintptr_t)0xdeadbeef; /* sentinel */
|
||||
size_t n_keys = 99;
|
||||
|
||||
int rc = kvmgr_capture_keys(kvmgr, &keys, &n_keys);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_size_t(0, n_keys);
|
||||
TEST_ASSERT_NULL(keys);
|
||||
|
||||
destroy(kvmgr);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* test 3: captured keys are deep copies — safe after kvmgr destroyed */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void test_capture_keys_are_deep_copies(void)
|
||||
{
|
||||
kvmgr_params_t params = { .key_type = KVMGR_KEY_STRING,
|
||||
.nslots = 4,
|
||||
.delta = 4,
|
||||
.datakiller = NULL };
|
||||
var kvmgr = makeInstance(&xkvmgr_type, ¶ms);
|
||||
TEST_ASSERT_NOT_NULL(kvmgr);
|
||||
|
||||
key_add(kvmgr, (void*)"keyA", (void*)"valA");
|
||||
key_add(kvmgr, (void*)"keyB", (void*)"valB");
|
||||
|
||||
slot_key_t* keys = NULL;
|
||||
size_t n_keys = 0;
|
||||
int rc = kvmgr_capture_keys(kvmgr, &keys, &n_keys);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_size_t(2, n_keys);
|
||||
|
||||
/* Stash manual copies of the captured key strings */
|
||||
char copy_a[256], copy_b[256];
|
||||
int found_a = 0, found_b = 0;
|
||||
size_t i;
|
||||
for (i = 0; i < n_keys; i++) {
|
||||
if (strcmp(keys[i].key, "keyA") == 0) {
|
||||
strncpy(copy_a, keys[i].key, sizeof(copy_a) - 1);
|
||||
copy_a[sizeof(copy_a) - 1] = '\0';
|
||||
found_a = 1;
|
||||
} else if (strcmp(keys[i].key, "keyB") == 0) {
|
||||
strncpy(copy_b, keys[i].key, sizeof(copy_b) - 1);
|
||||
copy_b[sizeof(copy_b) - 1] = '\0';
|
||||
found_b = 1;
|
||||
}
|
||||
}
|
||||
TEST_ASSERT_TRUE(found_a);
|
||||
TEST_ASSERT_TRUE(found_b);
|
||||
|
||||
/* Destroy the kvmgr — this frees all kvtab entries including the
|
||||
original key strings. Captured keys[] must remain valid. */
|
||||
destroy(kvmgr);
|
||||
|
||||
TEST_ASSERT_EQUAL_STRING("keyA", copy_a);
|
||||
TEST_ASSERT_EQUAL_STRING("keyB", copy_b);
|
||||
|
||||
/* Directly verify the captured keys array is still readable */
|
||||
int still_a = 0, still_b = 0;
|
||||
for (i = 0; i < n_keys; i++) {
|
||||
if (strcmp(keys[i].key, "keyA") == 0) still_a = 1;
|
||||
if (strcmp(keys[i].key, "keyB") == 0) still_b = 1;
|
||||
}
|
||||
TEST_ASSERT_TRUE(still_a);
|
||||
TEST_ASSERT_TRUE(still_b);
|
||||
|
||||
free(keys);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* test 4: capture from an integer-keyed kvmgr with 3 entries */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void test_capture_keys_int_kvmgr(void)
|
||||
{
|
||||
kvmgr_params_t params = { .key_type = KVMGR_KEY_INT,
|
||||
.nslots = 8,
|
||||
.delta = 4,
|
||||
.datakiller = NULL };
|
||||
var kvmgr = makeInstance(&xkvmgr_type, ¶ms);
|
||||
TEST_ASSERT_NOT_NULL(kvmgr);
|
||||
|
||||
/* Add 3 integer-keyed entries; the integer keys are jobids.
|
||||
For integer-keyed kvmgrs, key_add expects a (void*)(long)intval. */
|
||||
int64_t jobid_1001 = 1001;
|
||||
int64_t jobid_1002 = 1002;
|
||||
int64_t jobid_1003 = 1003;
|
||||
|
||||
int idx_1001 =
|
||||
key_add(kvmgr, (void*)(long)jobid_1001, (void*)"data_1001");
|
||||
int idx_1002 =
|
||||
key_add(kvmgr, (void*)(long)jobid_1002, (void*)"data_1002");
|
||||
int idx_1003 =
|
||||
key_add(kvmgr, (void*)(long)jobid_1003, (void*)"data_1003");
|
||||
TEST_ASSERT_TRUE(idx_1001 >= 0);
|
||||
TEST_ASSERT_TRUE(idx_1002 >= 0);
|
||||
TEST_ASSERT_TRUE(idx_1003 >= 0);
|
||||
|
||||
slot_key_t* keys = NULL;
|
||||
size_t n_keys = 0;
|
||||
int rc = kvmgr_capture_keys(kvmgr, &keys, &n_keys);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_size_t(3, n_keys);
|
||||
TEST_ASSERT_NOT_NULL(keys);
|
||||
|
||||
/* Every inserted integer key must appear as a decimal string with the
|
||||
correct index */
|
||||
int found_idx = -1;
|
||||
TEST_ASSERT_TRUE(find_key_in_captured(keys, n_keys, "1001", &found_idx));
|
||||
TEST_ASSERT_EQUAL_INT(idx_1001, found_idx);
|
||||
|
||||
TEST_ASSERT_TRUE(find_key_in_captured(keys, n_keys, "1002", &found_idx));
|
||||
TEST_ASSERT_EQUAL_INT(idx_1002, found_idx);
|
||||
|
||||
TEST_ASSERT_TRUE(find_key_in_captured(keys, n_keys, "1003", &found_idx));
|
||||
TEST_ASSERT_EQUAL_INT(idx_1003, found_idx);
|
||||
|
||||
/* key_len must equal strlen(key)+1 */
|
||||
size_t i;
|
||||
for (i = 0; i < n_keys; i++) {
|
||||
TEST_ASSERT_EQUAL_size_t(strlen(keys[i].key) + 1, keys[i].key_len);
|
||||
}
|
||||
|
||||
free(keys);
|
||||
destroy(kvmgr);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* entry point called from test.c */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void test_kvmgr_capture(void)
|
||||
{
|
||||
printf("\n>> kvmgr_capture_keys test\n");
|
||||
RUN_TEST(test_capture_keys_string_kvmgr);
|
||||
RUN_TEST(test_capture_keys_empty_kvmgr);
|
||||
RUN_TEST(test_capture_keys_are_deep_copies);
|
||||
RUN_TEST(test_capture_keys_int_kvmgr);
|
||||
}
|
||||
136
src/lib/testsuite/test_zarray_cow.c
Normal file
136
src/lib/testsuite/test_zarray_cow.c
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
#include <assert.h>
|
||||
#include <rtti.h>
|
||||
#include <stdio.h>
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "zarray.h"
|
||||
|
||||
/* xzarray_type is defined in zarray.c */
|
||||
extern typeinfo_t xzarray_type;
|
||||
|
||||
/* Helper: create an xzarray instance */
|
||||
static meta_obj_t* make_test_xzarray(void)
|
||||
{
|
||||
xzarray_params_t params = { .delta = 4, .datakiller = NULL };
|
||||
return (meta_obj_t*)makeInstance(&xzarray_type, ¶ms);
|
||||
}
|
||||
|
||||
static void test_freeze_thaw_slots_exist(void)
|
||||
{
|
||||
/* After P0-2, vfreeze and vthaw must be wired (non-NULL). */
|
||||
const intf_varray_t* iface
|
||||
= (const intf_varray_t*)xzarray_type.interfaces.varray;
|
||||
|
||||
TEST_ASSERT_NOT_NULL(iface);
|
||||
|
||||
/* Access slots without crash — verifies struct layout has them */
|
||||
(void)iface->vfreeze;
|
||||
(void)iface->vthaw;
|
||||
|
||||
/* Slots must be non-NULL after P0-2 wires the real implementations */
|
||||
TEST_ASSERT_NOT_NULL(iface->vfreeze);
|
||||
TEST_ASSERT_NOT_NULL(iface->vthaw);
|
||||
|
||||
printf("test_freeze_thaw_slots_exist: PASS\n");
|
||||
}
|
||||
|
||||
static void test_freeze_creates_shadow(void)
|
||||
{
|
||||
meta_obj_t* arr = make_test_xzarray();
|
||||
TEST_ASSERT_NOT_NULL(arr);
|
||||
|
||||
/* Store 3 items */
|
||||
char* items[3] = { "alpha", "beta", "gamma" };
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int idx = vstore(arr, items[i]);
|
||||
TEST_ASSERT_TRUE(idx >= 0);
|
||||
}
|
||||
|
||||
/* Freeze should succeed */
|
||||
TEST_ASSERT_EQUAL_INT(0, vfreeze(arr));
|
||||
|
||||
/* After freeze, vget must still return stored values (COW mode active) */
|
||||
void* out = NULL;
|
||||
TEST_ASSERT_EQUAL_INT(0, vget(arr, 0, &out));
|
||||
TEST_ASSERT_NOT_NULL(out);
|
||||
|
||||
/* Thaw to clean up */
|
||||
vthaw(arr, NULL, NULL);
|
||||
destroy(arr);
|
||||
}
|
||||
|
||||
static void test_freeze_thaw_preserves_all_entries(void)
|
||||
{
|
||||
meta_obj_t* arr = make_test_xzarray();
|
||||
TEST_ASSERT_NOT_NULL(arr);
|
||||
|
||||
int N = 5;
|
||||
char* items[5] = { "a", "b", "c", "d", "e" };
|
||||
int idxs[5];
|
||||
for (int i = 0; i < N; i++) {
|
||||
idxs[i] = vstore(arr, items[i]);
|
||||
TEST_ASSERT_TRUE(idxs[i] >= 0);
|
||||
}
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, vfreeze(arr));
|
||||
TEST_ASSERT_EQUAL_INT(0, vthaw(arr, NULL, NULL));
|
||||
|
||||
/* All 5 entries must be reachable after thaw */
|
||||
for (int i = 0; i < N; i++) {
|
||||
void* out = NULL;
|
||||
TEST_ASSERT_EQUAL_INT(0, vget(arr, idxs[i], &out));
|
||||
TEST_ASSERT_EQUAL_PTR(items[i], out);
|
||||
}
|
||||
|
||||
destroy(arr);
|
||||
}
|
||||
|
||||
static void test_mutation_during_cow_visible_after_thaw(void)
|
||||
{
|
||||
meta_obj_t* arr = make_test_xzarray();
|
||||
TEST_ASSERT_NOT_NULL(arr);
|
||||
|
||||
char* original = "original";
|
||||
char* updated = "updated";
|
||||
|
||||
int idx = vstore(arr, original);
|
||||
TEST_ASSERT_TRUE(idx >= 0);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, vfreeze(arr));
|
||||
|
||||
/* Overwrite index during COW — write to new active array */
|
||||
int put_idxs[1] = { idx };
|
||||
void* put_data[1] = { updated };
|
||||
vput_vec(arr, 1, put_idxs, put_data);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, vthaw(arr, NULL, NULL));
|
||||
|
||||
/* After thaw, the updated value must win over the shadow value */
|
||||
void* out = NULL;
|
||||
TEST_ASSERT_EQUAL_INT(0, vget(arr, idx, &out));
|
||||
TEST_ASSERT_EQUAL_PTR(updated, out);
|
||||
|
||||
destroy(arr);
|
||||
}
|
||||
|
||||
static void test_double_freeze_fails(void)
|
||||
{
|
||||
meta_obj_t* arr = make_test_xzarray();
|
||||
TEST_ASSERT_NOT_NULL(arr);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, vfreeze(arr));
|
||||
/* Second freeze must fail (already in COW mode) */
|
||||
TEST_ASSERT_TRUE(vfreeze(arr) != 0);
|
||||
vthaw(arr, NULL, NULL);
|
||||
destroy(arr);
|
||||
}
|
||||
|
||||
void test_zarray_cow(void)
|
||||
{
|
||||
printf("\n>> zarray COW interface slot test\n\n");
|
||||
RUN_TEST(test_freeze_thaw_slots_exist);
|
||||
RUN_TEST(test_freeze_creates_shadow);
|
||||
RUN_TEST(test_freeze_thaw_preserves_all_entries);
|
||||
RUN_TEST(test_mutation_during_cow_visible_after_thaw);
|
||||
RUN_TEST(test_double_freeze_fails);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue