calog/tests/testNet.c

367 lines
12 KiB
C

// testNet.c -- exercises the calog network library over loopback. Each test runs two
// concurrent scripts on separate context threads: a server (listen/bind, then blocking
// accept/recv) and a client (connect/send). A serverReady signal gates the client so there
// is no bind/connect race. Covers TCP (stream round-trip) and UDP (datagram round-trip,
// including the sender address reported by udpRecvFrom).
#define _POSIX_C_SOURCE 200809L
#include "calog.h"
#include "calogNet.h"
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define PUMP_LIMIT 8000
#define TCP_PORT 47811
#define UDP_PORT 47812
#define ENET_PORT 47813
static CalogT *calog = NULL;
static _Atomic bool serverListening = false;
static _Atomic int32_t finishedCount = 0;
static _Atomic int32_t errorCount = 0;
static char serverGot[64] = { 0 };
static _Atomic int32_t serverGotLen = -1;
static char clientGot[64] = { 0 };
static _Atomic int32_t clientGotLen = -1;
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 void copyReport(CalogValueT *arg, char *dest, _Atomic int32_t *lenOut);
static int32_t nativeFinished(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportClient(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeReportServer(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeServerReady(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void onError(uint64_t contextId, const char *message, void *userData);
static void pumpUntilFinished(void);
static void pumpUntilListening(void);
static void resetState(void);
static void testEnet(void);
static void testEnetSafety(void);
static void testTcp(void);
static void testUdp(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 void copyReport(CalogValueT *arg, char *dest, _Atomic int32_t *lenOut) {
size_t length;
length = (size_t)arg->as.s.length;
if (length >= 64) {
length = 63;
}
memcpy(dest, arg->as.s.bytes, length);
dest[length] = '\0';
atomic_store(lenOut, (int32_t)length);
}
static int32_t nativeFinished(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_fetch_add(&finishedCount, 1);
calogValueNil(result);
return calogOkE;
}
static int32_t nativeReportClient(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "reportClient expects one string");
}
copyReport(&args[0], clientGot, &clientGotLen);
return calogOkE;
}
static int32_t nativeReportServer(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
calogValueNil(result);
if (argCount != 1 || args[0].type != calogStringE) {
return calogFail(result, calogErrArgE, "reportServer expects one string");
}
copyReport(&args[0], serverGot, &serverGotLen);
return calogOkE;
}
static int32_t nativeServerReady(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
atomic_store(&serverListening, true);
calogValueNil(result);
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 pumpUntilFinished(void) {
struct timespec ts = { 0, 500000 };
int i;
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&finishedCount) >= 2) {
return;
}
nanosleep(&ts, NULL);
}
}
static void pumpUntilListening(void) {
struct timespec ts = { 0, 500000 };
int i;
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&serverListening) || atomic_load(&errorCount) != 0) {
return;
}
nanosleep(&ts, NULL);
}
}
static void resetState(void) {
atomic_store(&serverListening, false);
atomic_store(&finishedCount, 0);
atomic_store(&serverGotLen, -1);
atomic_store(&clientGotLen, -1);
memset(serverGot, 0, sizeof(serverGot));
memset(clientGot, 0, sizeof(clientGot));
}
static void testEnet(void) {
CalogContextT *server;
CalogContextT *client;
char serverScript[768];
char clientScript[768];
resetState();
// Server: echo each reliable packet, stop when the client disconnects.
snprintf(serverScript, sizeof(serverScript),
"local host = enetHost(%d, 4)\n"
"serverReady()\n"
"local running = true\n"
"local count = 0\n"
"while running and count < 2000 do\n"
" local ev = enetService(host, 50)\n"
" if ev.type == 'receive' then\n"
" reportServer(ev.data)\n"
" enetSend(ev.peer, 0, 'pong:' .. ev.data, true)\n"
" elseif ev.type == 'disconnect' then\n"
" running = false\n"
" end\n"
" count = count + 1\n"
"end\n"
"enetClose(host)\n"
"finished()", ENET_PORT);
// Client: connect, send once, read the reply, then disconnect gracefully.
snprintf(clientScript, sizeof(clientScript),
"local host = enetHost(0, 1)\n"
"local peer = enetConnect(host, '127.0.0.1', %d, 1)\n"
"local finishedClient = false\n"
"local count = 0\n"
"while not finishedClient and count < 2000 do\n"
" local ev = enetService(host, 50)\n"
" if ev.type == 'connect' then\n"
" enetSend(peer, 0, 'hi', true)\n"
" elseif ev.type == 'receive' then\n"
" reportClient(ev.data)\n"
" enetDisconnect(peer)\n"
" elseif ev.type == 'disconnect' then\n"
" finishedClient = true\n"
" end\n"
" count = count + 1\n"
"end\n"
"enetClose(host)\n"
"finished()", ENET_PORT);
server = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(server, serverScript);
pumpUntilListening();
client = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(client, clientScript);
pumpUntilFinished();
CHECK(strcmp(serverGot, "hi") == 0, "ENet server received the reliable packet");
CHECK(strcmp(clientGot, "pong:hi") == 0, "ENet client received the reliable reply over its peer");
CHECK(atomic_load(&errorCount) == 0, "no errors during the ENet round-trip");
calogContextClose(server);
calogContextClose(client);
}
static void testEnetSafety(void) {
CalogContextT *ctx;
struct timespec ts = { 0, 500000 };
int32_t before;
int i;
resetState();
before = atomic_load(&errorCount);
ctx = calogContextOpen(calog, &calogLuaEngine);
// Using a peer handle after its host is closed must raise a clean script error, NOT a
// use-after-free (enetClose drops the host's peer handles). ASan would flag any UAF.
calogContextEval(ctx,
"local h = enetHost(0, 1)\n"
"local p = enetConnect(h, '127.0.0.1', 59999, 1)\n"
"enetClose(h)\n"
"enetSend(p, 0, 'x', true)\n"
"done()");
for (i = 0; i < PUMP_LIMIT; i++) {
calogPump(calog);
if (atomic_load(&errorCount) > before) {
break;
}
nanosleep(&ts, NULL);
}
CHECK(atomic_load(&errorCount) > before, "using an ENet peer handle after enetClose is a clean error, not a use-after-free");
calogContextClose(ctx);
}
static void testTcp(void) {
CalogContextT *server;
CalogContextT *client;
char serverScript[512];
char clientScript[512];
resetState();
snprintf(serverScript, sizeof(serverScript),
"local l = tcpListen(%d)\n"
"serverReady()\n"
"local c = tcpAccept(l)\n"
"local d = tcpRecv(c, 100)\n"
"reportServer(d)\n"
"tcpSend(c, 'pong:' .. d)\n"
"tcpClose(c)\n"
"tcpClose(l)\n"
"finished()", TCP_PORT);
snprintf(clientScript, sizeof(clientScript),
"local c = tcpConnect('127.0.0.1', %d)\n"
"tcpSend(c, 'ping')\n"
"local r = tcpRecv(c, 100)\n"
"reportClient(r)\n"
"tcpClose(c)\n"
"finished()", TCP_PORT);
server = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(server, serverScript);
pumpUntilListening();
client = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(client, clientScript);
pumpUntilFinished();
CHECK(strcmp(serverGot, "ping") == 0, "TCP server received the client's stream payload");
CHECK(strcmp(clientGot, "pong:ping") == 0, "TCP client received the server's reply");
CHECK(atomic_load(&errorCount) == 0, "no errors during the TCP round-trip");
calogContextClose(server);
calogContextClose(client);
}
static void testUdp(void) {
CalogContextT *server;
CalogContextT *client;
char serverScript[512];
char clientScript[512];
resetState();
// The server replies to the address udpRecvFrom reports, so the client need not bind a
// known port -- this also checks the {data, host, port} map.
snprintf(serverScript, sizeof(serverScript),
"local s = udpOpen(%d)\n"
"serverReady()\n"
"local m = udpRecvFrom(s, 100)\n"
"reportServer(m.data)\n"
"udpSendTo(s, m.host, m.port, 'pong:' .. m.data)\n"
"udpClose(s)\n"
"finished()", UDP_PORT);
snprintf(clientScript, sizeof(clientScript),
"local c = udpOpen(0)\n"
"udpSendTo(c, '127.0.0.1', %d, 'hi')\n"
"local m = udpRecvFrom(c, 100)\n"
"reportClient(m.data)\n"
"udpClose(c)\n"
"finished()", UDP_PORT);
server = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(server, serverScript);
pumpUntilListening();
client = calogContextOpen(calog, &calogLuaEngine);
calogContextEval(client, clientScript);
pumpUntilFinished();
CHECK(strcmp(serverGot, "hi") == 0, "UDP server received the datagram");
CHECK(strcmp(clientGot, "pong:hi") == 0, "UDP client received the reply routed to its reported address");
CHECK(atomic_load(&errorCount) == 0, "no errors during the UDP round-trip");
calogContextClose(server);
calogContextClose(client);
}
int main(void) {
calog = calogCreate();
if (calog == NULL) {
printf("calog create failed\n");
return 1;
}
calogSetErrorHandler(calog, onError, NULL);
calogRegister(calog, "serverReady", nativeServerReady, NULL);
calogRegister(calog, "reportServer", nativeReportServer, NULL);
calogRegister(calog, "reportClient", nativeReportClient, NULL);
calogRegister(calog, "finished", nativeFinished, NULL);
if (calogNetRegister(calog) != calogOkE) {
printf("calogNetRegister failed\n");
return 1;
}
testTcp();
testUdp();
testEnet();
testEnetSafety();
calogDestroy(calog);
calogNetShutdown();
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
fflush(stdout);
if (testsFailed != 0) {
return 1;
}
return 0;
}