// testUtil.c -- exercises the utility libraries (calogRegex, calogCsv, calogProc) from a Lua // context: regex match/search/replace/split with flags and groups, RFC 4180 CSV round-trips // including quoting and embedded NULs, and subprocess run capturing stdout/stderr/exit + feeding // stdin. A bad regex and a bad CSV delimiter are reported as errors the script can catch. #define _POSIX_C_SOURCE 200809L #include "calog.h" #include "calogCsv.h" #include "calogProc.h" #include "calogRegex.h" #include #include #include #include #define PUMP_LIMIT 4000 static CalogT *calog = NULL; static _Atomic int32_t checksRun = 0; static _Atomic int32_t checksFailed = 0; static _Atomic bool scriptDone = false; static int32_t nativeCheck(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData); static int32_t nativeDone(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 const char *SCRIPT = // --- regex --- "check(table.concat(regexMatch('c(a)(l)og', 'calog'), ',') == 'calog,a,l', 'regexMatch groups')\n" "check(regexMatch('zzz', 'calog') == nil, 'regexMatch no match is nil')\n" "check(regexMatch('A', 'abc', 'i') ~= nil, 'regexMatch i flag')\n" "check(regexReplace('a', 'banana', 'X', 'g') == 'bXnXnX', 'regexReplace global')\n" "check(regexReplace('a', 'banana', 'X') == 'bXnana', 'regexReplace first only')\n" "check(regexReplace('([a-z]+)@([a-z]+)', 'user@host', '${2}.${1}') == 'host.user', 'regexReplace group refs')\n" "check(table.concat(regexSplit(',', 'a,b,c'), '|') == 'a|b|c', 'regexSplit')\n" "local s = regexSearch('([0-9]+)', 'abc123def')\n" "check(s.match == '123' and s.start == 3 and s['end'] == 6, 'regexSearch offsets')\n" "check(s.groups[1] == '123', 'regexSearch capture group')\n" "check(not pcall(function() regexMatch('(', 'x') end), 'bad pattern is an error')\n" // --- csv --- "local rows = csvParse('a,b,c\\n\"x,y\",2,\"has\"\"q\"\\n')\n" "check(#rows == 2, 'csvParse row count')\n" "check(rows[1][3] == 'c', 'csvParse plain cell')\n" "check(rows[2][1] == 'x,y', 'csvParse quoted cell with delimiter')\n" "check(rows[2][3] == 'has\"q', 'csvParse escaped quote')\n" "local nul = csvParse('a\\0b,c')\n" "check(#nul[1][1] == 3, 'csvParse cell is binary-safe')\n" "check(csvFormat({{'has,comma', 'x'}}) == '\"has,comma\",x', 'csvFormat quotes a delimiter')\n" "check(csvFormat({{1, 2}, {3, 4}}) == '1,2\\r\\n3,4', 'csvFormat ints joined by CRLF')\n" "check(not pcall(function() csvParse('a,b', ',,') end), 'bad delimiter is an error')\n" // --- subprocess --- "local r = procRun({'printf', 'hi there'})\n" "check(r.exit == 0 and r.stdout == 'hi there', 'procRun captures stdout')\n" "local r2 = procRun({'cat'}, {stdin = 'feed me'})\n" "check(r2.stdout == 'feed me', 'procRun feeds stdin')\n" "local r3 = procRun({'sh', '-c', 'exit 3'})\n" "check(r3.exit == 3, 'procRun reports the exit code')\n" "local r4 = procRun({'sh', '-c', 'echo out; echo err 1>&2'})\n" "check(r4.exit == 0 and string.find(r4.stdout, 'out') ~= nil and string.find(r4.stderr, 'err') ~= nil, 'procRun splits stdout and stderr')\n" "done()\n"; static int32_t nativeCheck(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) { (void)userData; calogValueNil(result); atomic_fetch_add(&checksRun, 1); if (argCount < 1 || args[0].type != calogBoolE) { atomic_fetch_add(&checksFailed, 1); printf("FAIL check: expected a boolean\n"); return calogOkE; } if (!args[0].as.b) { atomic_fetch_add(&checksFailed, 1); printf("FAIL %s\n", argCount >= 2 && args[1].type == calogStringE ? args[1].as.s.bytes : "(unnamed)"); } return calogOkE; } 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 void onError(uint64_t contextId, const char *message, void *userData) { (void)contextId; (void)userData; printf("FAIL script error: %s\n", message); atomic_fetch_add(&checksFailed, 1); atomic_store(&scriptDone, true); } static void pumpUntilDone(void) { struct timespec ts = { 0, 500000 }; int32_t i; for (i = 0; i < PUMP_LIMIT; i++) { calogPump(calog); if (atomic_load(&scriptDone)) { calogPump(calog); return; } nanosleep(&ts, NULL); } } int main(void) { CalogContextT *ctx; calog = calogCreate(); if (calog == NULL) { printf("calog create failed\n"); return 1; } calogSetErrorHandler(calog, onError, NULL); if (calogRegexRegister(calog) != calogOkE || calogCsvRegister(calog) != calogOkE || calogProcRegister(calog) != calogOkE) { printf("utility register failed\n"); return 1; } calogRegisterInline(calog, "check", nativeCheck, NULL); calogRegisterInline(calog, "done", nativeDone, NULL); ctx = calogContextOpen(calog, &calogLuaEngine); if (ctx == NULL) { printf("context open failed\n"); return 1; } calogContextEval(ctx, SCRIPT); pumpUntilDone(); calogDestroy(calog); printf("\n%d checks, %d failed\n", atomic_load(&checksRun), atomic_load(&checksFailed)); fflush(stdout); return atomic_load(&checksFailed) == 0 ? 0 : 1; }