calog/tests/testUtil.c

146 lines
5.6 KiB
C

// 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 <stdatomic.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#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;
}