// testEngineBerry.c -- Berry 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 sec-10 cross-thread // callback, the error handler, and concurrent contexts. #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); // 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 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"); } // Read a field out of a map the script built and handed to C (aggregate ingress). 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, &calogBerryEngine); CHECK(ctx != NULL, "opened a Berry context"); atomic_store(&scriptDone, false); calogContextEval(ctx, "report(42)\nreportInline(1)\ndone()"); 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, &calogBerryEngine); atomic_store(&scriptDone, false); atomic_store(&nameLen, -1); atomic_store(&reportedValue, 0); calogContextEval(ctx, "u = makeUser()\nreport(u['age'])\nreportName(u['name'])\ndone()"); pumpUntilDone(); CHECK(atomic_load(&reportedValue) == 36, "read an int field from a materialized record map"); CHECK(atomic_load(&nameLen) == 3 && memcmp(storedName, "ada", 3) == 0, "read a string field from a materialized record map"); calogContextClose(ctx); } static void testMapIngress(void) { CalogContextT *ctx; ctx = calogContextOpen(calog, &calogBerryEngine); atomic_store(&scriptDone, false); atomic_store(&reportedValue, 0); // The script builds a map (with a nested list, exercising list ingress too) and // hands it to a native, which reads a field. calogContextEval(ctx, "m = {'age': 9, 'tags': [1, 2]}\nmapAge(m)\ndone()"); pumpUntilDone(); CHECK(atomic_load(&reportedValue) == 9, "native read a field from a map the script built"); calogContextClose(ctx); } static void testForeignFunction(void) { CalogContextT *ctx; ctx = calogContextOpen(calog, &calogBerryEngine); atomic_store(&scriptDone, false); atomic_store(&reportedValue, 0); // The script receives a host-owned function value and calls it. calogContextEval(ctx, "u = getAdder()\nreport(u(2, 3))\ndone()"); 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, &calogBerryEngine); atomic_store(&scriptDone, false); atomic_store(&storedCb, NULL); calogContextEval(ctx, "def cb(x) return x + 100 end\nsetCb(cb)\ndone()"); pumpUntilDone(); callback = atomic_load(&storedCb); CHECK(callback != NULL, "a Berry closure 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, &calogBerryEngine); before = atomic_load(&errorCount); atomic_store(&scriptDone, false); calogContextEval(ctx, "@@@ not valid berry @@@"); 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, &calogBerryEngine); calogContextEval(ctxs[i], "bump()\nbump()\nbump()"); } 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]); } } 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; }