// testJs.c -- single-threaded Duktape (JavaScript) adapter tests. // // Mirrors testSquirrel/testLua: scalar/string/array/object marshalling between JS // and CalogValueT, a JS function pinned as a CalogFnT and invoked from C through the // broker, a closure passed as a native argument and called back, and the export // error paths. Single-threaded on purpose (a CalogFnT is invoked on its owner's // heap thread); the threaded engine path is testEngineJs. Built under ASan+UBSan. #include "calogInternal.h" #include "jsAdapter.h" #include #include #include #define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__) #define DOUBLER_INPUT 21 #define DOUBLER_EXPECTED 42 #define ADDER_EXPECTED 6 #define SUM_EXPECTED 42 static CalogT *broker = NULL; static CalogJsT *jsctx = NULL; static CalogValueT recorded; 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 nativeApplyTo5(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeRecord(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeSumArr(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static void testExportErrors(void); static void testExportInvoke(void); static void testFunctionValueArg(void); static void testMarshalling(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 nativeApplyTo5(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { CalogValueT arg; CalogValueT inner; int32_t status; (void)userData; if (argCount != 1 || args[0].type != calogFnE) { return calogFail(result, calogErrArgE, "applyTo5 expects one function"); } calogValueInt(&arg, 5); status = calogFnInvoke(args[0].as.fn, &arg, 1, &inner); calogValueFree(&arg); calogValueMove(result, &inner); return status; } static int32_t nativeRecord(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { int32_t status; (void)userData; calogValueFree(&recorded); calogValueNil(&recorded); calogValueNil(result); if (argCount != 1) { return calogFail(result, calogErrArgE, "record expects one argument"); } status = calogValueCopy(&recorded, &args[0]); if (status != calogOkE) { return calogFail(result, status, "record failed to copy argument"); } return calogOkE; } static int32_t nativeSumArr(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { CalogAggT *aggregate; int64_t sum; int64_t index; (void)userData; if (argCount != 1 || args[0].type != calogAggE) { return calogFail(result, calogErrArgE, "sumArr expects one array"); } aggregate = args[0].as.agg; sum = 0; for (index = 0; index < aggregate->arrayCount; index++) { if (aggregate->array[index].type != calogIntE) { return calogFail(result, calogErrTypeE, "sumArr expects integer elements"); } sum += aggregate->array[index].as.i; } calogValueInt(result, sum); return calogOkE; } static void testExportErrors(void) { CalogFnT *callable; int32_t status; status = calogJsExport(jsctx, "noSuchGlobal", &callable); CHECK(status == calogErrNotFoundE && callable == NULL, "exporting an undefined name fails as not-found"); status = calogJsRun(jsctx, "var myNumber = 5;"); CHECK(status == calogOkE, "define a non-function global"); status = calogJsExport(jsctx, "myNumber", &callable); CHECK(status == calogErrTypeE && callable == NULL, "exporting a non-function fails as a type error"); } static void testExportInvoke(void) { CalogFnT *doubler; CalogValueT arg; CalogValueT result; int32_t status; status = calogJsRun(jsctx, "function doubler(x) { return x * 2; }"); CHECK(status == calogOkE, "define doubler"); status = calogJsExport(jsctx, "doubler", &doubler); CHECK(status == calogOkE, "export doubler as a CalogFnT"); calogValueInt(&arg, DOUBLER_INPUT); status = calogFnInvoke(doubler, &arg, 1, &result); CHECK(status == calogOkE && result.type == calogIntE && result.as.i == DOUBLER_EXPECTED, "invoke exported JS function from C"); calogValueFree(&result); calogValueFree(&arg); calogFnRelease(doubler); } static void testFunctionValueArg(void) { int32_t status; // The anonymous closure crosses into the native as a CalogFnT, is invoked with // 5, and the result (6) is recorded. The CalogFnT is released when the native // call's args are freed -- all on this one thread. status = calogJsRun(jsctx, "record(applyTo5(function(x) { return x + 1; }))"); CHECK(status == calogOkE && recorded.type == calogIntE && recorded.as.i == ADDER_EXPECTED, "closure passed as an argument is called back through the broker"); } static void testMarshalling(void) { CalogValueT key; CalogValueT *got; int32_t status; status = calogJsRun(jsctx, "record('hi\\u0000there')"); CHECK(status == calogOkE && recorded.type == calogStringE && recorded.as.s.length == 8, "binary-safe string marshalled from JS (embedded NUL preserved)"); status = calogJsRun(jsctx, "record(sumArr([10, 20, 12]))"); CHECK(status == calogOkE && recorded.type == calogIntE && recorded.as.i == SUM_EXPECTED, "JS array marshalled into the hybrid aggregate"); status = calogJsRun(jsctx, "record({a: 1, b: 2})"); CHECK(status == calogOkE && recorded.type == calogAggE && recorded.as.agg->pairCount == 2, "JS object marshalled into a map aggregate"); status = calogValueString(&key, "b", 1); CHECK(status == calogOkE, "build lookup key"); got = (recorded.type == calogAggE) ? calogAggGet(recorded.as.agg, &key) : NULL; CHECK(got != NULL && got->type == calogIntE && got->as.i == 2, "object key/value survived the round-trip"); calogValueFree(&key); // A JS BigInt round-trips to int64 EXACTLY -- the fidelity Duktape (double-only) // could not offer: 2^53 + 1 survives, where a double would round it down to 2^53. status = calogJsRun(jsctx, "record(9007199254740993n)"); CHECK(status == calogOkE && recorded.type == calogIntE && recorded.as.i == 9007199254740993LL, "JS BigInt round-trips to int64 exactly (2^53+1 preserved)"); } int main(void) { broker = calogBrokerCreate(); if (broker == NULL) { printf("broker create failed\n"); return 1; } if (calogJsCreate(&jsctx, broker, 0) != calogOkE) { printf("js context create failed\n"); return 1; } calogRegister(broker, "record", nativeRecord, NULL); calogRegister(broker, "applyTo5", nativeApplyTo5, NULL); calogRegister(broker, "sumArr", nativeSumArr, NULL); calogJsExpose(jsctx, "record"); calogJsExpose(jsctx, "applyTo5"); calogJsExpose(jsctx, "sumArr"); calogValueNil(&recorded); testExportInvoke(); testFunctionValueArg(); testMarshalling(); testExportErrors(); calogValueFree(&recorded); calogJsDestroy(jsctx); calogBrokerDestroy(broker); printf("\n%d checks, %d failed\n", testsRun, testsFailed); fflush(stdout); if (testsFailed != 0) { return 1; } return 0; }