// testTask.c -- exercises the calog task library: a parent Lua script spawns child script // contexts, feeds them code, queries the task count and its own id, loads a script file, and // closes tasks. Children report values back through a host native so the parent's management // of them can be observed. #define _POSIX_C_SOURCE 200809L #include "calog.h" #include "calogTask.h" #include #include #include #include #include #define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__) #define PUMP_LIMIT 4000 #define TAG_COUNT 16 static CalogT *calog = NULL; static _Atomic int64_t results[TAG_COUNT]; static _Atomic bool scriptDone = false; static _Atomic int32_t errorCount = 0; static int32_t testsRun = 0; static int32_t testsFailed = 0; 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 pumpUntilDone(void); static void pumpUntilScriptDone(void); static void resetTags(void); static void testTaskCrossEngine(void); static void testTaskError(void); static void testTaskLoad(void); static void testTaskMutualClose(void); static void testTaskSelfClose(void); static void testTaskSpawn(void); static void writeFile(const char *path, const char *text); 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_store(&scriptDone, true); calogValueNil(result); return calogOkE; } // Accept int or real so the same observer works from every engine (e.g. JS numbers marshal // as reals). static int64_t asInt(const CalogValueT *value) { if (value->type == calogIntE) { return value->as.i; } return (int64_t)value->as.r; } 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 >= TAG_COUNT) { 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 pumpUntilDone(void) { struct timespec ts = { 0, 500000 }; int errorsBefore; int i; errorsBefore = atomic_load(&errorCount); for (i = 0; i < PUMP_LIMIT; i++) { calogPump(calog); if (atomic_load(&scriptDone) || atomic_load(&errorCount) != errorsBefore) { calogPump(calog); return; } nanosleep(&ts, NULL); } } // Waits for the parent script's done() regardless of child errors (used by tests that // deliberately trigger a refused operation in a child but expect the parent to finish). static void pumpUntilScriptDone(void) { struct timespec ts = { 0, 500000 }; int i; for (i = 0; i < PUMP_LIMIT; i++) { calogPump(calog); if (atomic_load(&scriptDone)) { calogPump(calog); calogPump(calog); return; } nanosleep(&ts, NULL); } } static void resetTags(void) { int32_t i; atomic_store(&scriptDone, false); for (i = 0; i < TAG_COUNT; i++) { atomic_store(&results[i], -1); } } static void testTaskCrossEngine(void) { CalogContextT *ctx; ctx = calogContextOpen(calog, &calogLuaEngine); resetTags(); // A Lua script launches a JavaScript task -- the whole point of naming the engine. calogContextEval(ctx, "local j = taskSpawn('js', 'report(8, 55)')\n" "taskClose(j)\n" "done()"); pumpUntilDone(); CHECK(atomic_load(&results[8]) == 55, "a Lua script spawned and ran a JavaScript task"); calogContextClose(ctx); } static void testTaskError(void) { CalogContextT *ctx; int32_t before; ctx = calogContextOpen(calog, &calogLuaEngine); before = atomic_load(&errorCount); resetTags(); // Spawning an unknown engine must raise a clean script-level error. calogContextEval(ctx, "taskSpawn('nosuchengine', 'return 1')\n done()"); pumpUntilDone(); CHECK(atomic_load(&errorCount) == before + 1, "taskSpawn of an unknown engine raised a script error"); calogContextClose(ctx); } static void testTaskLoad(void) { CalogContextT *ctx; writeFile("clTask.lua", "report(6, 77)\n"); ctx = calogContextOpen(calog, &calogLuaEngine); resetTags(); // taskLoad launches the script FILE by base name; it reports 77, then we close it. calogContextEval(ctx, "local f = taskLoad('clTask')\n" "taskClose(f)\n" "done()"); pumpUntilDone(); CHECK(atomic_load(&results[6]) == 77, "taskLoad launched a script file that ran"); calogContextClose(ctx); remove("clTask.lua"); } static void testTaskMutualClose(void) { CalogContextT *ctx; ctx = calogContextOpen(calog, &calogLuaEngine); resetTags(); // Two children each told to close the OTHER's handle. Both are refused (the parent owns // both), so there is no mutual pthread_join deadlock -- reaching the end proves it. calogContextEval(ctx, "local a = taskSpawn('lua', '')\n" "local b = taskSpawn('lua', '')\n" "taskEval(a, 'taskClose(' .. b .. ')')\n" "taskEval(b, 'taskClose(' .. a .. ')')\n" "taskClose(a)\n" "taskClose(b)\n" "report(1, 1)\n" "done()"); pumpUntilScriptDone(); CHECK(atomic_load(&results[1]) == 1, "mutual cross-close is refused, not a deadlock"); calogContextClose(ctx); } static void testTaskSelfClose(void) { CalogContextT *ctx; int32_t before; ctx = calogContextOpen(calog, &calogLuaEngine); before = atomic_load(&errorCount); resetTags(); // A child is told to close its OWN handle. That must be refused (only the owner closes), // NOT a self-join that frees the running context -- reaching the end proves no crash. calogContextEval(ctx, "local t = taskSpawn('lua', '')\n" "taskEval(t, 'taskClose(' .. t .. ')')\n" "taskClose(t)\n" "report(1, 1)\n" "done()"); pumpUntilScriptDone(); CHECK(atomic_load(&results[1]) == 1, "a task closing its own handle is refused, not a self-join crash"); CHECK(atomic_load(&errorCount) > before, "the self-close attempt raised a clean error"); calogContextClose(ctx); } static void testTaskSpawn(void) { CalogContextT *ctx; ctx = calogContextOpen(calog, &calogLuaEngine); resetTags(); // Spawn a child (reports 42), read the live task count + own id, feed the child more code // (reports 99), close it, then read the count again. calogContextEval(ctx, "local t = taskSpawn('lua', 'report(1, 42)')\n" "report(2, taskCount())\n" "report(3, taskSelf())\n" "taskEval(t, 'report(4, 99)')\n" "taskClose(t)\n" "report(5, taskCount())\n" "done()"); pumpUntilDone(); CHECK(atomic_load(&results[1]) == 42, "a spawned child task ran its code"); CHECK(atomic_load(&results[2]) == 1, "taskCount saw the one open task"); CHECK(atomic_load(&results[3]) > 0, "taskSelf returned the caller's own non-zero context id"); CHECK(atomic_load(&results[4]) == 99, "taskEval fed more code into the running task"); CHECK(atomic_load(&results[5]) == 0, "taskCount saw the task gone after taskClose"); calogContextClose(ctx); } static void writeFile(const char *path, const char *text) { FILE *file; file = fopen(path, "wb"); if (file == NULL) { return; } fwrite(text, 1, strlen(text), file); fclose(file); } int main(void) { calog = calogCreate(); if (calog == NULL) { printf("calog create failed\n"); return 1; } calogSetErrorHandler(calog, onError, NULL); calogRegisterEngine(calog, &calogLuaEngine); // so taskLoad can find *.lua // Inline observers: they run on the reporting task's own thread and complete within its // eval, so they are not aborted when a task is closed mid-report (unlike a host native, // whose cross-thread call taskClose's cooperative shutdown can interrupt). calogRegisterInline(calog, "report", nativeReport, NULL); calogRegisterInline(calog, "done", nativeDone, NULL); if (calogTaskRegister(calog) != calogOkE) { printf("calogTaskRegister failed\n"); return 1; } testTaskSpawn(); testTaskLoad(); testTaskCrossEngine(); testTaskSelfClose(); testTaskMutualClose(); testTaskError(); calogDestroy(calog); calogTaskShutdown(); printf("\n%d checks, %d failed\n", testsRun, testsFailed); fflush(stdout); if (testsFailed != 0) { return 1; } return 0; }