// testTimer.c -- exercises the timer library from Lua: a one-shot fires once after a delay, a // cancelled one-shot never fires, and a repeating timer fires several times before cancelling // itself. Each callback runs on its owning context and reports back through host natives, which // the main thread services by pumping. #define _POSIX_C_SOURCE 200809L #include "calog.h" #include "calogTimer.h" #include #include #include #define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__) #define PUMP_LIMIT 4000 #define RESULT_SLOTS 12 static CalogT *calog = NULL; static _Atomic int64_t results[RESULT_SLOTS]; static _Atomic int32_t doneCount = 0; static _Atomic int32_t errorCount = 0; static int32_t testsRun = 0; static int32_t testsFailed = 0; static int64_t asInt(const CalogValueT *value); static void checkImpl(bool condition, const char *message, const char *file, int32_t line); static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static void onError(uint64_t contextId, const char *message, void *userData); static void pumpSettle(void); static void pumpUntilDone(int32_t target); static int64_t asInt(const CalogValueT *value) { if (value->type == calogIntE) { return value->as.i; } return (int64_t)value->as.r; } 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 int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)args; (void)argCount; (void)userData; atomic_fetch_add(&doneCount, 1); calogValueNil(result); return calogOkE; } static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { int64_t tag; (void)userData; calogValueNil(result); if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE) || (args[1].type != calogIntE && args[1].type != calogRealE)) { return calogFail(result, calogErrArgE, "report expects (tag, value)"); } tag = asInt(&args[0]); if (tag < 0 || tag >= RESULT_SLOTS) { return calogFail(result, calogErrArgE, "report: tag out of range"); } atomic_store(&results[tag], asInt(&args[1])); return calogOkE; } static void onError(uint64_t contextId, const char *message, void *userData) { (void)contextId; (void)userData; fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)"); atomic_fetch_add(&errorCount, 1); } static void pumpSettle(void) { struct timespec ts = { 0, 500000 }; int i; // Keep pumping for ~60ms so a broken cancel or a stray extra fire has time to show up. for (i = 0; i < 120; i++) { calogPump(calog); nanosleep(&ts, NULL); } } static void pumpUntilDone(int32_t target) { struct timespec ts = { 0, 500000 }; int i; for (i = 0; i < PUMP_LIMIT; i++) { calogPump(calog); if (atomic_load(&doneCount) >= target) { calogPump(calog); return; } nanosleep(&ts, NULL); } } int main(void) { CalogContextT *ctxAfter; CalogContextT *ctxCancel; CalogContextT *ctxEvery; int32_t i; calog = calogCreate(); if (calog == NULL) { printf("calog create failed\n"); return 1; } calogSetErrorHandler(calog, onError, NULL); calogRegister(calog, "report", nativeReport, NULL); calogRegister(calog, "done", nativeDone, NULL); if (calogTimerRegister(calog) != calogOkE) { printf("calogTimerRegister failed\n"); return 1; } for (i = 0; i < RESULT_SLOTS; i++) { atomic_store(&results[i], -1); } // A one-shot fires its callback once, ~10ms from now, on its owning context. ctxAfter = calogContextOpen(calog, &calogLuaEngine); calogContextEval(ctxAfter, "timerAfter(10, function() report(1, 42); done() end)"); // A one-shot cancelled immediately must NEVER fire (slot 3 stays -1). Slot 4 confirms the // script itself ran to completion. ctxCancel = calogContextOpen(calog, &calogLuaEngine); calogContextEval(ctxCancel, "local h = timerAfter(20, function() report(3, 999) end)\n" "timerCancel(h)\n" "report(4, 1)\n" "local okr = pcall(function() timerAfter(1/0, function() end) end)\n" "report(5, okr and 0 or 1)\n" // 1, a non-finite delay is rejected, not UB "done()"); // A repeating timer fires every ~5ms; after the third fire it cancels itself and reports the // final count. Slot 2 must settle at exactly 3. ctxEvery = calogContextOpen(calog, &calogLuaEngine); calogContextEval(ctxEvery, "local count = 0\n" "local id\n" "id = timerEvery(5, function()\n" " count = count + 1\n" " report(2, count)\n" " if count >= 3 then timerCancel(id); done() end\n" "end)"); pumpUntilDone(3); pumpSettle(); CHECK(atomic_load(&results[1]) == 42, "timerAfter fires its callback once after the delay"); CHECK(atomic_load(&results[4]) == 1, "a script that cancels a pending one-shot still runs to completion"); CHECK(atomic_load(&results[3]) == -1, "timerCancel of a one-shot before it fires prevents the callback"); CHECK(atomic_load(&results[2]) == 3, "timerEvery fires repeatedly and timerCancel stops it after 3 fires"); CHECK(atomic_load(&results[5]) == 1, "a non-finite / out-of-range delay is rejected"); CHECK(atomic_load(&doneCount) >= 3, "all three timer scripts completed"); CHECK(atomic_load(&errorCount) == 0, "no uncaught errors"); // Release pending callbacks + stop the thread while the contexts are still alive. calogTimerShutdown(); calogContextClose(ctxAfter); calogContextClose(ctxCancel); calogContextClose(ctxEvery); calogDestroy(calog); printf("\n%d checks, %d failed\n", testsRun, testsFailed); fflush(stdout); if (testsFailed != 0) { return 1; } return 0; }