// testDbPg.c -- live Postgres backend test. Given a libpq conninfo string on argv[1], // drives a create/insert/update/query round-trip from a Lua script through the DB library // and checks the typed values (int, text, double, bool) that come back. #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 reportedBlob[16] = { 0 }; static _Atomic int32_t reportedBlobLen = -1; static double reportedScore = 0.0; static _Atomic bool reportedOk = true; 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 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 != 6 || args[0].type != calogIntE || args[1].type != calogIntE || args[2].type != calogStringE || args[3].type != calogIntE || args[4].type != calogRealE || args[5].type != calogBoolE) { return calogFail(result, calogErrArgE, "report expects (count, id, name, affected, score, ok)"); } 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); reportedScore = args[4].as.r; atomic_store(&reportedOk, args[5].as.b); 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(reportedBlob)) { length = sizeof(reportedBlob); } memcpy(reportedBlob, args[0].as.s.bytes, length); atomic_store(&reportedBlobLen, (int32_t)length); return calogOkE; } static void onError(uint64_t contextId, const char *message, void *userData) { (void)contextId; (void)userData; (void)message; 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); } } int main(int argc, char **argv) { CalogContextT *ctx; char script[2048]; const char *conninfo; if (argc < 2) { printf("usage: testDbPg \n"); return 2; } conninfo = argv[1]; 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; } snprintf(script, sizeof(script), "local db = dbOpen('postgres', '%s')\n" "dbExec(db, 'drop table if exists ct')\n" "dbExec(db, 'create table ct (id int, name text, score double precision, ok boolean)')\n" "dbExec(db, 'insert into ct values ($1, $2, $3, $4)', 1, 'alice', 3.5, true)\n" "dbExec(db, 'insert into ct values ($1, $2, $3, $4)', 2, 'bob', 7.25, false)\n" "local n = dbExec(db, 'update ct set name = $1 where id = $2', 'bobby', 2)\n" "local rows = dbQuery(db, 'select id, name, score, ok from ct order by id')\n" "report(#rows, rows[2].id, rows[2].name, n, rows[2].score, rows[2].ok)\n" "dbExec(db, 'drop table if exists bb')\n" "dbExec(db, 'create table bb (v bytea)')\n" "dbExec(db, 'insert into bb values ($1)', 'x\\0y')\n" "local br = dbQuery(db, 'select v from bb')\n" "reportBlob(br[1].v)\n" "dbClose(db)\n" "done()", conninfo); ctx = calogContextOpen(calog, &calogLuaEngine); calogContextEval(ctx, script); pumpUntilDone(); CHECK(atomic_load(&reportedCount) == 2, "Postgres dbQuery returned both rows"); CHECK(atomic_load(&reportedId) == 2, "the integer column read back typed"); CHECK(strcmp(reportedName, "bobby") == 0, "a bound-param update took effect; text read back"); CHECK(atomic_load(&execAffected) == 1, "dbExec reported the correct rows-affected"); CHECK(reportedScore == 7.25, "the double precision column read back as a real"); CHECK(atomic_load(&reportedOk) == false, "the boolean column read back as a bool"); CHECK(atomic_load(&reportedBlobLen) == 3, "a bytea param with an embedded NUL round-tripped at full length"); CHECK(reportedBlob[0] == 'x' && reportedBlob[1] == '\0' && reportedBlob[2] == 'y', "the embedded NUL survived the Postgres bound-param round-trip"); calogContextClose(ctx); calogDestroy(calog); calogDbShutdown(); printf("\n%d checks, %d failed\n", testsRun, testsFailed); fflush(stdout); return (testsFailed != 0) ? 1 : 0; }