222 lines
8.4 KiB
C
222 lines
8.4 KiB
C
// testEngineLua.c -- a real Lua interpreter running on a CalogContextT thread.
|
|
//
|
|
// Proves the CalogEngineT wiring: a Lua context creates its interpreter on its own
|
|
// thread (calogLuaEngine.createInterpreter), exposes broker natives there, and runs
|
|
// scripts via calogContextEval. The script calls one thread-agnostic native (report,
|
|
// owner 0, run inline on the Lua thread) and one native owned by a DIFFERENT
|
|
// context (doubleIt, owned by ctx2) -- the exposed-native trampoline routes that
|
|
// call through the broker to ctx2's thread and back, with no callback in the
|
|
// script. Built under ASan+UBSan; the threading core is covered by testActor.
|
|
|
|
#include "calog.h"
|
|
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
|
|
|
|
#define DOUBLE_EXPECTED 42
|
|
#define PURE_EXPECTED 6
|
|
|
|
static CalogT *broker = NULL;
|
|
static CalogContextT *luaCtx = NULL;
|
|
static CalogContextT *workerCtx = NULL;
|
|
static _Atomic int64_t reportedValue = 0;
|
|
static _Atomic uint32_t reportedCtxId = 0;
|
|
static _Atomic uint32_t doubleItCtxId = 0;
|
|
static CalogFnT *_Atomic storedCb = NULL;
|
|
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 nativeDoubleIt(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 nativeSetCb(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static void testCallbackAcrossContexts(void);
|
|
static void testCrossContextFromScript(void);
|
|
static void testFailedInterpreter(void);
|
|
static void testPureScript(void);
|
|
static void testScriptError(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 nativeDoubleIt(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)userData;
|
|
// Owned by workerCtx: routing must run this on workerCtx's thread, so record
|
|
// the id it actually ran on.
|
|
atomic_store(&doubleItCtxId, calogCurrentId());
|
|
if (argCount != 1 || args[0].type != calogIntE) {
|
|
return calogFail(result, calogErrArgE, "doubleIt expects one integer");
|
|
}
|
|
calogValueInt(result, args[0].as.i * 2);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)userData;
|
|
if (argCount != 1 || args[0].type != calogIntE) {
|
|
return calogFail(result, calogErrArgE, "report expects one integer");
|
|
}
|
|
atomic_store(&reportedCtxId, calogCurrentId());
|
|
atomic_store(&reportedValue, args[0].as.i);
|
|
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");
|
|
}
|
|
// Runs on luaCtx's thread; the closure is owned by luaCtx. Retain it so it
|
|
// outlives this call (the trampoline frees the args afterward).
|
|
calogFnRetain(args[0].as.fn);
|
|
atomic_store(&storedCb, args[0].as.fn);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static void testCallbackAcrossContexts(void) {
|
|
CalogFnT *callback;
|
|
CalogValueT arg;
|
|
CalogValueT result;
|
|
int32_t status;
|
|
|
|
// A Lua closure is captured on luaCtx's thread via setCb (owned by luaCtx).
|
|
status = calogContextEval(luaCtx, "setCb(function(x) return x + 100 end)", &result);
|
|
CHECK(status == calogOkE, "captured a Lua closure as a CalogFnT on its owner thread");
|
|
calogValueFree(&result);
|
|
|
|
callback = atomic_load(&storedCb);
|
|
CHECK(callback != NULL, "callback was stored");
|
|
|
|
// Invoke it from the MAIN thread: calogFnInvoke must marshal to luaCtx's thread
|
|
// (design.md sec 10) so the closure runs on its owner, never on the main thread.
|
|
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 context");
|
|
calogValueFree(&result);
|
|
calogValueFree(&arg);
|
|
|
|
// Drop the last reference from the main thread: the luaL_unref must run on
|
|
// luaCtx's thread, not here (ASan/TSan would flag an off-thread touch).
|
|
calogFnRelease(callback);
|
|
}
|
|
|
|
|
|
static void testCrossContextFromScript(void) {
|
|
CalogValueT result;
|
|
int32_t status;
|
|
|
|
// doubleIt is owned by workerCtx; report is thread-agnostic. The script reads
|
|
// as plain synchronous code -- the cross-context hop is invisible to it.
|
|
status = calogContextEval(luaCtx, "report(doubleIt(21))", &result);
|
|
CHECK(status == calogOkE, "script with a cross-context call ran without error");
|
|
CHECK(atomic_load(&reportedValue) == DOUBLE_EXPECTED, "cross-context doubleIt result flowed back into the script");
|
|
CHECK(atomic_load(&doubleItCtxId) == calogContextId(workerCtx), "doubleIt executed on the worker context's thread");
|
|
CHECK(atomic_load(&reportedCtxId) == calogContextId(luaCtx), "report executed on the Lua context's own thread");
|
|
calogValueFree(&result);
|
|
}
|
|
|
|
|
|
static void testFailedInterpreter(void) {
|
|
CalogContextT *failCtx;
|
|
const char *badNames[1];
|
|
CalogConfigT badConfig;
|
|
CalogValueT result;
|
|
int32_t status;
|
|
|
|
// A config naming a native that was never registered makes createInterpreter
|
|
// fail on the context's thread (interp stays NULL). The eval must come back as
|
|
// an error rather than dereferencing the NULL interpreter.
|
|
badNames[0] = "noSuchNative";
|
|
badConfig.exposeNames = badNames;
|
|
badConfig.exposeCount = 1;
|
|
calogContextCreate(broker, &calogLuaEngine, &badConfig, &failCtx);
|
|
calogContextStart(failCtx);
|
|
status = calogContextEval(failCtx, "report(1)", &result);
|
|
CHECK(status != calogOkE, "eval on a context whose interpreter failed to init returns an error, not a crash");
|
|
calogValueFree(&result);
|
|
calogContextDestroy(failCtx);
|
|
}
|
|
|
|
|
|
static void testPureScript(void) {
|
|
CalogValueT result;
|
|
int32_t status;
|
|
|
|
status = calogContextEval(luaCtx, "report(1 + 2 + 3)", &result);
|
|
CHECK(status == calogOkE && atomic_load(&reportedValue) == PURE_EXPECTED, "a second eval on the live interpreter ran");
|
|
calogValueFree(&result);
|
|
}
|
|
|
|
|
|
static void testScriptError(void) {
|
|
CalogValueT result;
|
|
int32_t status;
|
|
|
|
// A syntax error must come back as a failing status with the error in result
|
|
// (the single channel), and must not wedge the interpreter.
|
|
status = calogContextEval(luaCtx, "@@@ not valid lua @@@", &result);
|
|
CHECK(status != calogOkE && result.type == calogStringE, "script error surfaced through result, not a crash");
|
|
calogValueFree(&result);
|
|
}
|
|
|
|
|
|
int main(void) {
|
|
const char *exposeNames[3];
|
|
CalogConfigT luaConfig;
|
|
|
|
broker = calogCreate();
|
|
if (broker == NULL) {
|
|
printf("broker create failed\n");
|
|
return 1;
|
|
}
|
|
|
|
// Worker context hosts doubleIt; it has no interpreter (a pure native owner).
|
|
calogContextCreate(broker, NULL, NULL, &workerCtx);
|
|
|
|
exposeNames[0] = "doubleIt";
|
|
exposeNames[1] = "report";
|
|
exposeNames[2] = "setCb";
|
|
luaConfig.exposeNames = exposeNames;
|
|
luaConfig.exposeCount = 3;
|
|
calogContextCreate(broker, &calogLuaEngine, &luaConfig, &luaCtx);
|
|
|
|
// setCb is owned by luaCtx, so register it once luaCtx exists (for its id), but
|
|
// before calogContextStart -- createInterpreter exposes the names on the thread.
|
|
calogRegister(broker, "doubleIt", nativeDoubleIt, NULL, calogContextId(workerCtx));
|
|
calogRegister(broker, "report", nativeReport, NULL, 0);
|
|
calogRegister(broker, "setCb", nativeSetCb, NULL, calogContextId(luaCtx));
|
|
|
|
calogContextStart(workerCtx);
|
|
calogContextStart(luaCtx);
|
|
|
|
testCrossContextFromScript();
|
|
testPureScript();
|
|
testScriptError();
|
|
testFailedInterpreter();
|
|
testCallbackAcrossContexts();
|
|
|
|
calogDestroy(broker);
|
|
|
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
|
fflush(stdout);
|
|
if (testsFailed != 0) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|