calog/tests/testTask.c

320 lines
9.7 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 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;
}