// testEngineJanet.c -- Janet on a context thread, driven the host way: register natives, open a // context, fire-and-forget a script, and calogPump on the host thread. Verifies host-thread // dispatch, the inline escape hatch, the cross-thread callback, aggregates both ways (array/table), // a foreign function value, the error handler, and THREE concurrent VMs (Janet's thread-local // janet_vm is the make-or-break gate for the actor model). #define _POSIX_C_SOURCE 200809L #include "calog.h" #include #include #include #include #include #define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__) #define PUMP_LIMIT 4000 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 char storedName[32] = { 0 }; static _Atomic int32_t nameLen = -1; 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 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 nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeMapAge(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 nativeReportName(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 testCrossThreadCallback(void); static void testForeignFunction(void); static void testHostAndInlineNatives(void); static void testMapIngress(void); static void testMaterializedRecord(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 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 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); 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 nativeMakeUser(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { CalogAggT *user; CalogValueT key; CalogValueT val; int32_t status; (void)args; (void)argCount; (void)userData; calogValueNil(result); status = calogAggCreate(&user, calogMapE); if (status != calogOkE) { return calogFail(result, status, "makeUser could not allocate"); } calogValueString(&key, "name", 4); calogValueString(&val, "ada", 3); calogAggSet(user, &key, &val); calogValueString(&key, "age", 3); calogValueInt(&val, 36); calogAggSet(user, &key, &val); calogValueAgg(result, user); return calogOkE; } static int32_t nativeMapAge(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { CalogValueT key; CalogValueT *found; (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogAggE) { return calogFail(result, calogErrArgE, "mapAge expects a map"); } calogValueString(&key, "age", 3); found = calogAggGet(args[0].as.agg, &key); calogValueFree(&key); if (found != NULL && found->type == calogIntE) { atomic_store(&reportedValue, found->as.i); } 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()); 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()); calogValueNil(result); return calogOkE; } static int32_t nativeReportName(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { size_t length; (void)userData; calogValueNil(result); if (argCount != 1 || args[0].type != calogStringE) { return calogFail(result, calogErrArgE, "reportName expects one string"); } length = (size_t)args[0].as.s.length; if (length >= sizeof(storedName)) { length = sizeof(storedName) - 1; } memcpy(storedName, args[0].as.s.bytes, length); storedName[length] = '\0'; atomic_store(&nameLen, (int32_t)length); 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); } 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); } } static void testHostAndInlineNatives(void) { CalogContextT *ctx; ctx = calogContextOpen(calog, &calogJanetEngine); CHECK(ctx != NULL, "opened a Janet 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 testMaterializedRecord(void) { CalogContextT *ctx; ctx = calogContextOpen(calog, &calogJanetEngine); atomic_store(&scriptDone, false); atomic_store(&nameLen, -1); atomic_store(&reportedValue, 0); calogContextEval(ctx, "(def u (makeUser)) (report (get u \"age\")) (reportName (get u \"name\")) (done)"); pumpUntilDone(); CHECK(atomic_load(&reportedValue) == 36, "read an int field from a materialized record table"); CHECK(atomic_load(&nameLen) == 3 && memcmp(storedName, "ada", 3) == 0, "read a string field from a materialized record table"); calogContextClose(ctx); } static void testMapIngress(void) { CalogContextT *ctx; ctx = calogContextOpen(calog, &calogJanetEngine); atomic_store(&scriptDone, false); atomic_store(&reportedValue, 0); // The script builds a table (with a nested array, exercising array ingress too) and hands it // to a native, which reads a field. calogContextEval(ctx, "(def m @{\"age\" 9 \"tags\" @[1 2]}) (mapAge m) (done)"); pumpUntilDone(); CHECK(atomic_load(&reportedValue) == 9, "native read a field from a table the script built"); calogContextClose(ctx); } static void testForeignFunction(void) { CalogContextT *ctx; ctx = calogContextOpen(calog, &calogJanetEngine); atomic_store(&scriptDone, false); atomic_store(&reportedValue, 0); // getAdder returns a function value (a callable abstract); the script calls it as (adder 2 3). calogContextEval(ctx, "(def adder (getAdder)) (report (adder 2 3)) (done)"); pumpUntilDone(); CHECK(atomic_load(&reportedValue) == 5, "script called a foreign function value pushed in from the host"); calogContextClose(ctx); } static void testCrossThreadCallback(void) { CalogContextT *ctx; CalogFnT *callback; CalogValueT arg; CalogValueT result; int32_t status; ctx = calogContextOpen(calog, &calogJanetEngine); atomic_store(&scriptDone, false); atomic_store(&storedCb, NULL); // A Janet function handed to setCb marshals to a CalogFnT (the function is gc-rooted). calogContextEval(ctx, "(defn cb [x] (+ x 100)) (setCb cb) (done)"); pumpUntilDone(); callback = atomic_load(&storedCb); CHECK(callback != NULL, "a Janet function was captured as a CalogFnT"); 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 testScriptError(void) { CalogContextT *ctx; int32_t before; ctx = calogContextOpen(calog, &calogJanetEngine); before = atomic_load(&errorCount); atomic_store(&scriptDone, false); calogContextEval(ctx, "(error \"intentional test error\")"); 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, &calogJanetEngine); 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 Janet VMs (thread-local janet_vm) all dispatched to the host"); for (i = 0; i < 3; i++) { calogContextClose(ctxs[i]); } } 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, "makeUser", nativeMakeUser, NULL); calogRegister(calog, "reportName", nativeReportName, NULL); calogRegister(calog, "getAdder", nativeGetAdder, NULL); calogRegister(calog, "mapAge", nativeMapAge, NULL); calogRegisterInline(calog, "reportInline", nativeReportInline, NULL); testHostAndInlineNatives(); testMaterializedRecord(); testMapIngress(); testForeignFunction(); testCrossThreadCallback(); testScriptError(); testConcurrentContexts(); calogDestroy(calog); printf("\n%d checks, %d failed\n", testsRun, testsFailed); fflush(stdout); if (testsFailed != 0) { return 1; } return 0; }