// 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 #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 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; }