calog/tests/testXml.c

166 lines
6.7 KiB
C

// testXml.c -- exercises the XML library: parse tags/attributes/nested + mixed content, entity
// decode and re-escape, self-closing elements, round-trip, and error handling, driven from Lua.
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
#include "calogXml.h"
#include <stdatomic.h>
#include <stdio.h>
#include <time.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define PUMP_LIMIT 4000
#define RESULT_SLOTS 16
static CalogT *calog = NULL;
static _Atomic int64_t results[RESULT_SLOTS];
static _Atomic int32_t doneCount = 0;
static _Atomic int32_t errorCount = 0;
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static int64_t asInt(const CalogValueT *value);
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 void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilDone(int32_t target);
static int64_t asInt(const CalogValueT *value) {
if (value->type == calogIntE) {
return value->as.i;
}
return (int64_t)value->as.r;
}
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_fetch_add(&doneCount, 1);
calogValueNil(result);
return calogOkE;
}
static int32_t nativeReport(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
int64_t tag;
(void)userData;
calogValueNil(result);
if (argCount != 2 || (args[0].type != calogIntE && args[0].type != calogRealE)) {
return calogFail(result, calogErrArgE, "report expects (tag, value)");
}
tag = asInt(&args[0]);
if (tag < 0 || tag >= RESULT_SLOTS) {
return calogFail(result, calogErrArgE, "report: tag out of range");
}
atomic_store(&results[tag], asInt(&args[1]));
return calogOkE;
}
static void onError(uint64_t contextId, const char *message, void *userData) {
(void)contextId;
(void)userData;
fprintf(stderr, " [script error] %s\n", (message != NULL) ? message : "(null)");
atomic_fetch_add(&errorCount, 1);
}
static void pumpUntilDone(int32_t target) {
struct timespec ts = { 0, 500000 };
int i;
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&doneCount) >= target) {
calogPump(calog);
return;
}
nanosleep(&ts, NULL);
}
}
int main(void) {
CalogContextT *ctx;
int32_t i;
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogSetErrorHandler(calog, onError, NULL);
calogRegister(calog, "report", nativeReport, NULL);
calogRegister(calog, "done", nativeDone, NULL);
calogXmlRegister(calog);
for (i = 0; i < RESULT_SLOTS; i++) {
atomic_store(&results[i], -1);
}
ctx = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(ctx,
"local e = xmlParse('<greeting>hello</greeting>')\n"
"report(1, (e.tag == 'greeting' and e.children[1] == 'hello') and 1 or 0)\n" // 1, tag + text
"local it = xmlParse('<item id=\"42\" name=\"widget\"/>')\n"
"report(2, (it.attr.id == '42' and it.attr.name == 'widget' and #it.children == 0) and 1 or 0)\n" // 1, attrs + self-close
"local r = xmlParse('<root><a>1</a><b>2</b></root>')\n"
"report(3, tonumber(r.children[1].children[1]) + tonumber(r.children[2].children[1]))\n" // 3, nested navigation
"local x = '<doc><p k=\"v\">text</p></doc>'\n"
"report(4, xmlStringify(xmlParse(x)) == x and 1 or 0)\n" // 1, lossless round-trip
"local t = xmlParse('<t>a &amp; b &lt; c</t>')\n"
"report(5, t.children[1] == 'a & b < c' and 1 or 0)\n" // 1, entities decode
"report(6, xmlStringify(t) == '<t>a &amp; b &lt; c</t>' and 1 or 0)\n" // 1, text re-escaped
"local ok = pcall(function() xmlParse('<unclosed>') end)\n"
"report(7, ok and 0 or 1)\n" // 1, malformed is catchable
"local p = xmlParse('<p>before<b>bold</b>after</p>')\n"
"report(8, (p.children[1]=='before' and p.children[2].tag=='b' and p.children[2].children[1]=='bold' and p.children[3]=='after') and 1 or 0)\n" // 1, mixed content
"local a = xmlParse('<a t=\"x &amp; y\"/>')\n"
"report(9, a.attr.t == 'x & y' and 1 or 0)\n" // 1, attribute entity decode
"report(10, xmlStringify(a) == '<a t=\"x &amp; y\"/>' and 1 or 0)\n" // 1, attribute re-escaped
"local w = xmlParse('<a t=\"x&#9;y&#10;z&#13;w\">p&#13;q</a>')\n"
"local w2 = xmlParse(xmlStringify(w))\n"
"report(11, w2.attr.t == 'x\\ty\\nz\\rw' and 1 or 0)\n" // 1, TAB/LF/CR in attr survive round-trip
"report(12, w2.children[1] == 'p\\rq' and 1 or 0)\n" // 1, CR in text survives round-trip
"done()");
pumpUntilDone(1);
CHECK(atomic_load(&results[1]) == 1, "parse a tag with a text child");
CHECK(atomic_load(&results[2]) == 1, "parse attributes on a self-closing element");
CHECK(atomic_load(&results[3]) == 3, "navigate nested elements");
CHECK(atomic_load(&results[4]) == 1, "stringify + parse is lossless for compact XML");
CHECK(atomic_load(&results[5]) == 1, "predefined entities decode to text");
CHECK(atomic_load(&results[6]) == 1, "text is re-escaped on stringify");
CHECK(atomic_load(&results[7]) == 1, "malformed XML raises a catchable error");
CHECK(atomic_load(&results[8]) == 1, "mixed content preserves text and element order");
CHECK(atomic_load(&results[9]) == 1, "attribute entities decode");
CHECK(atomic_load(&results[10]) == 1, "attribute values are re-escaped on stringify");
CHECK(atomic_load(&results[11]) == 1, "TAB/LF/CR in an attribute survive a round-trip");
CHECK(atomic_load(&results[12]) == 1, "CR in text survives a round-trip");
CHECK(atomic_load(&errorCount) == 0, "no uncaught errors");
calogDestroy(calog);
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);
if (testsFailed != 0) {
return 1;
}
return 0;
}