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