// 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 #include #include #include #include #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; }