145 lines
5.8 KiB
C
145 lines
5.8 KiB
C
// testHooks.c -- validates the library lifecycle hooks: calogAtDestroy (a runtime-shutdown hook run
|
|
// in one of two phases around context teardown) and calogAtContext (per-context init/shutdown run on
|
|
// each context's own thread). A monotonic sequence counter records the order of every hook event so
|
|
// the phase ordering can be asserted: before-hook < context shutdowns < after-hook.
|
|
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "calog.h"
|
|
|
|
#include <stdatomic.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <time.h>
|
|
|
|
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
|
|
|
|
#define PUMP_LIMIT 4000
|
|
|
|
static _Atomic int32_t seqCounter = 0; // hands out a global order to every hook event
|
|
static _Atomic int32_t destroyBeforeSeq = -1;
|
|
static _Atomic int32_t destroyAfterSeq = -1;
|
|
static _Atomic int32_t initCount = 0;
|
|
static _Atomic int32_t shutdownCount = 0;
|
|
static _Atomic int32_t firstShutdownSeq = -1;
|
|
static _Atomic int32_t lastShutdownSeq = -1;
|
|
static _Atomic int64_t seenUserData = 0;
|
|
static _Atomic bool lateOpenSucceeded = false; // a context opened DURING teardown (must not happen)
|
|
static _Atomic bool lateEvalAccepted = false; // work queued onto a context that has stopped serving
|
|
static int32_t testsRun = 0;
|
|
static int32_t testsFailed = 0;
|
|
static int32_t userToken = 0x5A5A;
|
|
static CalogT *calog = NULL; // file scope: the shutdown hook reaches it too
|
|
|
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
|
|
static void contextInitHook(CalogContextT *context, void *userData);
|
|
static void contextShutdownHook(CalogContextT *context, void *userData);
|
|
static void destroyAfterHook(void);
|
|
static void destroyBeforeHook(void);
|
|
|
|
|
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
|
|
testsRun++;
|
|
if (!condition) {
|
|
testsFailed++;
|
|
printf("FAIL %s:%d %s\n", file, line, message);
|
|
}
|
|
}
|
|
|
|
|
|
static void contextInitHook(CalogContextT *context, void *userData) {
|
|
(void)context;
|
|
atomic_fetch_add(&seqCounter, 1);
|
|
atomic_fetch_add(&initCount, 1);
|
|
atomic_store(&seenUserData, (int64_t)*(int32_t *)userData);
|
|
}
|
|
|
|
|
|
static void contextShutdownHook(CalogContextT *context, void *userData) {
|
|
CalogContextT *late;
|
|
int32_t seq;
|
|
int32_t expected;
|
|
|
|
(void)userData;
|
|
seq = atomic_fetch_add(&seqCounter, 1);
|
|
atomic_fetch_add(&shutdownCount, 1);
|
|
expected = -1;
|
|
atomic_compare_exchange_strong(&firstShutdownSeq, &expected, seq);
|
|
atomic_store(&lastShutdownSeq, seq);
|
|
// This hook runs on the context's own thread while calogDestroy is walking the registry -- the
|
|
// one window where a script (taskSpawn) could still ask for a context the teardown has already
|
|
// walked past, then be freed with its thread running. Opening one has to be refused here.
|
|
(void)context;
|
|
late = calogContextOpen(calog, &calogLuaEngine);
|
|
if (late != NULL) {
|
|
atomic_store(&lateOpenSucceeded, true);
|
|
calogContextClose(late);
|
|
}
|
|
// Same window, the other half: this hook runs AFTER serveLoop stopped serving and BEFORE the
|
|
// interpreter is marked dead, so the context still resolves in the registry. Queueing work here
|
|
// used to be accepted and then never served -- a fire-and-forget eval silently dropped, and a
|
|
// blocking caller left waiting on a reply that could not come. It must be refused.
|
|
if (calogContextEval(context, "local dropped = 1") == calogOkE) {
|
|
atomic_store(&lateEvalAccepted, true);
|
|
}
|
|
}
|
|
|
|
|
|
static void destroyAfterHook(void) {
|
|
atomic_store(&destroyAfterSeq, atomic_fetch_add(&seqCounter, 1));
|
|
}
|
|
|
|
|
|
static void destroyBeforeHook(void) {
|
|
atomic_store(&destroyBeforeSeq, atomic_fetch_add(&seqCounter, 1));
|
|
}
|
|
|
|
|
|
int main(void) {
|
|
CalogContextT *ctxA;
|
|
CalogContextT *ctxB;
|
|
struct timespec ts = { 0, 500000 };
|
|
int32_t i;
|
|
|
|
calog = calogCreate();
|
|
if (calog == NULL) {
|
|
printf("calog create failed\n");
|
|
return 1;
|
|
}
|
|
calogAtDestroy(calog, destroyBeforeHook, calogDestroyBeforeContextsE);
|
|
calogAtDestroy(calog, destroyAfterHook, calogDestroyAfterContextsE);
|
|
calogAtContext(calog, contextInitHook, contextShutdownHook, &userToken);
|
|
|
|
ctxA = calogContextOpen(calog, &calogLuaEngine);
|
|
ctxB = calogContextOpen(calog, &calogLuaEngine);
|
|
calogContextEval(ctxA, "local a = 1");
|
|
calogContextEval(ctxB, "local b = 2");
|
|
for (i = 0; i < PUMP_LIMIT; i++) {
|
|
calogPump(calog);
|
|
if (atomic_load(&initCount) >= 2) {
|
|
break;
|
|
}
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
|
|
CHECK(atomic_load(&initCount) == 2, "per-context init hook fired once per context");
|
|
CHECK(atomic_load(&seenUserData) == 0x5A5A, "context hook received its registered userData");
|
|
|
|
calogDestroy(calog); // before-hook -> tear down contexts (their shutdown hooks) -> after-hook
|
|
|
|
CHECK(atomic_load(&shutdownCount) == 2, "per-context shutdown hook fired once per context");
|
|
CHECK(!atomic_load(&lateOpenSucceeded), "opening a context during teardown is refused, not registered too late");
|
|
CHECK(!atomic_load(&lateEvalAccepted), "queueing work onto a context that has stopped serving is refused, not orphaned");
|
|
CHECK(atomic_load(&destroyBeforeSeq) >= 0, "before-context destroy hook fired");
|
|
CHECK(atomic_load(&destroyAfterSeq) >= 0, "after-context destroy hook fired");
|
|
CHECK(atomic_load(&destroyBeforeSeq) < atomic_load(&firstShutdownSeq), "before-hook ran before context teardown");
|
|
CHECK(atomic_load(&lastShutdownSeq) < atomic_load(&destroyAfterSeq), "after-hook ran after context teardown");
|
|
|
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
|
fflush(stdout);
|
|
if (testsFailed != 0) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|