225 lines
7.3 KiB
C
225 lines
7.3 KiB
C
// testDb.c -- exercises the calog DB library end to end against an in-memory SQLite
|
|
// database, driven from a Lua script: open, create, insert with bound params (including
|
|
// binary-safe data), update (rows-affected), query into row maps, close, and an error path.
|
|
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "calog.h"
|
|
#include "calogDb.h"
|
|
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
|
|
|
|
#define PUMP_LIMIT 4000
|
|
|
|
static CalogT *calog = NULL;
|
|
static _Atomic int64_t reportedCount = -1;
|
|
static _Atomic int64_t reportedId = -1;
|
|
static _Atomic int64_t execAffected = -1;
|
|
static char reportedName[32] = { 0 };
|
|
static char blobData[32] = { 0 };
|
|
static _Atomic int32_t blobLen = -1;
|
|
static _Atomic bool scriptDone = false;
|
|
static _Atomic int32_t errorCount = 0;
|
|
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 nativeDone(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 nativeReportBlob(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 testDbBinary(void);
|
|
static void testDbError(void);
|
|
static void testDbRoundtrip(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 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 nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
|
|
size_t length;
|
|
|
|
(void)userData;
|
|
calogValueNil(result);
|
|
if (argCount != 4 || args[0].type != calogIntE || args[1].type != calogIntE || args[2].type != calogStringE || args[3].type != calogIntE) {
|
|
return calogFail(result, calogErrArgE, "report expects (count, id, name, affected)");
|
|
}
|
|
atomic_store(&reportedCount, args[0].as.i);
|
|
atomic_store(&reportedId, args[1].as.i);
|
|
length = (size_t)args[2].as.s.length;
|
|
if (length >= sizeof(reportedName)) {
|
|
length = sizeof(reportedName) - 1;
|
|
}
|
|
memcpy(reportedName, args[2].as.s.bytes, length);
|
|
reportedName[length] = '\0';
|
|
atomic_store(&execAffected, args[3].as.i);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static int32_t nativeReportBlob(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, "reportBlob expects one string");
|
|
}
|
|
length = (size_t)args[0].as.s.length;
|
|
if (length > sizeof(blobData)) {
|
|
length = sizeof(blobData);
|
|
}
|
|
memcpy(blobData, args[0].as.s.bytes, length);
|
|
atomic_store(&blobLen, (int32_t)length);
|
|
return calogOkE;
|
|
}
|
|
|
|
|
|
static void onError(uint64_t contextId, const char *message, void *userData) {
|
|
(void)contextId;
|
|
(void)message;
|
|
(void)userData;
|
|
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 testDbBinary(void) {
|
|
CalogContextT *ctx;
|
|
|
|
ctx = calogContextOpen(calog, &calogLuaEngine);
|
|
atomic_store(&scriptDone, false);
|
|
atomic_store(&blobLen, -1);
|
|
// Bind and read back a value with an embedded NUL -- proves binary-safe round-trip.
|
|
calogContextEval(ctx,
|
|
"local db = dbOpen('sqlite', ':memory:')\n"
|
|
"dbExec(db, 'create table b (v blob)')\n"
|
|
"dbExec(db, 'insert into b values (?)', 'a\\0b')\n"
|
|
"local rows = dbQuery(db, 'select v from b')\n"
|
|
"reportBlob(rows[1].v)\n"
|
|
"dbClose(db)\n"
|
|
"done()");
|
|
pumpUntilDone();
|
|
|
|
CHECK(atomic_load(&blobLen) == 3, "binary value round-tripped with its exact byte length");
|
|
CHECK(blobData[0] == 'a' && blobData[1] == '\0' && blobData[2] == 'b', "the embedded NUL survived the DB round-trip");
|
|
|
|
calogContextClose(ctx);
|
|
}
|
|
|
|
|
|
static void testDbError(void) {
|
|
CalogContextT *ctx;
|
|
int32_t before;
|
|
|
|
ctx = calogContextOpen(calog, &calogLuaEngine);
|
|
before = atomic_load(&errorCount);
|
|
atomic_store(&scriptDone, false);
|
|
// Invalid SQL should surface as a script-level error (the native fails).
|
|
calogContextEval(ctx,
|
|
"local db = dbOpen('sqlite', ':memory:')\n"
|
|
"dbQuery(db, 'select * from nonexistent_table')\n"
|
|
"done()");
|
|
pumpUntilDone();
|
|
|
|
CHECK(atomic_load(&errorCount) == before + 1, "a bad query raised a script-level error");
|
|
|
|
calogContextClose(ctx);
|
|
}
|
|
|
|
|
|
static void testDbRoundtrip(void) {
|
|
CalogContextT *ctx;
|
|
|
|
ctx = calogContextOpen(calog, &calogLuaEngine);
|
|
atomic_store(&scriptDone, false);
|
|
atomic_store(&reportedCount, -1);
|
|
calogContextEval(ctx,
|
|
"local db = dbOpen('sqlite', ':memory:')\n"
|
|
"dbExec(db, 'create table t (id integer, name text)')\n"
|
|
"dbExec(db, 'insert into t values (?, ?)', 1, 'alice')\n"
|
|
"dbExec(db, 'insert into t values (?, ?)', 2, 'bob')\n"
|
|
"local n = dbExec(db, 'update t set name = ? where id = ?', 'bobby', 2)\n"
|
|
"local rows = dbQuery(db, 'select id, name from t order by id')\n"
|
|
"report(#rows, rows[2].id, rows[2].name, n)\n"
|
|
"dbClose(db)\n"
|
|
"done()");
|
|
pumpUntilDone();
|
|
|
|
CHECK(atomic_load(&reportedCount) == 2, "dbQuery returned both rows");
|
|
CHECK(atomic_load(&reportedId) == 2, "the second row's integer column read back correctly");
|
|
CHECK(strcmp(reportedName, "bobby") == 0, "a bound-param update took effect and the text column read back");
|
|
CHECK(atomic_load(&execAffected) == 1, "dbExec reported the correct rows-affected count");
|
|
|
|
calogContextClose(ctx);
|
|
}
|
|
|
|
|
|
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, "reportBlob", nativeReportBlob, NULL);
|
|
calogRegister(calog, "done", nativeDone, NULL);
|
|
if (calogDbRegister(calog) != calogOkE) {
|
|
printf("calogDbRegister failed\n");
|
|
return 1;
|
|
}
|
|
|
|
testDbRoundtrip();
|
|
testDbBinary();
|
|
testDbError();
|
|
|
|
calogDestroy(calog);
|
|
calogDbShutdown();
|
|
|
|
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
|
|
fflush(stdout);
|
|
if (testsFailed != 0) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|