nim-ffi/tests/e2e/c/test_timer_e2e.c
2026-07-09 15:06:42 -03:00

400 lines
12 KiB
C

/* End-to-end test for the generated C timer bindings. Exercises the same
* surface as the C++ suite — constructor, methods, nested seq/Option payloads,
* multi-parameter requests, the error channel and the typed event listener —
* and aborts (non-zero exit) on the first failure so ctest reports it.
*
* Both call surfaces are covered. The async API takes a result callback whose
* reply/error are owned by the binding and valid only for that callback, so the
* checks below *copy out* what they need into a waiter struct and poll a `done`
* flag to sequence each call. The blocking `_sync` API (further down) collapses
* that dance into one call whose out-param is caller-owned and freed with the
* generated helper. */
#include "my_timer.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
static void wait_done(volatile int* done) {
for (int i = 0; i < 500 && !*done; i++) {
struct timespec t = {0, 10 * 1000 * 1000}; /* 10ms */
nanosleep(&t, NULL);
}
assert(*done);
}
static int g_event_count = 0;
static char g_event_message[256];
static void on_echo_fired(const EchoEvent* evt, void* user_data) {
int* hits = (int*)user_data;
if (hits) {
(*hits)++;
}
g_event_count = (int)evt->echoCount;
snprintf(g_event_message, sizeof(g_event_message), "%s",
evt->message.data ? evt->message.data : "");
}
typedef struct {
volatile int done;
int err_code;
MyTimerCtx* ctx;
char err[256];
} CreateWaiter;
static void on_created(int ec, MyTimerCtx* ctx, const char* em, void* ud) {
CreateWaiter* w = (CreateWaiter*)ud;
w->err_code = ec;
w->ctx = ctx;
if (em) {
snprintf(w->err, sizeof(w->err), "%s", em);
}
w->done = 1;
}
static MyTimerCtx* make_ctx(void) {
CreateWaiter w;
memset(&w, 0, sizeof(w));
TimerConfig config = {nimffi_str("c-e2e")};
my_timer_ctx_create(&config, on_created, &w);
wait_done(&w.done);
if (w.err_code != 0) {
fprintf(stderr, "create failed: %s\n", w.err[0] ? w.err : "?");
}
assert(w.err_code == 0);
assert(w.ctx != NULL);
return w.ctx;
}
typedef struct {
volatile int done;
int err_code;
char err[256];
char text_a[256];
char text_b[256];
long long num_a;
int flag;
} ReplyWaiter;
static void on_version(int ec, const NimFfiStr* reply, const char* em, void* ud) {
ReplyWaiter* w = (ReplyWaiter*)ud;
w->err_code = ec;
if (reply && reply->data) snprintf(w->text_a, sizeof(w->text_a), "%s", reply->data);
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
w->done = 1;
}
static void test_version(MyTimerCtx* ctx) {
ReplyWaiter w;
memset(&w, 0, sizeof(w));
my_timer_ctx_version(ctx, on_version, &w);
wait_done(&w.done);
assert(w.err_code == 0);
assert(strcmp(w.text_a, "nim-timer v0.1.0") == 0);
}
static void on_echo(int ec, const EchoResponse* reply, const char* em, void* ud) {
ReplyWaiter* w = (ReplyWaiter*)ud;
w->err_code = ec;
if (reply) {
if (reply->echoed.data) snprintf(w->text_a, sizeof(w->text_a), "%s", reply->echoed.data);
if (reply->timerName.data)
snprintf(w->text_b, sizeof(w->text_b), "%s", reply->timerName.data);
}
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
w->done = 1;
}
static void test_echo(MyTimerCtx* ctx) {
ReplyWaiter w;
memset(&w, 0, sizeof(w));
EchoRequest req = {nimffi_str("hello"), 10};
my_timer_ctx_echo(ctx, &req, on_echo, &w);
wait_done(&w.done);
assert(w.err_code == 0);
assert(strcmp(w.text_a, "hello") == 0);
assert(strcmp(w.text_b, "c-e2e") == 0);
}
static void on_complex(int ec, const ComplexResponse* reply, const char* em, void* ud) {
ReplyWaiter* w = (ReplyWaiter*)ud;
w->err_code = ec;
if (reply) {
w->num_a = (long long)reply->itemCount;
w->flag = (int)reply->hasNote;
if (reply->summary.data)
snprintf(w->text_a, sizeof(w->text_a), "%s", reply->summary.data);
}
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
w->done = 1;
}
static void test_complex(MyTimerCtx* ctx) {
EchoRequest items[2] = {{nimffi_str("one"), 1}, {nimffi_str("two"), 2}};
NimFfiStr tags[2] = {nimffi_str("a"), nimffi_str("b")};
ComplexRequest req;
req.messages.data = items;
req.messages.len = 2;
req.tags.data = tags;
req.tags.len = 2;
req.note.has_value = true;
req.note.value = nimffi_str("note");
req.retries.has_value = false;
req.retries.value = 0;
ReplyWaiter w;
memset(&w, 0, sizeof(w));
my_timer_ctx_complex(ctx, &req, on_complex, &w);
wait_done(&w.done);
assert(w.err_code == 0);
assert(w.num_a == 2);
assert(w.flag == true);
assert(strstr(w.text_a, "note=note") != NULL);
}
static void on_schedule(int ec, const ScheduleResult* reply, const char* em, void* ud) {
ReplyWaiter* w = (ReplyWaiter*)ud;
w->err_code = ec;
if (reply) {
w->num_a = (long long)reply->willRunCount;
if (reply->jobId.data) snprintf(w->text_a, sizeof(w->text_a), "%s", reply->jobId.data);
}
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
w->done = 1;
}
static void test_schedule_ok(MyTimerCtx* ctx) {
NimFfiStr payload[1] = {nimffi_str("p")};
JobSpec job;
job.name = nimffi_str("rollup");
job.payload.data = payload;
job.payload.len = 1;
job.priority = 1;
NimFfiStr retry_on[1] = {nimffi_str("timeout")};
RetryPolicy retry;
retry.maxAttempts = 3;
retry.backoffMs = 100;
retry.retryOn.data = retry_on;
retry.retryOn.len = 1;
ScheduleConfig sched;
sched.startAtMs = 1000;
sched.intervalMs = 0;
sched.jitter.has_value = false;
sched.jitter.value = 0;
ReplyWaiter w;
memset(&w, 0, sizeof(w));
my_timer_ctx_schedule(ctx, &job, &retry, &sched, on_schedule, &w);
wait_done(&w.done);
assert(w.err_code == 0);
assert(strcmp(w.text_a, "c-e2e:rollup") == 0);
assert(w.num_a == 1);
}
static void test_schedule_error(MyTimerCtx* ctx) {
NimFfiStr payload[1] = {nimffi_str("p")};
JobSpec job;
job.name = nimffi_str(""); /* empty name → handler returns err */
job.payload.data = payload;
job.payload.len = 1;
job.priority = 1;
NimFfiStr retry_on[1] = {nimffi_str("timeout")};
RetryPolicy retry;
retry.maxAttempts = 3;
retry.backoffMs = 100;
retry.retryOn.data = retry_on;
retry.retryOn.len = 1;
ScheduleConfig sched;
sched.startAtMs = 0;
sched.intervalMs = 0;
sched.jitter.has_value = false;
sched.jitter.value = 0;
ReplyWaiter w;
memset(&w, 0, sizeof(w));
my_timer_ctx_schedule(ctx, &job, &retry, &sched, on_schedule, &w);
wait_done(&w.done);
assert(w.err_code != 0);
assert(w.err[0] != '\0');
assert(strstr(w.err, "job name") != NULL);
}
static void test_event(MyTimerCtx* ctx) {
int hits = 0;
uint64_t handle =
my_timer_ctx_add_on_echo_fired_listener(ctx, on_echo_fired, &hits);
assert(handle != 0);
ReplyWaiter w;
memset(&w, 0, sizeof(w));
EchoRequest req = {nimffi_str("evt"), 1};
my_timer_ctx_echo(ctx, &req, on_echo, &w);
wait_done(&w.done);
assert(w.err_code == 0);
/* The event fires from the dispatch thread; poll briefly for delivery. */
for (int i = 0; i < 100 && hits == 0; i++) {
struct timespec t = {0, 10 * 1000 * 1000};
nanosleep(&t, NULL);
}
assert(hits >= 1);
assert(strcmp(g_event_message, "evt") == 0);
assert(g_event_count == 1);
assert(my_timer_ctx_remove_event_listener(ctx, handle) == true);
}
/* ── Blocking `_sync` path ──────────────────────────────────────────────────
* The generated `_sync` wrappers collapse the submit + wait + copy-out dance
* above into a single blocking call: on success `*out` is caller-owned and
* released with the generated free helper; on failure the error text lands in
* `err`. These mirror the async checks to prove both surfaces round-trip. */
#define SYNC_TIMEOUT_MS 5000
static MyTimerCtx* make_ctx_sync(void) {
MyTimerCtx* ctx = NULL;
char err[256] = {0};
TimerConfig config = {nimffi_str("c-e2e-sync")};
int rc = my_timer_ctx_create_sync(&config, &ctx, err, sizeof(err), SYNC_TIMEOUT_MS);
if (rc != 0) {
fprintf(stderr, "create_sync failed: %s\n", err[0] ? err : "?");
}
assert(rc == 0);
assert(ctx != NULL);
return ctx;
}
static void test_version_sync(MyTimerCtx* ctx) {
NimFfiStr version = {0};
char err[256] = {0};
int rc = my_timer_ctx_version_sync(ctx, &version, err, sizeof(err), SYNC_TIMEOUT_MS);
assert(rc == 0);
assert(version.data != NULL);
assert(strcmp(version.data, "nim-timer v0.1.0") == 0);
nimffi_free_str(&version);
}
static void test_echo_sync(MyTimerCtx* ctx) {
EchoRequest req = {nimffi_str("hello-sync"), 10};
EchoResponse resp = {0};
char err[256] = {0};
int rc = my_timer_ctx_echo_sync(ctx, &req, &resp, err, sizeof(err), SYNC_TIMEOUT_MS);
assert(rc == 0);
assert(resp.echoed.data && strcmp(resp.echoed.data, "hello-sync") == 0);
assert(resp.timerName.data && strcmp(resp.timerName.data, "c-e2e-sync") == 0);
my_timer_free_EchoResponse(&resp);
}
static void test_complex_sync(MyTimerCtx* ctx) {
EchoRequest items[2] = {{nimffi_str("one"), 1}, {nimffi_str("two"), 2}};
NimFfiStr tags[2] = {nimffi_str("a"), nimffi_str("b")};
ComplexRequest req;
req.messages.data = items;
req.messages.len = 2;
req.tags.data = tags;
req.tags.len = 2;
req.note.has_value = true;
req.note.value = nimffi_str("note");
req.retries.has_value = false;
req.retries.value = 0;
ComplexResponse resp = {0};
char err[256] = {0};
int rc =
my_timer_ctx_complex_sync(ctx, &req, &resp, err, sizeof(err), SYNC_TIMEOUT_MS);
assert(rc == 0);
assert(resp.itemCount == 2);
assert(resp.hasNote == true);
assert(resp.summary.data && strstr(resp.summary.data, "note=note") != NULL);
my_timer_free_ComplexResponse(&resp);
}
static void test_schedule_ok_sync(MyTimerCtx* ctx) {
NimFfiStr payload[1] = {nimffi_str("p")};
JobSpec job;
job.name = nimffi_str("rollup");
job.payload.data = payload;
job.payload.len = 1;
job.priority = 1;
NimFfiStr retry_on[1] = {nimffi_str("timeout")};
RetryPolicy retry;
retry.maxAttempts = 3;
retry.backoffMs = 100;
retry.retryOn.data = retry_on;
retry.retryOn.len = 1;
ScheduleConfig sched;
sched.startAtMs = 1000;
sched.intervalMs = 0;
sched.jitter.has_value = false;
sched.jitter.value = 0;
ScheduleResult resp = {0};
char err[256] = {0};
int rc = my_timer_ctx_schedule_sync(ctx, &job, &retry, &sched, &resp, err,
sizeof(err), SYNC_TIMEOUT_MS);
assert(rc == 0);
assert(resp.jobId.data && strcmp(resp.jobId.data, "c-e2e-sync:rollup") == 0);
assert(resp.willRunCount == 1);
my_timer_free_ScheduleResult(&resp);
}
static void test_schedule_error_sync(MyTimerCtx* ctx) {
NimFfiStr payload[1] = {nimffi_str("p")};
JobSpec job;
job.name = nimffi_str(""); /* empty name → handler returns err */
job.payload.data = payload;
job.payload.len = 1;
job.priority = 1;
NimFfiStr retry_on[1] = {nimffi_str("timeout")};
RetryPolicy retry;
retry.maxAttempts = 3;
retry.backoffMs = 100;
retry.retryOn.data = retry_on;
retry.retryOn.len = 1;
ScheduleConfig sched;
sched.startAtMs = 0;
sched.intervalMs = 0;
sched.jitter.has_value = false;
sched.jitter.value = 0;
ScheduleResult resp = {0};
char err[256] = {0};
int rc = my_timer_ctx_schedule_sync(ctx, &job, &retry, &sched, &resp, err,
sizeof(err), SYNC_TIMEOUT_MS);
assert(rc != 0);
assert(err[0] != '\0');
assert(strstr(err, "job name") != NULL);
}
int main(void) {
MyTimerCtx* ctx = make_ctx();
test_version(ctx);
test_echo(ctx);
test_complex(ctx);
test_schedule_ok(ctx);
test_schedule_error(ctx);
test_event(ctx);
my_timer_ctx_destroy(ctx);
MyTimerCtx* sctx = make_ctx_sync();
test_version_sync(sctx);
test_echo_sync(sctx);
test_complex_sync(sctx);
test_schedule_ok_sync(sctx);
test_schedule_error_sync(sctx);
my_timer_ctx_destroy(sctx);
printf("all C e2e checks passed\n");
return 0;
}