392 lines
13 KiB
C
392 lines
13 KiB
C
// testEngineLua.c -- Lua on a context thread, driven the way a host drives calog:
|
|
// register natives, open a context, fire-and-forget a script, and calogPump on the
|
|
// host thread to service the script's native calls. Verifies host-thread dispatch,
|
|
// the inline escape hatch, the sec-10 cross-thread callback, the error handler, and
|
|
// concurrent contexts. Built under ASan+UBSan.
|
|
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "calog.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 // ~2s at 0.5ms/iter, a generous upper bound
|
|
|
|
static CalogT *calog = NULL;
|
|
static _Atomic int64_t reportedValue = 0;
|
|
static _Atomic uint64_t reportedCtxId = 0xFFFFu;
|
|
static _Atomic uint64_t inlineCtxId = 0xFFFFu;
|
|
static _Atomic int32_t bumpCount = 0;
|
|
static _Atomic bool scriptDone = false;
|
|
static _Atomic int32_t errorCount = 0;
|
|
static _Atomic uint64_t errorCtxId = 0;
|
|
static CalogFnT *_Atomic storedCb = NULL;
|
|
static _Atomic int32_t matchCount = 0;
|
|
static _Atomic int32_t mismatchCount = 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 nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t nativeCheckRuntime(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t nativeDone(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t nativeSetCb(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 testConcurrentContexts(void);
|
|
static void testContextLoad(void);
|
|
static void testCrossThreadCallback(void);
|
|
static void testForeignFunction(void);
|
|
static void testHostAndInlineNatives(void);
|
|
static void testScriptError(void);
|
|
static void testSingleThreadMultiRuntime(void);
|
|
|
|
|
|
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 nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)userData;
|
|
calogValueNil(result);
|
|
if (argCount != 2 || args[0].type != calogIntE || args[1].type != calogIntE) {
|
|
return calogFail(result, calogErrArgE, "add expects two integers");
|
|
}
|
|
calogValueInt(result, args[0].as.i + args[1].as.i);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t nativeBump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)args;
|
|
(void)argCount;
|
|
(void)userData;
|
|
atomic_fetch_add(&bumpCount, 1);
|
|
calogValueNil(result);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t nativeCheckRuntime(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)args;
|
|
(void)argCount;
|
|
// userData is the runtime this native was registered under. Run from a script and
|
|
// serviced by calogPump, it must see calogCurrent() == that runtime -- which holds
|
|
// only if pump presents the pumping thread as the pumped runtime's host, so one
|
|
// thread can host several runtimes.
|
|
if (calogCurrent() == (CalogT *)userData) {
|
|
atomic_fetch_add(&matchCount, 1);
|
|
} else {
|
|
atomic_fetch_add(&mismatchCount, 1);
|
|
}
|
|
calogValueNil(result);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
static int32_t nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
CalogFnT *callable;
|
|
int32_t status;
|
|
|
|
(void)args;
|
|
(void)argCount;
|
|
(void)userData;
|
|
calogValueNil(result);
|
|
// A host-owned function value handed to the script; calling it routes back here.
|
|
status = calogFnFromNative(&callable, calog, nativeAdd, NULL);
|
|
if (status != calogOkE) {
|
|
return calogFail(result, status, "getAdder could not allocate");
|
|
}
|
|
calogValueFn(result, callable);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)userData;
|
|
calogValueNil(result);
|
|
if (argCount != 1 || args[0].type != calogIntE) {
|
|
return calogFail(result, calogErrArgE, "report expects one integer");
|
|
}
|
|
atomic_store(&reportedCtxId, calogCurrentId()); // should be 0 (host thread)
|
|
atomic_store(&reportedValue, args[0].as.i);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t nativeReportInline(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)args;
|
|
(void)argCount;
|
|
(void)userData;
|
|
atomic_store(&inlineCtxId, calogCurrentId()); // should be the script's context id
|
|
calogValueNil(result);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)userData;
|
|
calogValueNil(result);
|
|
if (argCount != 1 || args[0].type != calogFnE) {
|
|
return calogFail(result, calogErrArgE, "setCb expects one function");
|
|
}
|
|
calogFnRetain(args[0].as.fn);
|
|
atomic_store(&storedCb, args[0].as.fn);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static void onError(uint64_t contextId, const char *message, void *userData) {
|
|
(void)message;
|
|
(void)userData;
|
|
atomic_store(&errorCtxId, contextId);
|
|
atomic_fetch_add(&errorCount, 1);
|
|
}
|
|
|
|
|
|
// Pump the host thread until the running script signals done (or errors), bounded.
|
|
static void pumpUntilDone(void) {
|
|
struct timespec ts = { 0, 500000 }; // 0.5 ms
|
|
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); // one more sweep to drain trailing calls
|
|
return;
|
|
}
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
}
|
|
|
|
|
|
static void testHostAndInlineNatives(void) {
|
|
CalogContextT *ctx;
|
|
|
|
ctx = calogContextOpen(calog, &calogLuaEngine);
|
|
CHECK(ctx != NULL, "opened a Lua context");
|
|
|
|
atomic_store(&scriptDone, false);
|
|
calogContextEval(ctx, "report(42); reportInline(1); done()");
|
|
pumpUntilDone();
|
|
|
|
CHECK(atomic_load(&reportedValue) == 42, "host native received the argument");
|
|
CHECK(atomic_load(&reportedCtxId) == 0, "default native ran on the host thread (id 0)");
|
|
CHECK(atomic_load(&inlineCtxId) == calogContextId(ctx), "inline native ran on the script's own thread");
|
|
|
|
calogContextClose(ctx);
|
|
}
|
|
|
|
|
|
static void testCrossThreadCallback(void) {
|
|
CalogContextT *ctx;
|
|
CalogFnT *callback;
|
|
CalogValueT arg;
|
|
CalogValueT result;
|
|
int32_t status;
|
|
|
|
ctx = calogContextOpen(calog, &calogLuaEngine);
|
|
atomic_store(&scriptDone, false);
|
|
atomic_store(&storedCb, NULL);
|
|
calogContextEval(ctx, "setCb(function(x) return x + 100 end); done()");
|
|
pumpUntilDone();
|
|
|
|
callback = atomic_load(&storedCb);
|
|
CHECK(callback != NULL, "a Lua closure was captured as a CalogFnT");
|
|
|
|
// Invoke it from the host thread: it must marshal to the Lua context's thread
|
|
// (sec 10). calogFnInvoke blocks-and-pumps the host queue while it waits.
|
|
calogValueInt(&arg, 7);
|
|
status = calogFnInvoke(callback, &arg, 1, &result);
|
|
CHECK(status == calogOkE && result.type == calogIntE && result.as.i == 107, "cross-thread callable invoke routed to the owner");
|
|
calogValueFree(&result);
|
|
calogValueFree(&arg);
|
|
calogFnRelease(callback);
|
|
|
|
calogContextClose(ctx);
|
|
}
|
|
|
|
|
|
static void testForeignFunction(void) {
|
|
CalogContextT *ctx;
|
|
|
|
ctx = calogContextOpen(calog, &calogLuaEngine);
|
|
atomic_store(&scriptDone, false);
|
|
atomic_store(&reportedValue, 0);
|
|
// The script receives a host-owned function value and calls it.
|
|
calogContextEval(ctx, "report(getAdder()(2, 3)); done()");
|
|
pumpUntilDone();
|
|
|
|
CHECK(atomic_load(&reportedValue) == 5, "script called a foreign function value pushed in from the host");
|
|
|
|
calogContextClose(ctx);
|
|
}
|
|
|
|
|
|
static void testScriptError(void) {
|
|
CalogContextT *ctx;
|
|
int32_t before;
|
|
|
|
ctx = calogContextOpen(calog, &calogLuaEngine);
|
|
before = atomic_load(&errorCount);
|
|
atomic_store(&scriptDone, false);
|
|
calogContextEval(ctx, "@@@ not valid lua @@@");
|
|
pumpUntilDone();
|
|
|
|
CHECK(atomic_load(&errorCount) == before + 1, "a fire-and-forget script error reached the error handler");
|
|
CHECK(atomic_load(&errorCtxId) == calogContextId(ctx), "the error names the failing context");
|
|
|
|
calogContextClose(ctx);
|
|
}
|
|
|
|
|
|
static void testConcurrentContexts(void) {
|
|
CalogContextT *ctxs[3];
|
|
struct timespec ts = { 0, 500000 };
|
|
int32_t i;
|
|
|
|
atomic_store(&bumpCount, 0);
|
|
for (i = 0; i < 3; i++) {
|
|
ctxs[i] = calogContextOpen(calog, &calogLuaEngine);
|
|
calogContextEval(ctxs[i], "bump(); bump(); bump()");
|
|
}
|
|
for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < 9; i++) {
|
|
calogPump(calog);
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
CHECK(atomic_load(&bumpCount) == 9, "three concurrent contexts all dispatched to the host thread");
|
|
for (i = 0; i < 3; i++) {
|
|
calogContextClose(ctxs[i]);
|
|
}
|
|
}
|
|
|
|
|
|
static void testContextLoad(void) {
|
|
CalogContextT *ctx;
|
|
struct timespec ts = { 0, 500000 };
|
|
FILE *file;
|
|
int32_t i;
|
|
|
|
// Registration makes the Lua engine (extension "lua") a load candidate.
|
|
calogRegisterEngine(calog, &calogLuaEngine);
|
|
|
|
// A file whose extension matches a registered engine is found, loaded, and run.
|
|
file = fopen("calogLoadTest.lua", "wb");
|
|
CHECK(file != NULL, "wrote a temporary .lua script");
|
|
if (file != NULL) {
|
|
fputs("bump()", file);
|
|
fclose(file);
|
|
}
|
|
atomic_store(&bumpCount, 0);
|
|
ctx = calogContextLoad(calog, "calogLoadTest");
|
|
CHECK(ctx != NULL, "calogContextLoad opened a context for the matching .lua file");
|
|
for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < 1; i++) {
|
|
calogPump(calog);
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
CHECK(atomic_load(&bumpCount) == 1, "the loaded script ran on its new context");
|
|
if (ctx != NULL) {
|
|
calogContextClose(ctx);
|
|
}
|
|
remove("calogLoadTest.lua");
|
|
|
|
// No file with a registered extension exists -> NULL.
|
|
ctx = calogContextLoad(calog, "calogNoSuchScript");
|
|
CHECK(ctx == NULL, "calogContextLoad returns NULL when no file matches");
|
|
}
|
|
|
|
|
|
static void testSingleThreadMultiRuntime(void) {
|
|
CalogT *calogB;
|
|
CalogContextT *ctxA;
|
|
CalogContextT *ctxB;
|
|
struct timespec ts = { 0, 500000 };
|
|
int32_t i;
|
|
|
|
// This one thread now hosts TWO runtimes. Each runs a script that calls a native
|
|
// registered under a different runtime; pumping each in turn must make calogCurrent()
|
|
// resolve to the runtime being pumped -- not whichever was created last.
|
|
calogB = calogCreate();
|
|
CHECK(calogB != NULL, "created a second runtime on this same thread");
|
|
calogRegister(calog, "checkRuntime", nativeCheckRuntime, calog);
|
|
calogRegister(calogB, "checkRuntime", nativeCheckRuntime, calogB);
|
|
|
|
atomic_store(&matchCount, 0);
|
|
atomic_store(&mismatchCount, 0);
|
|
ctxA = calogContextOpen(calog, &calogLuaEngine);
|
|
ctxB = calogContextOpen(calogB, &calogLuaEngine);
|
|
calogContextEval(ctxA, "checkRuntime()");
|
|
calogContextEval(ctxB, "checkRuntime()");
|
|
|
|
for (i = 0; i < PUMP_LIMIT && (atomic_load(&matchCount) + atomic_load(&mismatchCount)) < 2; i++) {
|
|
calogPump(calog);
|
|
calogPump(calogB);
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
CHECK(atomic_load(&matchCount) == 2, "one thread pumped two runtimes; each native saw its own runtime");
|
|
CHECK(atomic_load(&mismatchCount) == 0, "no native resolved to the wrong runtime while sharing a host thread");
|
|
|
|
calogContextClose(ctxA);
|
|
calogContextClose(ctxB);
|
|
calogDestroy(calogB);
|
|
}
|
|
|
|
|
|
int main(void) {
|
|
calog = calogCreate();
|
|
if (calog == NULL) {
|
|
printf("calog create failed\n");
|
|
return 1;
|
|
}
|
|
calogSetErrorHandler(calog, onError, NULL);
|
|
calogRegister(calog, "report", nativeReport, NULL);
|
|
calogRegister(calog, "setCb", nativeSetCb, NULL);
|
|
calogRegister(calog, "done", nativeDone, NULL);
|
|
calogRegister(calog, "bump", nativeBump, NULL);
|
|
calogRegister(calog, "getAdder", nativeGetAdder, NULL);
|
|
calogRegisterInline(calog, "reportInline", nativeReportInline, NULL);
|
|
|
|
testHostAndInlineNatives();
|
|
testCrossThreadCallback();
|
|
testForeignFunction();
|
|
testScriptError();
|
|
testConcurrentContexts();
|
|
testSingleThreadMultiRuntime();
|
|
testContextLoad();
|
|
|
|
calogDestroy(calog);
|
|
|
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
|
fflush(stdout);
|
|
if (testsFailed != 0) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|