369 lines
12 KiB
C
369 lines
12 KiB
C
// 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 <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#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 testTaskSpawnRacesTeardown(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);
|
|
}
|
|
|
|
|
|
// A script spawning tasks while the host tears the runtime down: calogDestroy walks the context
|
|
// registry, and a spawn landing in that window used to be able to register a context the walk had
|
|
// already passed -- freed, with its thread still running, along with the slot array the spawn had
|
|
// just reallocated under the walk's feet. calogContextOpen now refuses once teardown has latched, so
|
|
// the spawns simply start failing. Its own runtime, since it destroys it: surviving IS the check
|
|
// (under ASan a missed context is a use-after-free, under TSan a data race on the slot table).
|
|
static void testTaskSpawnRacesTeardown(void) {
|
|
CalogT *racer;
|
|
CalogContextT *ctx;
|
|
struct timespec ts = { 0, 2000000 };
|
|
int32_t i;
|
|
|
|
racer = calogCreate();
|
|
if (racer == NULL) {
|
|
CHECK(false, "spawn-vs-teardown: runtime create failed");
|
|
return;
|
|
}
|
|
calogSetErrorHandler(racer, onError, NULL);
|
|
if (calogTaskRegister(racer) != calogOkE) {
|
|
CHECK(false, "spawn-vs-teardown: task library register failed");
|
|
calogDestroy(racer);
|
|
return;
|
|
}
|
|
ctx = calogContextOpen(racer, &calogLuaEngine);
|
|
if (ctx == NULL) {
|
|
CHECK(false, "spawn-vs-teardown: context open failed");
|
|
calogDestroy(racer);
|
|
return;
|
|
}
|
|
// Spawn AND close in the same loop: taskClose reaches calogContextClose from this script's own
|
|
// thread, so it races the teardown's own stop-join-free of the very same context. Keep going
|
|
// until a spawn is refused (which is what teardown does to it) or the budget runs out; each
|
|
// child exits immediately, so the thread count stays bounded.
|
|
calogContextEval(ctx,
|
|
"for i = 1, 200 do\n"
|
|
" local ok, task = pcall(taskSpawn, 'lua', 'taskExit()')\n"
|
|
" if not ok then break end\n"
|
|
" pcall(taskClose, task)\n"
|
|
"end");
|
|
for (i = 0; i < 4; i++) {
|
|
calogPump(racer);
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
calogDestroy(racer); // races the spawn loop above
|
|
CHECK(true, "a spawn loop racing calogDestroy tears down cleanly");
|
|
}
|
|
|
|
|
|
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);
|
|
testTaskSpawnRacesTeardown();
|
|
|
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
|
fflush(stdout);
|
|
if (testsFailed != 0) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|