Files
2026-09-01 13:17:19 +09:00

473 lines
14 KiB
C

/*
* the device types that had no test of their own: pipe, thread, process and
* pty, plus the dns client.
*
* these came out of bin/t01.c and bin/t05.c, demos that exercised the library by
* printing what happened and leaving a human to judge it. everything else it
* covered - sockets, tls, the http tasks - has a test of its own by now. these
* four did not, so this is what was actually worth keeping from it.
*
* a demo can sleep and loop forever; a test cannot. so nothing here is a
* translation of that code - it is the same API driven to a definite end, with
* a deadline on every wait so a hang is a failure rather than a stuck suite.
*/
#include <hio-pipe.h>
#include <hio-thr.h>
#include <hio-pro.h>
#include <hio-pty.h>
#include <hio-dns.h>
#include <hio-prv.h>
#include "tap.h"
#include <string.h>
#include <unistd.h>
#define PAYLOAD "device-payload"
static hio_t* g_hio = HIO_NULL;
static int g_timeout;
static hio_tmridx_t g_tmr = HIO_TMRIDX_INVALID;
static void quiet_logging (hio_t* hio)
{
hio_bitmask_t mask = HIO_LOG_FATAL | HIO_LOG_ALL_TYPES;
hio_setoption (hio, HIO_LOG_MASK, &mask);
}
static void on_deadline (hio_t* hio, const hio_ntime_t* now, hio_tmrjob_t* job)
{
g_timeout = 1;
}
/* run the loop until the flag goes true or the deadline passes. every wait in
* this file goes through here, so nothing can hang the suite. */
static void run_until (int* flag, int secs)
{
hio_tmrjob_t j;
g_timeout = 0;
HIO_MEMSET (&j, 0, HIO_SIZEOF(j));
hio_gettime (g_hio, &j.when);
j.when.sec += secs;
j.handler = on_deadline;
j.idxptr = &g_tmr;
g_tmr = hio_instmrjob(g_hio, &j);
while (!*flag && !g_timeout)
{
if (hio_exec(g_hio) <= -1) break;
}
if (g_tmr != HIO_TMRIDX_INVALID) { hio_deltmrjob (g_hio, g_tmr); g_tmr = HIO_TMRIDX_INVALID; }
}
/* ------------------------------------------------------------------ */
/* pipe */
/* a pipe device is a write end and a read end of one pipe, presented as a
* single device. what goes in comes back out to on_read(). */
static hio_dev_pipe_t* g_pipe;
static int g_pipe_read, g_pipe_wrote, g_pipe_closed_ends;
static hio_bch_t g_pipe_buf[64];
static int g_pipe_len;
static int pipe_on_read (hio_dev_pipe_t* dev, const void* data, hio_iolen_t dlen)
{
if (dlen <= 0) return 0;
if (dlen > (hio_iolen_t)HIO_SIZEOF(g_pipe_buf) - 1) dlen = HIO_SIZEOF(g_pipe_buf) - 1;
HIO_MEMCPY (g_pipe_buf, data, dlen);
g_pipe_buf[dlen] = '\0';
g_pipe_len = (int)dlen;
g_pipe_read = 1;
return 0;
}
static int pipe_on_write (hio_dev_pipe_t* dev, hio_iolen_t wrlen, void* wrctx)
{
g_pipe_wrote = 1;
return 0;
}
static void pipe_on_close (hio_dev_pipe_t* dev, hio_dev_pipe_sid_t sid)
{
/* fires once per end and once for the master */
g_pipe_closed_ends++;
if (sid == HIO_DEV_PIPE_MASTER) g_pipe = HIO_NULL;
}
static void test_pipe (void)
{
hio_dev_pipe_make_t mi;
g_pipe_read = g_pipe_wrote = g_pipe_closed_ends = g_pipe_len = 0;
HIO_MEMSET (&mi, 0, HIO_SIZEOF(mi));
mi.on_read = pipe_on_read;
mi.on_write = pipe_on_write;
mi.on_close = pipe_on_close;
g_pipe = hio_dev_pipe_make(g_hio, 0, &mi);
if (!g_pipe) { FAIL ("a pipe device can be made"); skip ("no pipe device", 3); return; }
PASS ("a pipe device can be made");
if (hio_dev_pipe_write(g_pipe, PAYLOAD, HIO_SIZEOF(PAYLOAD) - 1, HIO_NULL) <= -1)
{
FAIL ("what is written to a pipe comes back out of it");
FAIL ("and the write is acknowledged");
}
else
{
run_until (&g_pipe_read, 3);
OK (g_pipe_read && g_pipe_len == (int)HIO_SIZEOF(PAYLOAD) - 1 &&
HIO_MEMCMP(g_pipe_buf, PAYLOAD, HIO_SIZEOF(PAYLOAD) - 1) == 0,
"what is written to a pipe comes back out of it");
OK (g_pipe_wrote, "and the write is acknowledged");
}
if (g_pipe) hio_dev_pipe_halt (g_pipe);
hio_exec (g_hio);
hio_exec (g_hio);
/* a pipe is a master over two slaves, so tearing it down closes three
* things, not one - which is the part of the slave model worth pinning */
OK (g_pipe_closed_ends >= 2, "and closing it closes both ends");
}
/* ------------------------------------------------------------------ */
/* thread */
/* the worker runs in its own thread with a pipe to the loop at each side. it
* must not outlive what it is asked to do, so it echoes once and returns -
* writing nothing more and looping over nothing. */
static hio_dev_thr_t* g_thr;
static int g_thr_read, g_thr_closed;
static hio_bch_t g_thr_buf[64];
static int g_thr_len;
static void thr_worker (hio_t* hio, hio_dev_thr_iopair_t* iop, void* ctx)
{
hio_bch_t buf[64];
ssize_t n;
n = read(iop->rfd, buf, HIO_COUNTOF(buf));
if (n > 0)
{
ssize_t k = 0;
while (k < n)
{
ssize_t w = write(iop->wfd, &buf[k], n - k);
if (w <= 0) break;
k += w;
}
}
/* returning ends the thread, which closes its side and gives the loop the
* EOF that on_close() below is waiting for */
}
static int thr_on_read (hio_dev_thr_t* dev, const void* data, hio_iolen_t dlen)
{
if (dlen <= 0) return 0;
if (dlen > (hio_iolen_t)HIO_SIZEOF(g_thr_buf) - 1) dlen = HIO_SIZEOF(g_thr_buf) - 1;
HIO_MEMCPY (g_thr_buf, data, dlen);
g_thr_buf[dlen] = '\0';
g_thr_len = (int)dlen;
g_thr_read = 1;
return 0;
}
static int thr_on_write (hio_dev_thr_t* dev, hio_iolen_t wrlen, void* wrctx)
{
return 0;
}
static void thr_on_close (hio_dev_thr_t* dev, hio_dev_thr_sid_t sid)
{
if (sid == HIO_DEV_THR_MASTER) { g_thr_closed = 1; g_thr = HIO_NULL; }
}
static void test_thr (void)
{
hio_dev_thr_make_t mi;
g_thr_read = g_thr_closed = g_thr_len = 0;
HIO_MEMSET (&mi, 0, HIO_SIZEOF(mi));
mi.thr_func = thr_worker;
mi.thr_ctx = HIO_NULL;
mi.on_read = thr_on_read;
mi.on_write = thr_on_write;
mi.on_close = thr_on_close;
g_thr = hio_dev_thr_make(g_hio, 0, &mi);
if (!g_thr) { FAIL ("a thread device can be made"); skip ("no thread device", 2); return; }
PASS ("a thread device can be made");
if (hio_dev_thr_write(g_thr, PAYLOAD, HIO_SIZEOF(PAYLOAD) - 1, HIO_NULL) <= -1)
{
FAIL ("data crosses to the worker thread and back");
}
else
{
run_until (&g_thr_read, 5);
OK (g_thr_read && g_thr_len == (int)HIO_SIZEOF(PAYLOAD) - 1 &&
HIO_MEMCMP(g_thr_buf, PAYLOAD, HIO_SIZEOF(PAYLOAD) - 1) == 0,
"data crosses to the worker thread and back");
}
/* the worker has returned by now, so the device should come down on its
* own once told to. waiting for on_close rather than assuming it is what
* catches a thread that never gets joined. */
if (g_thr) hio_dev_thr_halt (g_thr);
run_until (&g_thr_closed, 5);
OK (g_thr_closed, "and the device closes once the worker returns");
hio_exec (g_hio);
}
/* ------------------------------------------------------------------ */
/* process */
static hio_dev_pro_t* g_pro;
static int g_pro_read, g_pro_closed_slaves;
static hio_bch_t g_pro_buf[128];
static int g_pro_len;
static int pro_on_read (hio_dev_pro_t* dev, hio_dev_pro_sid_t sid, const void* data, hio_iolen_t dlen)
{
if (sid != HIO_DEV_PRO_OUT || dlen <= 0) return 0;
if (dlen > (hio_iolen_t)HIO_SIZEOF(g_pro_buf) - 1) dlen = HIO_SIZEOF(g_pro_buf) - 1;
HIO_MEMCPY (g_pro_buf, data, dlen);
g_pro_buf[dlen] = '\0';
g_pro_len = (int)dlen;
g_pro_read = 1;
return 0;
}
static int pro_on_write (hio_dev_pro_t* dev, hio_iolen_t wrlen, void* wrctx)
{
return 0;
}
static void pro_on_close (hio_dev_pro_t* dev, hio_dev_pro_sid_t sid)
{
if (sid == HIO_DEV_PRO_MASTER) g_pro = HIO_NULL;
else g_pro_closed_slaves++;
}
static void test_pro (void)
{
hio_dev_pro_make_t mi;
g_pro_read = g_pro_closed_slaves = g_pro_len = 0;
HIO_MEMSET (&mi, 0, HIO_SIZEOF(mi));
/* 'cat' echoes stdin to stdout and exits on EOF - the smallest child that
* exercises writing to one slave and reading from another */
mi.flags = HIO_DEV_PRO_READOUT | HIO_DEV_PRO_WRITEIN | HIO_DEV_PRO_ERRTONUL | HIO_DEV_PRO_SHELL;
mi.cmd = "cat";
mi.on_read = pro_on_read;
mi.on_write = pro_on_write;
mi.on_close = pro_on_close;
g_pro = hio_dev_pro_make(g_hio, 0, &mi);
if (!g_pro) { FAIL ("a process device can be made"); skip ("no process device", 2); return; }
PASS ("a process device can be made");
if (hio_dev_pro_write(g_pro, PAYLOAD, HIO_SIZEOF(PAYLOAD) - 1, HIO_NULL) <= -1)
{
FAIL ("what is written to the child comes back from its output");
FAIL ("and closing the input ends the child");
}
else
{
run_until (&g_pro_read, 5);
OK (g_pro_read && g_pro_len == (int)HIO_SIZEOF(PAYLOAD) - 1 &&
HIO_MEMCMP(g_pro_buf, PAYLOAD, HIO_SIZEOF(PAYLOAD) - 1) == 0,
"what is written to the child comes back from its output");
/* a zero-length write closes the child's stdin, which is what makes
* cat exit - so this also checks the device notices a child leaving */
g_pro_closed_slaves = 0;
hio_dev_pro_write (g_pro, HIO_NULL, 0, HIO_NULL);
run_until (&g_pro_closed_slaves, 5);
OK (g_pro_closed_slaves > 0, "and closing the input ends the child");
}
if (g_pro) hio_dev_pro_halt (g_pro);
hio_exec (g_hio);
hio_exec (g_hio);
}
/* ------------------------------------------------------------------ */
/* pty */
/* a pty device runs a child on a pseudo-terminal rather than on pipes. the
* difference that matters to a test is that a terminal echoes: what is written
* to it comes back without the child doing anything, which is why this reads
* back its own input rather than a command's output. */
static hio_dev_pty_t* g_pty;
static int g_pty_read, g_pty_closed;
static hio_bch_t g_pty_buf[256];
static int g_pty_len;
static int pty_on_read (hio_dev_pty_t* dev, const void* data, hio_iolen_t dlen)
{
if (dlen <= 0) return 0;
if (g_pty_len + (int)dlen < (int)HIO_SIZEOF(g_pty_buf) - 1)
{
HIO_MEMCPY (&g_pty_buf[g_pty_len], data, dlen);
g_pty_len += (int)dlen;
g_pty_buf[g_pty_len] = '\0';
}
g_pty_read = 1;
return 0;
}
static int pty_on_write (hio_dev_pty_t* dev, hio_iolen_t wrlen, void* wrctx)
{
return 0;
}
static void pty_on_close (hio_dev_pty_t* dev)
{
g_pty_closed = 1;
g_pty = HIO_NULL;
}
static void test_pty (void)
{
hio_dev_pty_make_t mi;
g_pty_read = g_pty_closed = g_pty_len = 0;
g_pty_buf[0] = '\0';
HIO_MEMSET (&mi, 0, HIO_SIZEOF(mi));
/* 'cat' on a terminal: the line is echoed by the tty discipline as it is
* written, and echoed again by cat once the newline completes it */
mi.flags = HIO_DEV_PTY_SHELL;
mi.cmd = "cat";
mi.on_read = pty_on_read;
mi.on_write = pty_on_write;
mi.on_close = pty_on_close;
g_pty = hio_dev_pty_make(g_hio, 0, &mi);
if (!g_pty)
{
/* no pty available - a container without /dev/pts, say. the system's
* answer, not a library fault. */
skip ("cannot allocate a pty", 2);
return;
}
PASS ("a pty device can be made");
if (hio_dev_pty_write(g_pty, PAYLOAD "\n", HIO_SIZEOF(PAYLOAD), HIO_NULL) <= -1)
{
FAIL ("and what is written to the terminal comes back from it");
}
else
{
run_until (&g_pty_read, 5);
OK (g_pty_read && strstr(g_pty_buf, PAYLOAD) != HIO_NULL,
"and what is written to the terminal comes back from it");
}
if (g_pty) hio_dev_pty_halt (g_pty);
hio_exec (g_hio);
hio_exec (g_hio);
}
/* ------------------------------------------------------------------ */
/* dns client */
/* no nameserver is assumed here, so what is tested is the half that needs
* none: that a lookup can be started, that an unanswerable one is reported
* through the callback rather than hanging or being forgotten, and that the
* service can be stopped with that request still outstanding.
*
* the last of those is the interesting one. there is no per-request cancel in
* the dnc api, so stopping the service is the only way to end a lookup, and
* anything built on it - the http proxy, when it grows name support - has to
* survive that happening while a request is in flight. */
static int g_dns_called;
static hio_errnum_t g_dns_status;
static void on_resolved (hio_svc_dnc_t* dnc, hio_dns_msg_t* reqmsg, hio_errnum_t status, const void* data, hio_oow_t len)
{
g_dns_called++;
g_dns_status = status;
}
static void test_dnc (void)
{
hio_svc_dnc_t* dnc;
hio_skad_t servaddr;
hio_ntime_t send_tmout, reply_tmout;
g_dns_called = 0;
g_dns_status = HIO_ENOERR;
/* a loopback port with nothing on it. the request goes out and is never
* answered, which is exactly the path worth covering. */
if (hio_bcstrtoskad(g_hio, "127.0.0.1:9", &servaddr) <= -1) { skip ("bad address", 4); return; }
HIO_INIT_NTIME (&send_tmout, 1, 0);
HIO_INIT_NTIME (&reply_tmout, 1, 0);
dnc = hio_svc_dnc_start(g_hio, &servaddr, HIO_NULL, &send_tmout, &reply_tmout, 1);
if (!dnc) { FAIL ("a dns client can be started"); skip ("no dns client", 3); return; }
PASS ("a dns client can be started");
OK (hio_svc_dnc_resolve(dnc, "test.invalid", HIO_DNS_RRT_A,
HIO_SVC_DNC_RESOLVE_FLAG_BRIEF, on_resolved, 0) != HIO_NULL,
"and a lookup can be started against it");
/* one try, one second - so this must be reported, not left pending */
run_until (&g_dns_called, 6);
/* HIO_ETMOUT specifically, not merely "some failure". a udp send to a
* closed port could plausibly come back as a refusal instead, and the two
* arrive by different paths in the client - naming the one expected is
* what keeps this pinned to the timeout path. */
OK (g_dns_called == 1 && g_dns_status == HIO_ETMOUT,
"and an unanswered lookup times out rather than being forgotten");
/* and stopping the service with a request outstanding must not leave the
* loop holding anything - which the leak checker is what really proves */
g_dns_called = 0;
hio_svc_dnc_resolve (dnc, "test2.invalid", HIO_DNS_RRT_A,
HIO_SVC_DNC_RESOLVE_FLAG_BRIEF, on_resolved, 0);
hio_svc_dnc_stop (dnc);
hio_exec (g_hio);
PASS ("and the service can be stopped with a lookup still in flight");
}
/* ------------------------------------------------------------------ */
int main (void)
{
hio_errinf_t errinf;
g_hio = hio_open(HIO_NULL, 0, HIO_NULL, HIO_FEATURE_ALL, 16, &errinf);
if (!g_hio)
{
no_plan ();
bail_out ("unable to open hio");
return -1;
}
quiet_logging (g_hio);
no_plan ();
test_pipe ();
test_thr ();
test_pro ();
test_pty ();
test_dnc ();
hio_close (g_hio);
return exit_status();
}