382 lines
15 KiB
C
382 lines
15 KiB
C
// testEngineMyBasic.c -- my-basic on context threads under the actor model. Verifies a
|
|
// host native is serviced on the host thread, and that several my-basic contexts run
|
|
// concurrently without corruption -- my-basic keeps process-global state, so the engine
|
|
// serializes my-basic work with a lock; this is the test that would race without it.
|
|
// Built under ASan+UBSan (make test) and ThreadSanitizer (make tsanmb).
|
|
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "calog.h"
|
|
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <time.h>
|
|
|
|
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
|
|
|
|
#define PUMP_LIMIT 4000
|
|
#define CONTEXT_COUNT 3
|
|
|
|
// A value well beyond 32 bits: my-basic's int_t is a 64-bit long long in calog's fork, so this
|
|
// round-trips as a literal, through arithmetic, and both across the host boundary. 5e9 > 2^32.
|
|
#define WIDE_VALUE 5000000000LL
|
|
#define WIDE_ADDEND 1000000000LL
|
|
#define WIDE_SUM (WIDE_VALUE + WIDE_ADDEND)
|
|
|
|
static CalogT *calog = NULL;
|
|
static _Atomic int32_t bumpCount = 0;
|
|
static _Atomic uint64_t bumpCtxId = 0xFFFFu;
|
|
static _Atomic int64_t reportedValue = 0;
|
|
static int32_t testsRun = 0;
|
|
static int32_t testsFailed = 0;
|
|
|
|
static int32_t blobEcho(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t blobLen(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t bump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
|
|
static int64_t evalAndReport(const char *script);
|
|
static int32_t makeBlob(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t makeBlob2(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t nativeAdd(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 nativeGetWide(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static int32_t report(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
|
|
static void testBinaryStrings(void);
|
|
static void testConcurrentContexts(void);
|
|
static void testForeignFunction(void);
|
|
static void testHostNative(void);
|
|
static void testWideInteger(void);
|
|
|
|
|
|
static int32_t blobEcho(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)userData;
|
|
calogValueNil(result);
|
|
if (argCount != 1 || args[0].type != calogStringE) {
|
|
return calogFail(result, calogErrArgE, "blobEcho expects one string");
|
|
}
|
|
// Hand the same bytes straight back; the script re-ingresses them, exercising the egress ->
|
|
// ingress round-trip for a binary buffer (the httpd tcpRecv -> manipulate -> tcpSend path).
|
|
return calogValueString(result, args[0].as.s.bytes, args[0].as.s.length);
|
|
}
|
|
|
|
|
|
static int32_t blobLen(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)userData;
|
|
calogValueNil(result);
|
|
if (argCount != 1 || args[0].type != calogStringE) {
|
|
return calogFail(result, calogErrArgE, "blobLen expects one string");
|
|
}
|
|
// Reports the egress byte length: a byte buffer must arrive as a length-3 string, not truncated
|
|
// to length 1 at the embedded NUL.
|
|
calogValueInt(result, args[0].as.s.length);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t bump(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)args;
|
|
(void)argCount;
|
|
(void)userData;
|
|
atomic_store(&bumpCtxId, calogCurrentId());
|
|
atomic_fetch_add(&bumpCount, 1);
|
|
calogValueNil(result);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
|
|
// Open a fresh my-basic context, run `script` (which must end by calling bump()), pump until it
|
|
// finishes, and return whatever the script last handed to report(). Serialises the boilerplate the
|
|
// foreign-function and wide-integer tests share.
|
|
static int64_t evalAndReport(const char *script) {
|
|
CalogContextT *ctx;
|
|
struct timespec ts = { 0, 500000 };
|
|
int32_t i;
|
|
|
|
ctx = calogContextOpen(calog, &calogMyBasicEngine);
|
|
atomic_store(&bumpCount, 0);
|
|
atomic_store(&reportedValue, 0);
|
|
calogContextEval(ctx, script);
|
|
for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < 1; i++) {
|
|
calogPump(calog);
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
calogContextClose(ctx);
|
|
return atomic_load(&reportedValue);
|
|
}
|
|
|
|
|
|
static int32_t makeBlob(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)args;
|
|
(void)argCount;
|
|
(void)userData;
|
|
// A 3-byte binary value with an embedded NUL ('a', 0x00, 'b'). Because it holds a NUL, it
|
|
// ingresses to my-basic as a byte buffer rather than a (truncated) string.
|
|
return calogValueString(result, "a\0b", 3);
|
|
}
|
|
|
|
|
|
static int32_t makeBlob2(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)args;
|
|
(void)argCount;
|
|
(void)userData;
|
|
// Same length as makeBlob() but differs only AFTER the NUL, so a NUL-terminated compare would
|
|
// wrongly call it equal -- the byte compare must look past the NUL.
|
|
return calogValueString(result, "a\0c", 3);
|
|
}
|
|
|
|
|
|
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 nativeGetAdder(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
CalogFnT *callable;
|
|
int32_t status;
|
|
|
|
(void)args;
|
|
(void)argCount;
|
|
(void)userData;
|
|
calogValueNil(result);
|
|
// Hand the script a host-owned function value; invoking it from BASIC routes back to nativeAdd.
|
|
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 nativeGetWide(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
(void)args;
|
|
(void)argCount;
|
|
(void)userData;
|
|
// A 64-bit value handed into the script, to prove ingress no longer clamps at 2^31.
|
|
calogValueInt(result, WIDE_VALUE);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t report(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(&reportedValue, args[0].as.i);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
// Binary byte buffers: my-basic's faithful handling of calog strings that carry embedded NUL bytes,
|
|
// via the adapter's usertype-ref BYTE type. Non-zero sentinels (42, 11) are reported on success so a
|
|
// script that errored (leaving reportedValue at 0) fails the check instead of passing silently.
|
|
static void testBinaryStrings(void) {
|
|
int64_t got;
|
|
|
|
// Egress: a byte buffer handed to a native arrives as a full length-3 string, not truncated to 1.
|
|
got = evalAndReport("report(blobLen(makeBlob()))\nbump()");
|
|
CHECK(got == 3, "a byte buffer egresses to the host as a full-length binary string");
|
|
|
|
// byteLen sees the true byte count.
|
|
got = evalAndReport("b = makeBlob()\nreport(byteLen(b))\nbump()");
|
|
CHECK(got == 3, "byteLen reports the full length including the embedded NUL");
|
|
|
|
// byteAt reads every byte, the embedded NUL included.
|
|
got = evalAndReport(
|
|
"b = makeBlob()\n"
|
|
"r = 0\n"
|
|
"IF byteAt(b, 0) = 97 THEN\n"
|
|
"IF byteAt(b, 1) = 0 THEN\n"
|
|
"IF byteAt(b, 2) = 98 THEN\n"
|
|
"r = 42\n"
|
|
"ENDIF\nENDIF\nENDIF\n"
|
|
"report(r)\nbump()");
|
|
CHECK(got == 42, "byteAt reads each byte including the embedded NUL as 0");
|
|
|
|
// '+' concatenates byte buffers and the interior NUL survives.
|
|
got = evalAndReport(
|
|
"b = makeBlob()\n"
|
|
"c = b + b\n"
|
|
"r = 0\n"
|
|
"IF byteLen(c) = 6 THEN\n"
|
|
"IF byteAt(c, 4) = 0 THEN\n"
|
|
"IF byteAt(c, 3) = 97 THEN\n"
|
|
"r = 42\n"
|
|
"ENDIF\nENDIF\nENDIF\n"
|
|
"report(r)\nbump()");
|
|
CHECK(got == 42, "the + operator concatenates byte buffers and preserves the interior NUL");
|
|
|
|
// Mixed concat: a text prefix plus a binary body (the httpd response-building case).
|
|
got = evalAndReport(
|
|
"b = makeBlob()\n"
|
|
"c = \"X\" + b\n"
|
|
"r = 0\n"
|
|
"IF byteLen(c) = 4 THEN\n"
|
|
"IF byteAt(c, 0) = 88 THEN\n"
|
|
"r = 42\n"
|
|
"ENDIF\nENDIF\n"
|
|
"report(r)\nbump()");
|
|
CHECK(got == 42, "a string prefix concatenates with a byte buffer (string + bytes)");
|
|
|
|
// Content equality via the fork's comparison patch: distinct buffers, identical bytes.
|
|
got = evalAndReport("b = makeBlob()\nd = makeBlob()\nIF b = d THEN\nreport(11)\nELSE\nreport(22)\nENDIF\nbump()");
|
|
CHECK(got == 11, "= compares byte buffers by content (equal content compares equal)");
|
|
|
|
// ...and buffers that differ only past the NUL compare unequal.
|
|
got = evalAndReport("b = makeBlob()\ne = makeBlob2()\nIF b = e THEN\nreport(11)\nELSE\nreport(22)\nENDIF\nbump()");
|
|
CHECK(got == 22, "= distinguishes byte buffers that differ only after the NUL");
|
|
|
|
// A byte buffer works as a content-addressed dict key (the hash and cmp hooks agree).
|
|
got = evalAndReport(
|
|
"m = DICT()\n"
|
|
"b = makeBlob()\n"
|
|
"e = makeBlob2()\n"
|
|
"SET(m, b, 10)\n"
|
|
"SET(m, e, 20)\n"
|
|
"report(GET(m, makeBlob()))\nbump()");
|
|
CHECK(got == 10, "a byte buffer is a content-addressed dict key");
|
|
|
|
// byteSlice extracts a sub-range, NUL included, clamped to the buffer.
|
|
got = evalAndReport(
|
|
"b = makeBlob()\n"
|
|
"s = byteSlice(b, 1, 2)\n"
|
|
"r = 0\n"
|
|
"IF byteLen(s) = 2 THEN\n"
|
|
"IF byteAt(s, 0) = 0 THEN\n"
|
|
"IF byteAt(s, 1) = 98 THEN\n"
|
|
"r = 42\n"
|
|
"ENDIF\nENDIF\nENDIF\n"
|
|
"report(r)\nbump()");
|
|
CHECK(got == 42, "byteSlice extracts a byte sub-range spanning the NUL");
|
|
|
|
// byteConcat joins several parts (byte buffers and strings) into one buffer.
|
|
got = evalAndReport("report(byteLen(byteConcat(makeBlob(), \"X\", makeBlob())))\nbump()");
|
|
CHECK(got == 7, "byteConcat joins byte buffers and strings (3 + 1 + 3 = 7)");
|
|
|
|
// Round-trip: egress to a native and back keeps the bytes.
|
|
got = evalAndReport("report(byteLen(blobEcho(makeBlob())))\nbump()");
|
|
CHECK(got == 3, "a byte buffer round-trips out to a native and back intact");
|
|
|
|
// strToByte / byteToStr round-trip NUL-free text.
|
|
got = evalAndReport("IF byteToStr(strToByte(\"hi\")) = \"hi\" THEN\nreport(11)\nELSE\nreport(22)\nENDIF\nbump()");
|
|
CHECK(got == 11, "strToByte and byteToStr round-trip NUL-free text");
|
|
}
|
|
|
|
|
|
static void testConcurrentContexts(void) {
|
|
CalogContextT *ctxs[CONTEXT_COUNT];
|
|
struct timespec ts = { 0, 500000 };
|
|
int32_t i;
|
|
|
|
// The scripts allocate (list creation) as they run so the parallel execution paths
|
|
// churn my-basic's now-atomic allocation counter concurrently -- the case that
|
|
// raced before the vendored patch (see make tsanmb).
|
|
atomic_store(&bumpCount, 0);
|
|
for (i = 0; i < CONTEXT_COUNT; i++) {
|
|
ctxs[i] = calogContextOpen(calog, &calogMyBasicEngine);
|
|
calogContextEval(ctxs[i], "a = list(1, 2, 3)\nb = list(4, 5, 6)\nbump()");
|
|
}
|
|
for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < CONTEXT_COUNT; i++) {
|
|
calogPump(calog);
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
CHECK(atomic_load(&bumpCount) == CONTEXT_COUNT, "several concurrent my-basic contexts all reached the host native");
|
|
for (i = 0; i < CONTEXT_COUNT; i++) {
|
|
calogContextClose(ctxs[i]);
|
|
}
|
|
}
|
|
|
|
|
|
static void testForeignFunction(void) {
|
|
int64_t got;
|
|
|
|
// getAdder() yields a host callable; calogInvoke(fn, ...) calls it from BASIC. This is the
|
|
// my-basic side of function-into-script -- the fork's usertype-ref + calogInvoke native.
|
|
got = evalAndReport("f = getAdder()\nreport(calogInvoke(f, 2, 3))\nbump()");
|
|
CHECK(got == 5, "my-basic invoked a host function value handed into the script");
|
|
}
|
|
|
|
|
|
static void testHostNative(void) {
|
|
CalogContextT *ctx;
|
|
struct timespec ts = { 0, 500000 };
|
|
int32_t i;
|
|
|
|
ctx = calogContextOpen(calog, &calogMyBasicEngine);
|
|
CHECK(ctx != NULL, "opened a my-basic context");
|
|
atomic_store(&bumpCount, 0);
|
|
calogContextEval(ctx, "bump()");
|
|
for (i = 0; i < PUMP_LIMIT && atomic_load(&bumpCount) < 1; i++) {
|
|
calogPump(calog);
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
CHECK(atomic_load(&bumpCount) == 1, "the my-basic script's native call ran");
|
|
CHECK(atomic_load(&bumpCtxId) == 0, "the native ran on the host thread (id 0)");
|
|
calogContextClose(ctx);
|
|
}
|
|
|
|
|
|
static void testWideInteger(void) {
|
|
int64_t got;
|
|
|
|
// A >2^32 literal: exercises strtoll parsing and the 64-bit int_t, then egress to the host.
|
|
got = evalAndReport("report(5000000000)\nbump()");
|
|
CHECK(got == WIDE_VALUE, "my-basic parsed and returned a 64-bit integer literal");
|
|
|
|
// Ingress (getWide, no clamp) + 64-bit arithmetic in BASIC + egress back to the host.
|
|
got = evalAndReport("report(getWide() + 1000000000)\nbump()");
|
|
CHECK(got == WIDE_SUM, "my-basic round-tripped a 64-bit value through arithmetic");
|
|
|
|
// ABS() must use llabs, not the 32-bit C abs(), now that int_t is 64-bit.
|
|
got = evalAndReport("report(ABS(0 - 5000000000))\nbump()");
|
|
CHECK(got == WIDE_VALUE, "my-basic ABS() preserves a 64-bit magnitude (no int truncation)");
|
|
}
|
|
|
|
|
|
int main(void) {
|
|
calog = calogCreate();
|
|
if (calog == NULL) {
|
|
printf("calog create failed\n");
|
|
return 1;
|
|
}
|
|
calogRegister(calog, "blobEcho", blobEcho, NULL);
|
|
calogRegister(calog, "blobLen", blobLen, NULL);
|
|
calogRegister(calog, "bump", bump, NULL);
|
|
calogRegister(calog, "getAdder", nativeGetAdder, NULL);
|
|
calogRegister(calog, "getWide", nativeGetWide, NULL);
|
|
calogRegister(calog, "makeBlob", makeBlob, NULL);
|
|
calogRegister(calog, "makeBlob2", makeBlob2, NULL);
|
|
calogRegister(calog, "report", report, NULL);
|
|
|
|
testHostNative();
|
|
testConcurrentContexts();
|
|
testForeignFunction();
|
|
testWideInteger();
|
|
testBinaryStrings();
|
|
|
|
calogDestroy(calog);
|
|
|
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
|
fflush(stdout);
|
|
if (testsFailed != 0) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|