X-Git-Url: https://git.distorted.org.uk/~mdw/mLib/blobdiff_plain/e63124bc579bfd97cfe2f620ddd84df9f20477d8..13ee740655965453b2f3fcd7093d2d8b13839903:/test/tvec-remote.c diff --git a/test/tvec-remote.c b/test/tvec-remote.c index e03297e..c167618 100644 --- a/test/tvec-remote.c +++ b/test/tvec-remote.c @@ -28,270 +28,2319 @@ /*----- Header files ------------------------------------------------------*/ #include +#include #include #include #include #include #include -#include +#include #include #include #include "alloc.h" +#include "bench.h" #include "buf.h" +#include "compiler.h" +#include "fdflags.h" +#include "growbuf.h" +#include "lbuf.h" +#include "mdup.h" +#include "quis.h" #include "tvec.h" -/*----- Data structures ---------------------------------------------------*/ +/*----- Preliminaries -----------------------------------------------------*/ -struct tvec_remote { - int infd, outfd; - dbuf bin, bout; - unsigned f; -#define TVRF_BROKEN 1u -}; +/* The control macros I'm using below provoke `dangling-else' warnings from + * compilers. Suppress them. I generally don't care. + */ -struct tvec_remotectx { - struct tvec_remote r; - pid_t kid; -}; +#if GCC_VERSION_P(7, 1) +# pragma GCC diagnostic ignored "-Wdangling-else" +#elif GCC_VERSION_P(4, 2) +# pragma GCC diagnostic ignored "-Wparentheses" +#endif -struct remote_output { - struct tvec_output _o; - struct tvec_remote r; -}; +#if CLANG_VERSION_P(3, 1) +# pragma clang diagnostic ignored "-Wdangling-else" +#endif /*----- Basic I/O ---------------------------------------------------------*/ -static int PRINTF_LIKE(3, 4) - ioerr(struct tvec_state *tv, struct tvec_remote *r, const char *msg, ...) +/* --- @init_comms@ --- * + * + * Arguments: @struct tvec_remotecomms *rc@ = communication state + * + * Returns: --- + * + * Use: Initialize a communication state. This doesn't allocate any + * resurces: it just ensures that everything is set up so that + * subsequent operations -- in particular @release_comms@ -- + * behave sensibly. + */ + +static void init_comms(struct tvec_remotecomms *rc) +{ + rc->bin = 0; rc->binsz = 0; DBCREATE(&rc->bout); + rc->infd = rc->outfd = -1; rc->f = 0; +} + +/* --- @close_comms@ --- * + * + * Arguments: @struct tvec_remotecomms *rc@ = communication state + * + * Returns: --- + * + * Use: Close the input and output descriptors. + * + * If the descriptors are already closed -- or were never opened + * -- then nothing happens. + */ + +static void close_comms(struct tvec_remotecomms *rc) +{ + if (rc->infd >= 0) { + if (rc->infd != rc->outfd) close(rc->infd); + rc->infd = -1; + } + if (rc->outfd >= 0) + { close(rc->outfd); rc->outfd = -1; } + rc->f |= TVRF_BROKEN; +} + +/* --- @release_comms@ --- * + * + * Arguments: @struct tvec_remotecomms *rc@ = communication state + * + * Returns: --- + * + * Use: Releases the resources -- most notably the input and output + * buffers -- held by the communication state. Also calls + * @close_comms@. + */ + +static void release_comms(struct tvec_remotecomms *rc) + { close_comms(rc); xfree(rc->bin); DBDESTROY(&rc->bout); } + +/* --- @setup_comms@ --- * + * + * Arguments: @struct tvec_remotecomms *rc@ = communication state + * @int infd, outfd@ = input and output file descriptors + * + * Returns: --- + * + * Use: Use the given descriptors for communication. + * + * Clears the private flags. + */ + +static void setup_comms(struct tvec_remotecomms *rc, int infd, int outfd) +{ + rc->infd = infd; rc->outfd = outfd; + rc->binoff = rc->binlen = 0; + rc->f &= ~0xffu; +} + +/* --- @ioerr@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_remotecomms *rc@ = communication state + * @const char *msg, ...@ = format string and arguments + * + * Returns: %$-1$%. + * + * Use: Reports the message as an error, closes communications and + * marks them as broken. + */ + +static PRINTF_LIKE(3, 4) + int ioerr(struct tvec_state *tv, struct tvec_remotecomms *rc, + const char *msg, ...) { va_list ap; va_start(ap, msg); - r->f |= TVRF_BROKEN; - tvec_write(tv, msg, &ap); + close_comms(rc); rc->f |= TVRF_BROKEN; + tvec_report_v(tv, TVLEV_ERR, msg, &ap); va_end(ap); return (-1); } -static int send_all(struct tvec_state *tv, struct tvec_remote *r, +/* --- @send_all@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_remotecomms *rc@ = communication state + * @const unsigned char *p@, @size_t sz@ = output buffer + * + * Returns: Zero on success, %$-1$% on error. + * + * Use: Send the output buffer over the communication state's output + * descriptor, even if it has to be written in multiple pieces. + */ + +static int send_all(struct tvec_state *tv, struct tvec_remotecomms *rc, const unsigned char *p, size_t sz) { ssize_t n; + int ret; while (sz) { - n = write(r->outfd, p, sz); + n = write(rc->outfd, p, sz); if (n > 0) { p += n; sz -= n; } - else - return (ioerr(tv, r, "failed to send: %s", - n ? strerror(errno) : "empty write")); + else { + ret = ioerr(tv, rc, "failed to send: %s", + n ? strerror(errno) : "empty write"); + goto end; + } } - return (0); + ret = 0; +end: + return (ret); } +/* --- @recv_all@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_remotecomms *rc@ = communication state + * @unsigned f@ = flags (@RCVF_...@) + * @unsigned char *p@, @size_t sz@ = input buffer + * @size_t min@ = minimum acceptable size to read + * @size_t *n_out@ = size read + * + * Returns: A @RECV_...@ code. + * + * Use: Receive data on the communication state's input descriptor to + * read at least @min@ bytes into the input buffer, even if it + * has to be done in multiple pieces. If more data is readily + * available, then up to @sz@ bytes will be read in total. + * + * If the descriptor immediately reports end-of-file, and + * @RCVF_ALLOWEOF@ is set in @f@, then return @RECV_EOF@. + * Otherwise, EOF is treated as an I/O error, resulting in a + * call to @ioerr@ and a return code of @RECV_FAIL@. If the + * read succeeded, then set @*n_out@ to the number of bytes read + * and return @RECV_OK@. + */ + #define RCVF_ALLOWEOF 1u -static int recv_all(struct tvec_state *tv, struct tvec_remote *r, - unsigned char *p, size_t sz, unsigned f) + +enum { + RECV_FAIL = -1, + RECV_OK = 0, + RECV_EOF = 1 +}; + +static int recv_all(struct tvec_state *tv, struct tvec_remotecomms *rc, + unsigned f, unsigned char *p, size_t sz, + size_t min, size_t *n_out) { + size_t tot = 0; ssize_t n; - unsigned ff = 0; -#define f_any 1u while (sz) { - n = read(r->infd, p, sz); - if (n > 0) - { p += n; sz -= n; ff |= f_any; } - else if (!n && (f&RCVF_ALLOWEOF) && !(ff&f_any)) - return (1); + n = read(rc->infd, p, sz); + if (n > 0) { + p += n; sz -= n; tot += n; + if (tot >= min) break; + } else if (!n && !tot && (f&RCVF_ALLOWEOF)) + { rc->f |= TVRF_BROKEN; return (RECV_EOF); } else - return (ioerr(tv, r, "failed to receive: %s", + return (ioerr(tv, rc, "failed to receive: %s", n ? strerror(errno) : "unexpected end-of-file")); } - return (0); + *n_out = tot; return (RECV_OK); #undef f_any } -int tvec_send(struct tvec_state *tv, struct tvec_reomte *r) +/* --- @buferr@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_remotecomms *rc@ = communication state + * + * Returns: %$-$%. + * + * Use: Report a problem preparing the output buffer. + */ + +static int buferr(struct tvec_state *tv, struct tvec_remotecomms *rc) + { return (ioerr(tv, rc, "failed to build output packet")); } + +/* --- @malformed@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_remotecomms *rc@ = communication state + * + * Returns: %$-$%. + * + * Use: Report an I/O error that the incoming packet is malformed. + */ + +static int malformed(struct tvec_state *tv, struct tvec_remotecomms *rc) + { return (ioerr(tv, rc, "received malformed packet")); } + +/* --- @remote_send@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_remotecomms *rc@ = communication state + * + * Returns: Zero on success, %$-1$% on error. + * + * Use: Send the accuulated contents of the output buffer @rc->bout@. + * + * The function arranges to convert @SIGPIPE@ into an error. + * + * If the output buffer is broken, report this as an I/O error. + */ + +#define SENDBUFSZ 4096 + +static int remote_send(struct tvec_state *tv, struct tvec_remotecomms *rc) +{ + void (*opipe)(int) = SIG_ERR; + int ret; + + /* Various preflight checks. */ + if (rc->f&TVRF_BROKEN) { ret = -1; goto end; } + if (DBBAD(&rc->bout)) { ret = buferr(tv, rc); goto end; } + + /* Arrange to trap broken-pipe errors. */ + opipe = signal(SIGPIPE, SIG_IGN); + if (opipe == SIG_ERR) { + ret = ioerr(tv, rc, "failed to ignore `SIGPIPE': %s", strerror(errno)); + goto end; + } + + /* Transmit the packet. */ + if (send_all(tv, rc, DBBASE(&rc->bout), DBLEN(&rc->bout))) + { ret = -1; goto end; } + + /* Done. Put things back the way we found them. */ + ret = 0; +end: + DBRESET(&rc->bout); + if (opipe != SIG_ERR) signal(SIGPIPE, opipe); + return (ret); +} + +/* --- @receive_buffered@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_remotecomms *rc@ = communication state + * @unsigned f@ = flags (@RCVF_...@) + * @size_t want@ = data block size required + * + * Returns: A @RECV_...@ code. + * + * Use: Reads a block of data from the input descriptor into the + * input buffer. + * + * This is the main machinery for manipulating the input buffer. + * The buffer has three regions: + * + * * from the buffer start to @rc->binoff@ is `consumed'; + * * from @rc->binoff@ to @rc->binlen@ is `available'; and + * * from @rc->binlen@ to @rc->binsz@ is `free'. + * + * Data is read into the start of the `free' region, and the + * `available' region is extended to include it. Data in the + * `consumed' region is periodically discarded by moving the + * data from the `available' region to the start of the buffer + * and decreasing @rc->binoff@ and @rc->binlen@. + * + * This function ensures that the `available' region contains at + * least @want@ bytes, by (a) extending the buffer, if + * necessary, so that @rc->binsz >= rc->binoff + want@, and (b) + * reading fresh data from the input descriptor to extend the + * `available' region. + * + * If absolutely no data is available, and @RCVF_ALLOWEOF@ is + * set in @f@, then return @RECV_EOF@. On I/O errors, including + * a short read or end-of-file if @RCVF_ALLOWEOF@ is clear, + * return @RECV_FAIL@. On success, return @RECV_OK@. The + * amount of data read is indicated by updating the input buffer + * variables as described above. + */ + +#define RECVBUFSZ 4096u + +static int receive_buffered(struct tvec_state *tv, + struct tvec_remotecomms *rc, + unsigned f, size_t want) { - kludge64 k; unsigned char lenbuf[8]; - const char *p; size_t sz; + size_t sz = 0; + int ret; - if (r->f&TVRF_BROKEN) return (-1); - if (BBAD(&r->bout.b)) - return (ioerr(tv, r, "failed to build output packet buffer"); + /* If we can supply the caller's requirement from the buffer then do + * that. + */ + if (rc->binlen - rc->binoff >= want) return (RECV_OK); - p = BBASE(r->bout.b); sz = BLEN(&r->bout.b); - ASSIGN64(k, sz); STORE64_L_(lenbuf, k); - if (send_all(tv, r, lenbuf, sizeof(lenbuf))) return (-1); - if (send_all(tv, r, p, sz)) return (-1); + /* If the buffer is too small then we must grow it. */ + GROWBUF_EXTEND(&arena_stdlib, rc->bin, rc->binsz, want, RECVBUFSZ, 1); - return (0); + /* Shunt the unused existing material to the start of the buffer. */ + memmove(rc->bin, rc->bin + rc->binoff, rc->binlen - rc->binoff); + rc->binlen -= rc->binoff; rc->binoff = 0; + + /* Satisfy the caller from the input stream, and try to fill up as much of + * the rest of the buffer as we can. + */ + ret = recv_all(tv, rc, rc->binlen ? 0 : f, + rc->bin + rc->binlen, rc->binsz - rc->binlen, + want - rc->binlen, &sz); + if (ret) return (ret); + + /* Note how much material we have and return. */ + rc->binlen += sz; return (RECV_OK); } -int tvec_recv(struct tvec_state *tv, struct tvec_reomte *r, buf *b_out) +/* --- @remote_recv@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @unsigned f@ = flags (@RCVF_...@) + * @buf *b_out@ = buffer to establish around the packet contents + * + * Returns: A @RECV_...@ code. + * + * Use: Receive a packet into the input buffer @rc->bin@ and + * establish @*b_out@ to read from it. + */ + +static int remote_recv(struct tvec_state *tv, struct tvec_remotecomms *rc, + unsigned f, buf *b_out) { - kludge64 k, szmax; unsigned char lenbuf[8]; - unsigned char *p; - size_t sz; - int rc; + kludge64 k, szmax; + size_t want; + int ret; - if (r->f&TVRF_BROKEN) return (-1); - ASSIGN64(k, (size_t)-1); - rc = recv_all(tv, r, lenbuf, sizeof(lenbuf), RCVF_ALLOWEOF); - if (rc) return (rc); - LOAD64_L_(k, lenbuf); + ASSIGN64(szmax, (size_t)-1); + + /* Preflight checks. */ + if (rc->f&TVRF_BROKEN) return (RECV_FAIL); + + /* See if we can read the next packet length from what we already have. */ + ret = receive_buffered(tv, rc, f, 8); if (ret) return (ret); + LOAD64_L_(k, rc->bin + rc->binoff); rc->binoff += 8; if (CMP64(k, >, szmax)) - return (ioerr(tv, r, "packet size 0x%08lx%08lx out of range", + return (ioerr(tv, rc, "packet size 0x%08lx%08lx out of range", (unsigned long)HI64(k), (unsigned long)LO64(k))); + want = GET64(size_t, k); - sz = GET64(size_t, k); buf_reset(&r->bin); p = buf_get(&r->bin.b, sz); - if (!p) return (ioerr(tv, r, "failed to allocate receive buffer")); - if (recv_all(tv, r, p, sz, 0)) return (-1); - buf_init(b_out, p, sz); return (0); + /* Read the next packet payload. */ + ret = receive_buffered(tv, rc, 0, want); if (ret) return (ret); + buf_init(b_out, rc->bin + rc->binoff, want); rc->binoff += want; + return (RECV_OK); } -/*----- Data formatting primitives ----------------------------------------*/ +/* --- @QUEUEPK_TAG@, @QUEUEPK@ --- * + * + * Arguments: @tag@ = control structure tag + * @struct tvec_state *tv@ = test-vector state + * @struct tvec_remotecomms *rc@ = communication state + * @unsigned fl@ = flags (@QF_...@) + * @unsigned pk@ = packet type + * + * Use: This is syntactically a statement head: the syntax is + * @QUEUEPK(tv, rc, f) body [else alt]@. The @body@ should + * write material to the output buffer @rc->bout@. The macro + * applies appropriate framing. If enough material has been + * collected, or if @QF_FORCE@ is set in @fl@, then + * @remote_send@ is invoked to transmit the buffered packets. + * If there is an error of any kind, then the @alt@ statement, + * if any, is executed. + */ + +#define QF_FORCE 1u +#define QUEUEPK_TAG(tag, tv, rc, fl, pk) \ + if ((rc)->f&TVRF_BROKEN) MC_GOELSE(tag##__else); else \ + MC_ALLOWELSE(tag##__else) \ + MC_AFTER(tag##__send, { \ + if ((DBBAD(&(rc)->bout) && (buferr((tv), (rc)), 1)) || \ + ((((fl)&QF_FORCE) || DBLEN(&(rc)->bout) >= SENDBUFSZ) && \ + remote_send(tv, rc))) \ + MC_GOELSE(tag##__else); \ + }) \ + DBUF_ENCLOSEITAG(tag##__frame, &(rc)->bout, (rc)->t, 64_L) \ + MC_BEFORE(tag##__pkty, { \ + dbuf_putu16l(&(rc)->bout, (pk)); \ + }) +#define QUEUEPK(tv, rc, fl, pk) QUEUEPK_TAG(queue, (tv), (rc), (fl), (pk)) /*----- Packet types ------------------------------------------------------*/ -#define TVPK_ERROR 0x0001 /* msg: string */ -#define TVPK_NOTICE 0x0002 /* msg: string */ -#define TVPK_STATUS 0x0003 /* st: char */ +#define TVPF_ACK 0x0001u + +#define TVPK_VER 0x0000u /* --> min, max: u16 * + * <-- ver: u16 */ +#define TVPK_BGROUP 0x0002u /* --> name: str16 + * <-- --- */ +#define TVPK_SETVAR 0x0004u /* --> name: str16, rv: value + * <-- rc: u8 */ +#define TVPK_TEST 0x0006u /* --> in: regs + * <-- --- */ +#define TVPK_EGROUP 0x0008u /* --> --- * + * <-- --- */ + +#define TVPK_REPORT 0x0100u /* <-- level: u16; msg: string */ +#define TVPK_PROGRESS 0x0102u /* <-- st: str16 */ -#define TVPK_BGROUP 0x0101 /* name: string */ -#define TVPK_TEST 0x0102 /* in: regs */ -#define TVPK_EGROUP 0x0103 /* --- */ +#define TVPK_SKIPGRP 0x0104u /* <-- excuse: str16 */ +#define TVPK_SKIP 0x0106u /* <-- excuse: str16 */ +#define TVPK_FAIL 0x0108u /* <-- flag: u8, detail: str16 */ +#define TVPK_DUMPREG 0x010au /* <-- ri: u16; disp: u16; + * flag: u8, rv: value */ +#define TVPK_BBENCH 0x010cu /* <-- ident: str32; unit: u16 */ +#define TVPK_EBENCH 0x010eu /* <-- ident: str32; unit: u16; + * flags: u16; n, t, cy: f64 */ -#define TVPK_SKIPGRP 0x0201 /* excuse: string */ -#define TVPK_SKIP 0x0202 /* excuse: string */ -#define TVPK_FAIL 0x0203 /* detail: string */ -#define TVPK_MISMATCH 0x0204 /* in, out: regs */ -#define TVPK_BBENCH 0x0205 /* in: regs */ -#define TVPK_EBENCH 0x0206 /* flags: u16; n: u64; t, cy: float */ +/*----- Server ------------------------------------------------------------*/ -/*----- The output driver -------------------------------------------------*/ +/* Forward declaration of output operations. */ +static const struct tvec_outops remote_ops; -#define SENDPK(ro, pk) \ - MC_BEFORE(setpk, \ - { buf_reset(&(ro)->r.bout); \ - buf_putu16l(&(ro)->r.bout.b, (pk)); }) \ - MC_AFTER(send, \ - { tvec_send(&ro->_o.tv, &ro->r); }) +static struct tvec_state srvtv; /* server's test-vector state */ +static struct tvec_remotecomms srvrc = TVEC_REMOTECOMMS_INIT; /* comms */ +static struct tvec_output srvout = { &remote_ops }; /* output state */ + +/* --- @tvec_setprogress@, @tvec_setprogress_v@ --- * + * + * Arguments: @const char *status@ = progress status token format + * @va_list ap@ = argument tail + * + * Returns: --- + * + * Use: Reports the progress of a test execution to the client. + * + * The framework makes use of tokens beginning with %|%|%: + * + * * %|%IDLE|%: during the top-level server code; + * + * * %|%SETUP|%: during the enclosing environment's @before@ + * function; + * + * * %|%RUN|%: during the environment's @run@ function, or the + * test function; and + * + * * %|%DONE|%: during the enclosing environment's @after@ + * function. + * + * The intent is that a test can use the progress token to check + * that a function which is expected to crash does so at the + * correct point, so it's expected that more complex test + * functions and/or environments will set their own progress + * tokens to reflect what's going on. + */ -static int sendstr(struct tvec_output *o, unsigned pk, - const char *p, va_list *ap) +int tvec_setprogress(const char *status, ...) { - struct remote_output *ro = (struct remote_output *)o; + va_list ap; + int rc; - if (ro->r.f&TVRF_BROKEN) return (-1); - dbuf_reset(&ro->r.bout); - buf_putu16l(&ro->r.bout.b, TVPK_ERROR); - buf_vputstrf16l(&ro->r.bout.b, msg, ap); - return (tvec_send(ro->_o.tv, &ro->r)); + va_start(ap, status); rc = tvec_setprogress_v(status, &ap); va_end(ap); + return (rc); } -static void report(struct tvec_output *o, unsigned pk, - const char *msg, va_list *ap) +int tvec_setprogress_v(const char *status, va_list *ap) { - if (sendstr(o, pk, msg, ap)) { - fprintf(stderr, "%s: ", QUIS); - vfprintf(stderr, msg, *ap); - fputc('\n', stderr); - } + /* Force immediate output in case we crash before the buffer is output + * organically. + */ + QUEUEPK(&srvtv, &srvrc, QF_FORCE, TVPK_PROGRESS) + dbuf_vputstrf16l(&srvrc.bout, status, ap); + else return (-1); + return (0); } -static void remote_error(struct tvec_output *o, const char *msg, va_list *ap) - { report(o, TVPK_ERROR, msg, ap); } - -static void remote_notice(struct tvec_output *o, - const char *msg, va_list *ap) - { report(o, TVPK_NOTICE, msg, ap); } +/* --- @tvec_remoteserver@ --- * + * + * Arguments: @int infd@, @int outfd@ = input and output file descriptors + * @const struct tvec_config *config@ = test configuration + * + * Returns: Suggested exit code. + * + * Use: Run a test server, reading packets from @infd@ and writing + * responses and notifications to @outfd@, and invoking tests as + * described by @config@. + * + * This function is not particularly general purpose. It + * expects to `take over' the process, and makes use of private + * global variables. + */ -static void remote_setstatus(struct tvec_ouptut *o, int st) +int tvec_remoteserver(int infd, int outfd, const struct tvec_config *config) { - struct remote_output *ro = (struct remote_output *)o; - SENDPK(ro, TVPK_STATUS) buf_putbyte(&ro->r.bout.b, st); -} + uint16 pk, u, v; + unsigned i; + buf b; + dstr d = DSTR_INIT; + const struct tvec_test *t; + void *p; size_t sz; + const struct tvec_env *env = 0; + const struct tvec_vardef *vd = 0; void *varctx; + struct tvec_reg *r = 0, rbuf, *r_alloc = 0; size_t rsz = 0; + void *ctx = 0; + int rc; -static void remote_skipgroup(struct tvec_output *o, - const char *excuse, va_list *ap) - { sendstr(o, TVPK_SKIPGRP, excuse, ap); } + /* Initialize the communication machinery. */ + setup_comms(&srvrc, infd, outfd); -static void remote_skip(struct tvec_output *o, - const char *excuse, va_list *ap) - { sendstr(o, TVPK_SKIP, excuse, ap); } + /* Begin a test session using our custom output driver. */ + tvec_begin(&srvtv, config, &srvout); -static void remote_fail(struct tvec_output *o, - const char *detail, va_list *ap) - { sendstr(o, TVPK_FAIL, detail, ap); } + /* Version negotiation. Expect a @TVPK_VER@ packet. At the moment, + * there's only version zero, so we return that. + */ + if (remote_recv(&srvtv, &srvrc, 0, &b)) { rc = -1; goto end; } + if (buf_getu16l(&b, &pk)) goto bad; + if (pk != TVPK_VER) { + rc = ioerr(&srvtv, &srvrc, + "unexpected packet type 0x%04x instead of client version", + pk); + goto end; + } + if (buf_getu16l(&b, &u) || buf_getu16l(&b, &v)) goto bad; + QUEUEPK(&srvtv, &srvrc, QF_FORCE, TVPK_VER | TVPF_ACK) + dbuf_putu16l(&srvrc.bout, 0); + else { rc = -1; goto end; } -static void remote_mismatch(struct tvec_output *o) -{ - struct remote_output *ro = (struct remote_output *)o; - struct tvec_state *rv = ro->_o.tv; + /* Handle packets until the server closes the connection. + * + * The protocol looks much simpler from our point of view than from the + * client. + * + * * Receive @TVPK_VER@; respond with @TVPK_VER | TVPF_ACK@. + * + * * Receive zero or more @TVPK_BGROUP@. Open a test group, producing + * output packets, and eventually answer with @TVPK_BGROUP | TVPF_ACK@. + * + * -- Receive zero or more @TVPK_TEST@. Run a test, producing output + * packets, and eventually answer with @TVPK_TEST | TVPF_ACK@. + * + * -- Receive @TVPK_EGROUP@. Maybe produce output packets, and + * answer with @TVPK_EGROUP | TVPF_ACK@. + * + * * Read EOF. Stop. + */ + for (;;) { - SENDPK(ro, TVPK_MISMATCH) { - tvec_serialize(tv, &ro->r.bout.b, tv->in, tv->nreg, tv->regsz); - tvec_serialize(tv, &ro->r.bout.b, tv->out, tv->nrout, tv->regsz); - } -} + /* Read a packet. End-of-file is expected here (and pretty much nowhere + * else). Otherwise, we expect to see @TVPK_BGROUP@. + */ + rc = remote_recv(&srvtv, &srvrc, RCVF_ALLOWEOF, &b); + if (rc == RECV_EOF) break; + else if (rc == RECV_FAIL) goto end; + if (buf_getu16l(&b, &pk)) goto bad; -static void remote_bbench(struct tvec_output *o) -{ - struct remote_output *ro = (struct remote_output *)o; - struct tvec_state *rv = ro->_o.tv; + switch (pk) { - SENDPK(ro, TVPK_BBENCH) - tvec_serialize(tv, &ro->r.bout.b, tv->in, tv->nreg, tv->regsz); -} + case TVPK_BGROUP: + /* Start a group. */ -static void remote_ebench(struct tvec_output *o, - const struct bench_timing *t) -{ - struct remote_output *ro = (struct remote_output *)o; - kludge64 k; + /* Parse the packet payload. */ + p = buf_getmem16l(&b, &sz); if (!p) goto bad; + if (BLEFT(&b)) goto bad; + + /* Find the group given its name. */ + for (t = srvtv.tests; t->name; t++) + if (strlen(t->name) == sz && MEMCMP(t->name, ==, p, sz)) + goto found_group; + rc = ioerr(&srvtv, &srvrc, "unknown test group `%.*s'", + (int)sz, (char *)p); + goto end; + + found_group: + /* Set up the test environment. */ + srvtv.test = t; env = t->env; + if (env && env->setup == tvec_remotesetup) + env = ((struct tvec_remoteenv *)env)->r.env; + if (!env || !env->ctxsz) ctx = 0; + else ctx = xmalloc(env->ctxsz); + if (env && env->setup) env->setup(&srvtv, env, 0, ctx); + + /* Initialize the registers. */ + tvec_initregs(&srvtv); - SENDPK(ro, TVPK_EBENCH) { - buf_putu16l(&ro->r.bout.b, t->f); - ASSIGN64(k, t->n); buf_putk64l(&ro->r.bout.b, k); - if (t->f&BTF_TIMEOK) buf_putf64l(&ro->r.bout.b, t->t); - if (t->f&BTF_CYOK) buf_putf64l(&ro->r.bout.b, t->cy); + /* Report that the group has been opened and that we're ready to run + * tests. + */ + QUEUEPK(&srvtv, &srvrc, QF_FORCE, TVPK_BGROUP | TVPF_ACK); + else { rc = -1; goto end; } + + /* Handle packets until we're told to end the group. */ + for (;;) { + + /* Read a packet. We expect @TVPK_EGROUP@ or @TVPK_TEST@. */ + if (remote_recv(&srvtv, &srvrc, 0, &b)) { rc = -1; goto end; } + if (buf_getu16l(&b, &pk)) goto bad; + + switch (pk) { + + case TVPK_EGROUP: + /* End the group. */ + + /* Check the payload. */ + if (BLEFT(&b)) goto bad; + + /* Leave the group loop. */ + goto endgroup; + + case TVPK_SETVAR: + /* Set a subenvironment variable. */ + + /* Get the variable name. */ + p = buf_getmem16l(&b, &sz); if (!p) goto bad; + DRESET(&d); DPUTM(&d, p, sz); DPUTZ(&d); + + /* Look up the variable definition. */ + if (env && env->findvar) { + vd = env->findvar(&srvtv, d.buf, &varctx, ctx); + if (vd) goto found_var; + } + rc = tvec_unkreg(&srvtv, d.buf); goto setvar_end; + found_var: + + /* Set up the register. */ + if (vd->regsz <= sizeof(rbuf)) + r = &rbuf; + else { + GROWBUF_REPLACE(&arena_stdlib, r_alloc, rsz, vd->regsz, + 8*sizeof(void *), 1); + r = r_alloc; + } + + /* Collect and set the value. */ + vd->def.ty->init(&r->v, &vd->def); + if (vd->def.ty->frombuf(&b, &r->v, &vd->def)) goto bad; + if (BLEFT(&b)) goto bad; + rc = vd->setvar(&srvtv, d.buf, &r->v, varctx); + + /* Send the reply. */ + setvar_end: + QUEUEPK(&srvtv, &srvrc, QF_FORCE, TVPK_SETVAR | TVPF_ACK) + dbuf_putbyte(&srvrc.bout, rc ? 0xff : 0); + else { rc = -1; goto end; } + if (vd) { vd->def.ty->release(&r->v, &vd->def); vd = 0; } + break; + + case TVPK_TEST: + /* Run a test. */ + + /* Parse the packet payload. */ + if (tvec_deserialize(srvtv.in, &b, srvtv.test->regs, + srvtv.nreg, srvtv.regsz)) + goto bad; + if (BLEFT(&b)) goto bad; + + /* If we're not skipping the test group, then actually try to + * run the test. + */ + if (!(srvtv.f&TVSF_SKIP)) { + + /* Prepare the output registers and reset the test outcome. + * (The environment may force a skip.) + */ + for (i = 0; i < srvtv.nrout; i++) + if (TVEC_REG(&srvtv, in, i)->f&TVRF_LIVE) + TVEC_REG(&srvtv, out, i)->f |= TVRF_LIVE; + srvtv.f |= TVSF_ACTIVE; srvtv.f &= ~TVSF_OUTMASK; + + /* Invoke the environment @before@ function. */ + tvec_setprogress("%%SETUP"); + if (env && env->before) env->before(&srvtv, ctx); + + /* Run the actual test. */ + if (!(srvtv.f&TVSF_ACTIVE)) + /* setup forced a skip */; + else { + tvec_setprogress("%%RUN"); + if (env && env->run) + env->run(&srvtv, t->fn, ctx); + else { + t->fn(srvtv.in, srvtv.out, ctx); + tvec_check(&srvtv, 0); + } + } + + /* Conclude the test. */ + tvec_setprogress("%%DONE"); + if (env && env->after) env->after(&srvtv, ctx); + tvec_endtest(&srvtv); + } + + /* Reset the input registers and report completion. */ + tvec_releaseregs(&srvtv); tvec_initregs(&srvtv); + QUEUEPK(&srvtv, &srvrc, QF_FORCE, TVPK_TEST | TVPF_ACK); + else { rc = -1; goto end; } + break; + + default: + /* Some other kind of packet. Complain. */ + + rc = ioerr(&srvtv, &srvrc, + "unexpected packet type 0x%04x during test group", + pk); + goto end; + + } + } + + endgroup: + /* The test group completed. */ + + /* Tear down the environment and release other resources. */ + if (env && env->teardown) env->teardown(&srvtv, ctx); + tvec_releaseregs(&srvtv); + xfree(ctx); srvtv.test = 0; env = 0; ctx = 0; + + /* Report completion. */ + QUEUEPK(&srvtv, &srvrc, QF_FORCE, TVPK_EGROUP | TVPF_ACK); + else { rc = -1; goto end; } + break; + + default: + rc = ioerr(&srvtv, &srvrc, + "unexpected packet type 0x%04x at top level", pk); + } } + rc = 0; + +end: + /* Clean up and return. */ + if (env && env->teardown) env->teardown(&srvtv, ctx); + if (vd) vd->def.ty->release(&r->v, &vd->def); + xfree(ctx); xfree(r_alloc); + if (srvtv.test) tvec_releaseregs(&srvtv); + release_comms(&srvrc); tvec_end(&srvtv); + return (rc ? 2 : 0); + +bad: + /* Miscellaneous malformed packet. */ + rc = malformed(&srvtv, &srvrc); goto end; } -static void remote_write(struct tvec_output *o, const char *p, size_t sz) - { assert(!"remote_write"); } -static void remote_bsession(struct tvec_output *o) - { assert(!"remote_bsession"); } +/*----- Server output driver ----------------------------------------------*/ + +/* --- @remote_bsession@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink (ignored) + * @struct tvec_state *tv@ = the test state producing output + * + * Returns: --- + * + * Use: Begin a test session. + * + * The remote driver does nothing at all. + */ + +static void remote_bsession(struct tvec_output *o, struct tvec_state *tv) + { ; } + +/* --- @remote_esession@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink (ignored) + * + * Returns: Suggested exit code. + * + * Use: End a test session. + * + * The remote driver returns a suitable exit code without + * printing anything. + */ + static int remote_esession(struct tvec_output *o) - { assert(!"remote_esession"); return (-1); } + { return (srvtv.f&TVSF_ERROR ? 2 : 0); } + +/* --- @remote_bgroup@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink (ignored) + * + * Returns: --- + * + * Use: Begin a test group. + * + * This is a stub which should never be called. + */ + static void remote_bgroup(struct tvec_output *o) { assert(!"remote_bgroup"); } -static void remote_btest(struct tvec_output *o) - { assert(!"remote_btest"); } -static void remote_egroup(struct tvec_output *o, unsigned outcome) - { assert(!"remote_egroup"); } -static void remote_etest(struct tvec_output *o, unsigned outcome) - { assert(!"remote_etest"); } -static void remote_destroy(struct tvec_output *o) +/* --- @remote_skipgroup@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink (ignored) + * @const char *excuse@, @va_list *ap@ = reason for skipping the + * group, or null + * + * Returns: --- + * + * Use: Report that a test group is being skipped. + * + * The remote driver sends a @TVPK_SKIP@ packet to its client. + */ + +static void remote_skipgroup(struct tvec_output *o, + const char *excuse, va_list *ap) { + QUEUEPK(&srvtv, &srvrc, 0, TVPK_SKIPGRP) + dbuf_vputstrf16l(&srvrc.bout, excuse, ap); } -static const struct tvec_outops remote_ops = { - remote_error, remote_notice, remote_setstatus, remote_write, - remote_bsession, remote_esession, - remote_bgroup, remote_egroup, remote_skip, - remote_btest, remote_skip, remote_fail, remote_mismatch, remote_etest, - remote_bbench, remote_ebench, - remote_destroy +/* --- @remote_egroup@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink (ignored) + * + * Returns: --- + * + * Use: Report that a test group has finished. + * + * This is a stub which should never be called. + */ + +static void remote_egroup(struct tvec_output *o) + { assert(!"remote_egroup"); } + +/* --- @remote_btest@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink (ignored) + * + * Returns: --- + * + * Use: Report that a test is starting. + * + * This is a stub which should never be called. + */ -/*----- Main code ---------------------------------------------------------*/ +static void remote_btest(struct tvec_output *o) + { assert(!"remote_btest"); } + +/* --- @remote_skip@, @remote_fail@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink (ignored) + * @unsigned attr@ = attribute to apply to the outcome + * @const char *outcome@ = outcome string to report + * @const char *detail@, @va_list *ap@ = a detail message + * @const char *excuse@, @va_list *ap@ = reason for skipping the + * test + * + * Returns: --- + * + * Use: Report that a test has been skipped or failed. + * + * The remote driver sends a @TVPK_SKIP@ or @TVPK_FAIL@ packet + * to its client as appropriate. + */ +static void remote_skip(struct tvec_output *o, + const char *excuse, va_list *ap) +{ + QUEUEPK(&srvtv, &srvrc, 0, TVPK_SKIP) + dbuf_vputstrf16l(&srvrc.bout, excuse, ap); +} +static void remote_fail(struct tvec_output *o, + const char *detail, va_list *ap) +{ + QUEUEPK(&srvtv, &srvrc, 0, TVPK_FAIL) + if (!detail) + dbuf_putbyte(&srvrc.bout, 0); + else { + dbuf_putbyte(&srvrc.bout, 1); + dbuf_vputstrf16l(&srvrc.bout, detail, ap); + } +} + +/* --- @remote_dumpreg@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink (ignored) + * @unsigned disp@ = register disposition + * @const union tvec_regval *rv@ = register value + * @const struct tvec_regdef *rd@ = register definition + * + * Returns: --- + * + * Use: Dump a register. + * + * The remote driver sends a @TVPK_DUMPREG@ packet to its + * client. This will only work if the register definition is + * one of those listed in the current test definition. + */ + +static void remote_dumpreg(struct tvec_output *o, + unsigned disp, const union tvec_regval *rv, + const struct tvec_regdef *rd) +{ + const struct tvec_regdef *reg; + unsigned r; + + /* Find the register definition. */ + for (reg = srvtv.test->regs, r = 0; reg->name; reg++, r++) + if (reg == rd) goto found; + assert(!"unexpected register definition"); + +found: + QUEUEPK(&srvtv, &srvrc, 0, TVPK_DUMPREG) { + dbuf_putu16l(&srvrc.bout, r); + dbuf_putu16l(&srvrc.bout, disp); + if (!rv) + dbuf_putbyte(&srvrc.bout, 0); + else { + dbuf_putbyte(&srvrc.bout, 1); + rd->ty->tobuf(DBUF_BUF(&srvrc.bout), rv, rd); + } + } +} + +/* --- @remote_etest@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink (ignored) + * @unsigned outcome@ = the test outcome + * + * Returns: --- + * + * Use: Report that a test has finished. + * + * The remote driver does nothing at all. + */ + +static void remote_etest(struct tvec_output *o, unsigned outcome) + { ; } + +/* --- @remote_bbench@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink (ignored) + * @const char *ident@ = identifying register values + * @unsigned unit@ = measurement unit (@TVBU_...@) + * + * Returns: --- + * + * Use: Report that a benchmark has started. + * + * The remote driver sends a @TVPK_BBENCH@ packet to its client. + */ + +static void remote_bbench(struct tvec_output *o, + const char *ident, unsigned unit) +{ + QUEUEPK(&srvtv, &srvrc, 0, TVPK_BBENCH) { + dbuf_putstr32l(&srvrc.bout, ident); + dbuf_putu16l(&srvrc.bout, unit); + } +} + +/* --- @remote_ebench@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink (ignored) + * @const char *ident@ = identifying register values + * @unsigned unit@ = measurement unit (@TVBU_...@) + * @const struct bench_timing *tm@ = measurement + * + * Returns: --- + * + * Use: Report a benchmark's results + * + * The remote driver sends a @TVPK_EBENCH@ packet to its client. + */ + +static void remote_ebench(struct tvec_output *o, + const char *ident, unsigned unit, + const struct bench_timing *t) +{ + QUEUEPK(&srvtv, &srvrc, 0, TVPK_EBENCH) { + dbuf_putstr32l(&srvrc.bout, ident); + dbuf_putu16l(&srvrc.bout, unit); + if (!t || !(t->f&BTF_ANY)) + dbuf_putu16l(&srvrc.bout, 0); + else { + dbuf_putu16l(&srvrc.bout, t->f); + dbuf_putf64l(&srvrc.bout, t->n); + if (t->f&BTF_TIMEOK) dbuf_putf64l(&srvrc.bout, t->t); + if (t->f&BTF_CYOK) dbuf_putf64l(&srvrc.bout, t->cy); + } + } +} + +/* --- @remote_report@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink (ignored) + * @unsigned level@ = message level (@TVLEV_...@) + * @const char *msg@, @va_list *ap@ = format string and + * arguments + * + * Returns: --- + * + * Use: Report a message to the user. + * + * The remote driver sends a @TVPK_REPORT@ packet to its + * client. If its attempt to transmit the packet fails, then + * the message is written to the standard error stream instead, + * in the hope that this will help it be noticed. + */ + +static void remote_report(struct tvec_output *o, unsigned level, + const char *msg, va_list *ap) +{ + QUEUEPK(&srvtv, &srvrc, 0, TVPK_REPORT) { + dbuf_putu16l(&srvrc.bout, level); + dbuf_vputstrf16l(&srvrc.bout, msg, ap); + } else { + fprintf(stderr, "%s %s: ", QUIS, tvec_strlevel(level)); + vfprintf(stderr, msg, *ap); + fputc('\n', stderr); + } +} + +/* --- @remote_destroy@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink (ignored) + * + * Returns: --- + * + * Use: Release the resources held by the output driver. + * + * The remote driver does nothing at all. + */ + +static void remote_destroy(struct tvec_output *o) + { ; } + +static const struct tvec_outops remote_ops = { + remote_bsession, remote_esession, + remote_bgroup, remote_skipgroup, remote_egroup, + remote_btest, remote_skip, remote_fail, remote_dumpreg, remote_etest, + remote_bbench, remote_ebench, + remote_report, + remote_destroy +}; + +/*----- Pseudoregister definitions ----------------------------------------*/ + +static tvec_setvarfn setvar_local, setvar_remote; + +static const struct tvec_flag exit_flags[] = { + + /* Cause codes. */ + { "running", TVXF_CAUSEMASK, TVXST_RUN }, + { "exited", TVXF_CAUSEMASK, TVXST_EXIT }, + { "killed", TVXF_CAUSEMASK, TVXST_KILL }, + { "stopped", TVXF_CAUSEMASK, TVXST_STOP }, + { "continued", TVXF_CAUSEMASK, TVXST_CONT }, + { "disconnected", TVXF_CAUSEMASK, TVXST_DISCONN }, + { "unknown", TVXF_CAUSEMASK, TVXST_UNK }, + { "error", TVXF_CAUSEMASK, TVXST_ERR }, + + /* + ;;; The signal name table is very boring to type. To make life less + ;;; awful, put the signal names in this list and evaluate the code to + ;;; get Emacs to regenerate it. + + (let ((signals '(HUP INT QUIT ILL TRAP ABRT IOT EMT FPE KILL BUS SEGV SYS + PIPE ALRM TERM URG STOP TSTP CONT CHLD CLD TTIN TTOU + POLL IO TIN XCPU XFSZ VTALRM PROF WINCH USR1 USR2 + STKFLT INFO PWR THR LWP LIBRT LOST))) + (save-excursion + (goto-char (point-min)) + (search-forward (concat "***" "BEGIN siglist" "***")) + (beginning-of-line 2) + (delete-region (point) + (progn + (search-forward "***END***") + (beginning-of-line) + (point))) + (dolist (sig signals) + (insert (format "#ifdef SIG%s\n { \"SIG%s\", TVXF_VALMASK | TVXF_SIG, SIG%s | TVXF_SIG },\n#endif\n" + sig sig sig))))) + */ + + /***BEGIN siglist***/ +#ifdef SIGHUP + { "SIGHUP", TVXF_VALMASK | TVXF_SIG, SIGHUP | TVXF_SIG }, +#endif +#ifdef SIGINT + { "SIGINT", TVXF_VALMASK | TVXF_SIG, SIGINT | TVXF_SIG }, +#endif +#ifdef SIGQUIT + { "SIGQUIT", TVXF_VALMASK | TVXF_SIG, SIGQUIT | TVXF_SIG }, +#endif +#ifdef SIGILL + { "SIGILL", TVXF_VALMASK | TVXF_SIG, SIGILL | TVXF_SIG }, +#endif +#ifdef SIGTRAP + { "SIGTRAP", TVXF_VALMASK | TVXF_SIG, SIGTRAP | TVXF_SIG }, +#endif +#ifdef SIGABRT + { "SIGABRT", TVXF_VALMASK | TVXF_SIG, SIGABRT | TVXF_SIG }, +#endif +#ifdef SIGIOT + { "SIGIOT", TVXF_VALMASK | TVXF_SIG, SIGIOT | TVXF_SIG }, +#endif +#ifdef SIGEMT + { "SIGEMT", TVXF_VALMASK | TVXF_SIG, SIGEMT | TVXF_SIG }, +#endif +#ifdef SIGFPE + { "SIGFPE", TVXF_VALMASK | TVXF_SIG, SIGFPE | TVXF_SIG }, +#endif +#ifdef SIGKILL + { "SIGKILL", TVXF_VALMASK | TVXF_SIG, SIGKILL | TVXF_SIG }, +#endif +#ifdef SIGBUS + { "SIGBUS", TVXF_VALMASK | TVXF_SIG, SIGBUS | TVXF_SIG }, +#endif +#ifdef SIGSEGV + { "SIGSEGV", TVXF_VALMASK | TVXF_SIG, SIGSEGV | TVXF_SIG }, +#endif +#ifdef SIGSYS + { "SIGSYS", TVXF_VALMASK | TVXF_SIG, SIGSYS | TVXF_SIG }, +#endif +#ifdef SIGPIPE + { "SIGPIPE", TVXF_VALMASK | TVXF_SIG, SIGPIPE | TVXF_SIG }, +#endif +#ifdef SIGALRM + { "SIGALRM", TVXF_VALMASK | TVXF_SIG, SIGALRM | TVXF_SIG }, +#endif +#ifdef SIGTERM + { "SIGTERM", TVXF_VALMASK | TVXF_SIG, SIGTERM | TVXF_SIG }, +#endif +#ifdef SIGURG + { "SIGURG", TVXF_VALMASK | TVXF_SIG, SIGURG | TVXF_SIG }, +#endif +#ifdef SIGSTOP + { "SIGSTOP", TVXF_VALMASK | TVXF_SIG, SIGSTOP | TVXF_SIG }, +#endif +#ifdef SIGTSTP + { "SIGTSTP", TVXF_VALMASK | TVXF_SIG, SIGTSTP | TVXF_SIG }, +#endif +#ifdef SIGCONT + { "SIGCONT", TVXF_VALMASK | TVXF_SIG, SIGCONT | TVXF_SIG }, +#endif +#ifdef SIGCHLD + { "SIGCHLD", TVXF_VALMASK | TVXF_SIG, SIGCHLD | TVXF_SIG }, +#endif +#ifdef SIGCLD + { "SIGCLD", TVXF_VALMASK | TVXF_SIG, SIGCLD | TVXF_SIG }, +#endif +#ifdef SIGTTIN + { "SIGTTIN", TVXF_VALMASK | TVXF_SIG, SIGTTIN | TVXF_SIG }, +#endif +#ifdef SIGTTOU + { "SIGTTOU", TVXF_VALMASK | TVXF_SIG, SIGTTOU | TVXF_SIG }, +#endif +#ifdef SIGPOLL + { "SIGPOLL", TVXF_VALMASK | TVXF_SIG, SIGPOLL | TVXF_SIG }, +#endif +#ifdef SIGIO + { "SIGIO", TVXF_VALMASK | TVXF_SIG, SIGIO | TVXF_SIG }, +#endif +#ifdef SIGTIN + { "SIGTIN", TVXF_VALMASK | TVXF_SIG, SIGTIN | TVXF_SIG }, +#endif +#ifdef SIGXCPU + { "SIGXCPU", TVXF_VALMASK | TVXF_SIG, SIGXCPU | TVXF_SIG }, +#endif +#ifdef SIGXFSZ + { "SIGXFSZ", TVXF_VALMASK | TVXF_SIG, SIGXFSZ | TVXF_SIG }, +#endif +#ifdef SIGVTALRM + { "SIGVTALRM", TVXF_VALMASK | TVXF_SIG, SIGVTALRM | TVXF_SIG }, +#endif +#ifdef SIGPROF + { "SIGPROF", TVXF_VALMASK | TVXF_SIG, SIGPROF | TVXF_SIG }, +#endif +#ifdef SIGWINCH + { "SIGWINCH", TVXF_VALMASK | TVXF_SIG, SIGWINCH | TVXF_SIG }, +#endif +#ifdef SIGUSR1 + { "SIGUSR1", TVXF_VALMASK | TVXF_SIG, SIGUSR1 | TVXF_SIG }, +#endif +#ifdef SIGUSR2 + { "SIGUSR2", TVXF_VALMASK | TVXF_SIG, SIGUSR2 | TVXF_SIG }, +#endif +#ifdef SIGSTKFLT + { "SIGSTKFLT", TVXF_VALMASK | TVXF_SIG, SIGSTKFLT | TVXF_SIG }, +#endif +#ifdef SIGINFO + { "SIGINFO", TVXF_VALMASK | TVXF_SIG, SIGINFO | TVXF_SIG }, +#endif +#ifdef SIGPWR + { "SIGPWR", TVXF_VALMASK | TVXF_SIG, SIGPWR | TVXF_SIG }, +#endif +#ifdef SIGTHR + { "SIGTHR", TVXF_VALMASK | TVXF_SIG, SIGTHR | TVXF_SIG }, +#endif +#ifdef SIGLWP + { "SIGLWP", TVXF_VALMASK | TVXF_SIG, SIGLWP | TVXF_SIG }, +#endif +#ifdef SIGLIBRT + { "SIGLIBRT", TVXF_VALMASK | TVXF_SIG, SIGLIBRT | TVXF_SIG }, +#endif +#ifdef SIGLOST + { "SIGLOST", TVXF_VALMASK | TVXF_SIG, SIGLOST | TVXF_SIG }, +#endif + /***END***/ + + /* This should be folded into the signal entries above. */ + { "signal", TVXF_SIG, TVXF_SIG }, + + TVEC_ENDFLAGS +}; +static const struct tvec_flaginfo exit_flaginfo = + { "exit-status", exit_flags, &tvrange_uint }; +static const struct tvec_vardef exit_var = + { sizeof(struct tvec_reg), setvar_local, + { "@exit", -1, &tvty_flags, 0, { &exit_flaginfo } } }; + +/* Progress. */ + +static const struct tvec_vardef progress_var = + { sizeof(struct tvec_reg), setvar_local, + { "@progress", -1, &tvty_text, 0 } }; + +/* Reconnection. */ + +static const struct tvec_uassoc reconn_assocs[] = { + { "on-demand", TVRCN_DEMAND }, + { "force", TVRCN_FORCE }, + { "skip", TVRCN_SKIP }, + TVEC_ENDENUM +}; +static const struct tvec_uenuminfo reconn_enuminfo = + { "remote-reconnection", reconn_assocs, &tvrange_uint }; +static const struct tvec_vardef reconn_var = + { sizeof(struct tvec_reg), setvar_local, + { "@reconnect", -1, &tvty_uenum, 0, { &reconn_enuminfo } } }; + +/*----- Client ------------------------------------------------------------*/ + +/* Connection state. */ +enum { + CONN_BROKEN = -2, /* previously broken */ + CONN_FAILED = -1, /* attempt freshly failed */ + CONN_ESTABLISHED = 0, /* previously established */ + CONN_FRESH = 1 /* freshly connected */ +}; + +/* --- @handle_packets@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_remotectx *r@ = remote client context + * @unsigned f@ = receive flags (@RCVF_...@) + * @uint16 end@ = expected end packet type + * @buf *b_out@ = buffer in which to return end packet payload + * + * Returns: A @RECV_...@ code. + * + * Use: Handles notification packets from the server until a final + * termination packet is received. + * + * The client/server protocol consists of a number of flows, + * beginning with a request from the client, followed by a + * number of notifications from the server, and terminated by an + * acknowledgement to the original request indicating that the + * server has completed acting on the original request. + * + * This function handles the notifications issued by the server, + * returning when one of the following occurs: (a) a packet of + * type @end@ is received, in which case the function returns + * @RECV_OK@ and the remainder of the packet payload is left in + * @b_out@; (b) the flag @RCVF_ALLOWEOF@ was set in @f@ on entry + * and end-of-file is received at a packet boundary, in which + * case the function returns @RECV_EOF@; or (c) an I/O error + * occurs, in which case @ioerr@ is called and the function + * returns @RECV_FAIL@. + */ + +static int handle_packets(struct tvec_state *tv, struct tvec_remotectx *r, + unsigned f, uint16 end, buf *b_out) +{ + struct tvec_output *o = tv->output; + uint16 pk, u, v; + const char *p; size_t n; + dstr d = DSTR_INIT; + buf *b = b_out; + const struct tvec_regdef *rd; + struct bench_timing bt; + struct tvec_reg *reg = 0; + unsigned i; + int rc; + + for (;;) { + + /* Read the next packet. If we didn't receive one then end the loop. + * Otherwise, retrieve the packet type and check it against @end@: quit + * the loop if we get a match. + */ + rc = remote_recv(tv, &r->rc, f, b); if (rc) break; + if (buf_getu16l(b, &pk)) goto bad; + if (pk == end) { rc = 0; break; } + + /* Dispatch based on the packet type. */ + switch (pk) { + + case TVPK_PROGRESS: + /* A progress report. Update the saved progress. */ + + p = buf_getmem16l(b, &n); if (!p) goto bad; + if (BLEFT(b)) goto bad; + + DRESET(&r->progress); DPUTM(&r->progress, p, n); DPUTZ(&r->progress); + break; + + case TVPK_REPORT: + /* A report. Recover the message and pass it along. */ + + if (buf_getu16l(b, &u)) goto bad; + p = buf_getmem16l(b, &n); if (!p) goto bad; + if (BLEFT(b)) goto bad; + + DRESET(&d); DPUTM(&d, p, n); DPUTZ(&d); + tvec_report(tv, u, "%s", d.buf); + break; + + case TVPK_SKIPGRP: + /* A request to skip the group. Recover the excuse message and pass + * it along. + */ + + p = buf_getmem16l(b, &n); if (!p) goto bad; + if (BLEFT(b)) goto bad; + + DRESET(&d); DPUTM(&d, p, n); DPUTZ(&d); + tvec_skipgroup(tv, "%s", d.buf); + break; + + case TVPK_SKIP: + /* A request to skip the test. Recover the excuse message and pass + * it along, if it's not unreasonable. + */ + + if (!(tv->f&TVSF_ACTIVE)) { + rc = ioerr(tv, &r->rc, "test `%s' not active", tv->test->name); + goto end; + } + + p = buf_getmem16l(b, &n); if (!p) goto bad; + if (BLEFT(b)) goto bad; + + DRESET(&d); DPUTM(&d, p, n); DPUTZ(&d); + tvec_skip(tv, "%s", d.buf); + break; + + case TVPK_FAIL: + /* A report that the test failed. Recover the detail message, if + * any, and pass it along, if it's not unreasonable. + */ + + if (!(tv->f&TVSF_ACTIVE) && + ((tv->f&TVSF_OUTMASK) != (TVOUT_LOSE << TVSF_OUTSHIFT))) { + rc = ioerr(tv, &r->rc, "test `%s' not active or failing", + tv->test->name); + goto end; + } + + rc = buf_getbyte(b); if (rc < 0) goto bad; + if (rc) { p = buf_getmem16l(b, &n); if (!p) goto bad; } + else p = 0; + if (BLEFT(b)) goto bad; + + if (!p) + tvec_fail(tv, 0); + else { + DRESET(&d); DPUTM(&d, p, n); DPUTZ(&d); + tvec_fail(tv, "%s", d.buf); + } + break; + + case TVPK_DUMPREG: + /* A request to dump a register. */ + + /* Find the register definition. */ + if (buf_getu16l(b, &u) || buf_getu16l(b, &v)) goto bad; + for (rd = tv->test->regs, i = 0; rd->name; rd++, i++) + if (i == u) goto found_reg; + rc = ioerr(tv, &r->rc, + "register definition %u out of range for test `%s'", + u, tv->test->name); + goto end; + found_reg: + if (v >= TVRD_LIMIT) { + rc = ioerr(tv, &r->rc, "register disposition %u out of range", v); + goto end; + } + + /* Read the flag. If there's no register value, then `dump' its + * absence. Otherwise retrieve the register value and dump it. + */ + rc = buf_getbyte(b); if (rc < 0) goto bad; + if (!rc) + tvec_dumpreg(tv, v, 0, rd); + else { + if (!reg) reg = xmalloc(tv->regsz); + rd->ty->init(®->v, rd); + rc = rd->ty->frombuf(b, ®->v, rd); + if (!rc) tvec_dumpreg(tv, v, ®->v, rd); + rd->ty->release(®->v, rd); + if (rc) goto bad; + } + if (BLEFT(b)) goto bad; + break; + + case TVPK_BBENCH: + /* A report that we're starting a benchmark. Pass this along. */ + + p = buf_getmem32l(b, &n); if (!p) goto bad; + if (buf_getu16l(b, &u)) goto bad; + if (BLEFT(b)) goto bad; + if (u >= TVBU_LIMIT) { + rc = ioerr(tv, &r->rc, "unit code %u out of range", u); + goto end; + } + + DRESET(&d); DPUTM(&d, p, n); DPUTZ(&d); + o->ops->bbench(o, d.buf, u); + break; + + case TVPK_EBENCH: + /* A report that a benchmark completed. Pass this along. */ + + p = buf_getmem32l(b, &n); if (!p) goto bad; + if (buf_getu16l(b, &u) || buf_getu16l(b, &v)) goto bad; + if (u >= TVBU_LIMIT) { + rc = ioerr(tv, &r->rc, "unit code %u out of range", u); + goto end; + } + if ((v&BTF_ANY) && buf_getf64l(b, &bt.n)) goto bad; + if ((v&BTF_TIMEOK) && buf_getf64l(b, &bt.t)) goto bad; + if ((v&BTF_CYOK) && buf_getf64l(b, &bt.cy)) goto bad; + if (BLEFT(b)) goto bad; + + DRESET(&d); DPUTM(&d, p, n); DPUTZ(&d); + o->ops->ebench(o, d.buf, u, v&BTF_ANY ? &bt : 0); + break; + + default: + /* Something else. This is unexpected. */ + + rc = ioerr(tv, &r->rc, "unexpected packet type 0x%04x", pk); + goto end; + } + } + +end: + DDESTROY(&d); + xfree(reg); + return (rc); +bad: + rc = malformed(tv, &r->rc); goto end; +} + +/* --- @reap_kid@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_remotectx *r@ = remote client context + * + * Returns: --- + * + * Use: Determine the exit status of a broken connection, setting + * @r->exit@ appropriately. + * + * If @r->kid@ is negative, the exit status has already been + * set, and nothing further happens; this is not an error. + * + * If @r->kid@ is zero, then there is no real child process + * (e.g., because the remote connection is a network connection + * or similar), so @r->exit@ is set equal to @RVXST_DISCONN@. + * + * If @r->kid@ is positive, then it holds a child process id; + * the function waits for it to end and collects its exit status + * + * It is an error to call this function if the connection is not + * broken. + */ + +static void reap_kid(struct tvec_state *tv, struct tvec_remotectx *r) +{ + pid_t kid; + int st; + + assert(r->rc.f&TVRF_BROKEN); + if (!r->kid) + { r->exit = TVXST_DISCONN; r->kid = -1; } + else if (r->kid > 0) { + kid = waitpid(r->kid, &st, 0); + if (kid < 0) { + tvec_notice(tv, "failed to wait for remote child: %s", + strerror(errno)); + r->exit = TVXST_ERR; + } else if (!kid) { + tvec_notice(tv, "remote child vanished without a trace"); + r->exit = TVXST_ERR; + } else if (WIFCONTINUED(st)) + r->exit = TVXST_CONT; + else if (WIFSIGNALED(st)) + r->exit = TVXST_KILL | TVXF_SIG | WTERMSIG(st); + else if (WIFSTOPPED(st)) + r->exit = TVXST_STOP | TVXF_SIG | WSTOPSIG(st); + else if (WIFEXITED(st)) + r->exit = TVXST_EXIT | WEXITSTATUS(st); + else { + tvec_notice(tv, "remote child died with unknown status 0x%04x", + (unsigned)st); + r->exit = TVXST_UNK; + } + r->kid = -1; + } +} + +/* --- @report_errline@ --- * + * + * Arguments: @char *p@ = pointer to the line + * @size_t n@ = length in characters + * @void *ctx@ = context, secretly a @struct tvec_remotectx@ + * + * Returns: --- + * + * Use: Print a line of stderr output from the child. If + * @TVRF_MUFFLE@ is set, then discard the line silently. + * + * This is an @lbuf_func@, invoked via @drain_errfd@. + */ + +static void report_errline(char *p, size_t n, void *ctx) +{ + struct tvec_remotectx *r = ctx; + struct tvec_state *tv = r->tv; + + if (p && !(r->rc.f&TVRF_MUFFLE)) + tvec_notice(tv, "child process stderr: %s", p); +} + +/* --- @drain_errfd@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_remotectx *r@ = remote client context + * @unsigned f@ = receive flags (@ERF_...@) + * + * Returns: Zero on success, %$-1$% on error. + * + * Use: Collect material written by the child to its stderr stream + * and report it. + * + * If @f@ has @ERF_SILENT@ set, then discard the stderr material + * without reporting it. Otherwise it is reported as + * @TVLEV_NOTE@. + * + * if @f@ has @ERF_CLOSE@ set, then continue reading until + * end-of-file is received; also, report any final partial line, + * and close @r->errfd@. + * + * If @r->errfd@ is already closed, or never established, then + * do nothing and return successfully. + */ + +#define ERF_SILENT 0x0001u +#define ERF_CLOSE 0x0002u +static int drain_errfd(struct tvec_state *tv, struct tvec_remotectx *r, + unsigned f) +{ + char *p; size_t sz; + ssize_t n; + int rc; + + /* Preliminaries. Bail if there is no error stream to fetch. Arrange + * (rather clumsily) to muffle the output if we're supposed to be client. + * And set the nonblocking state on @errfd@ appropriately. + */ + if (r->errfd < 0) { rc = 0; goto end; } + if (f&ERF_SILENT) r->rc.f |= TVRF_MUFFLE; + else r->rc.f &= ~TVRF_MUFFLE; + if (fdflags(r->errfd, O_NONBLOCK, f&ERF_CLOSE ? 0 : O_NONBLOCK, 0, 0)) { + rc = ioerr(tv, &r->rc, "failed to %s error non-blocking flag", + f&ERF_CLOSE ? "clear" : "set"); + goto end; + } + + /* Read pieces of error output and feed them into the line buffer. */ + for (;;) { + sz = lbuf_free(&r->errbuf, &p); + n = read(r->errfd, p, sz); + if (!n) break; + if (n < 0) { + if (errno == EINTR) continue; + if (!(f&ERF_CLOSE) && (errno == EWOULDBLOCK || errno == EAGAIN)) + break; + rc = ioerr(tv, &r->rc, "failed to read child stderr: %s", + strerror(errno)); + goto end; + } + lbuf_flush(&r->errbuf, p, n); + } + + /* Done. */ + rc = 0; +end: + if (f&ERF_CLOSE) { + lbuf_close(&r->errbuf); + close(r->errfd); r->errfd = -1; + } + return (rc); +} + +/* --- @disconnect_remote@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_remotectx *r@ = remote client context + * @unsigned f@ = receive flags (@DCF_...@) + * + * Returns: --- + * + * Use: Disconnect and shut down all of the remote client state. + * + * If @f@ has @DCF_KILL@ set then send the child process (if + * any) @SIGTERM@ to make sure it shuts down in a timely manner. + * + * In detail: this function closes the @infd@ and @outfd@ + * descriptors, drains and closes @errfd@, and collects the exit + * status (if any). + */ + +#define DCF_KILL 0x0100u +static void disconnect_remote(struct tvec_state *tv, + struct tvec_remotectx *r, unsigned f) +{ + if (r->kid > 0 && (f&DCF_KILL)) kill(r->kid, SIGTERM); + close_comms(&r->rc); + drain_errfd(tv, r, f | ERF_CLOSE); reap_kid(tv, r); +} + +/* --- @connect_remote@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_remotectx *r@ = remote client context + * + * Returns: Zero on success, %$-1$% on error. + * + * Use: Connect to the test server. + */ + +static int connect_remote(struct tvec_state *tv, struct tvec_remotectx *r) +{ + const struct tvec_remoteenv *re = r->re; + pid_t kid = 0; + buf b; + uint16 v; + int infd = -1, outfd = -1, errfd = -1, rc; + + /* If we're already connected, then there's nothing to do. */ + if (r->kid >= 0) { rc = 0; goto end; } + + /* Set the preliminary progress indication. */ + DRESET(&r->progress); DPUTS(&r->progress, "%INIT"); + + /* Call the connection function to establish descriptors. */ + if (re->r.connect(&kid, &infd, &outfd, &errfd, tv, re)) + { rc = -1; goto end; } + + /* Establish communications state. */ + setup_comms(&r->rc, infd, outfd); r->kid = kid; r->errfd = errfd; + lbuf_init(&r->errbuf, report_errline, r); + r->exit = TVXST_RUN; r->rc.f &= ~TVRF_BROKEN; + + /* Do version negotiation. */ + QUEUEPK(tv, &r->rc, QF_FORCE, TVPK_VER) { + dbuf_putu16l(&r->rc.bout, 0); + dbuf_putu16l(&r->rc.bout, 0); + } else { rc = -1; goto end; } + if (handle_packets(tv, r, 0, TVPK_VER | TVPF_ACK, &b)) + { rc = -1; goto end; } + if (buf_getu16l(&b, &v)) goto bad; + if (BLEFT(&b)) { rc = malformed(tv, &r->rc); goto end; } + if (v) { + rc = ioerr(tv, &r->rc, "protocol version %u not supported", v); + goto end; + } + r->ver = v; + + /* Begin the test group at the server. */ + QUEUEPK(tv, &r->rc, QF_FORCE, TVPK_BGROUP) + dbuf_putstr16l(&r->rc.bout, tv->test->name); + else { rc = -1; goto end; } + if (handle_packets(tv, r, 0, TVPK_BGROUP | TVPF_ACK, &b)) + { rc = -1; goto end; } + if (BLEFT(&b)) { rc = malformed(tv, &r->rc); goto end; } + + /* Done. */ + rc = 0; +end: + if (rc) disconnect_remote(tv, r, DCF_KILL); + return (rc); +bad: + rc = malformed(tv, &r->rc); goto end; +} + +/* --- @check_comms@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_remotectx *r@ = remote client context + * + * Returns: A @CONN_...@ code reflecting the current communication + * state. + * + * Use: Determine the current connection state. If the connection + * has recently broken (i.e., @TVRF_BROKEN@ is set in @r->rc.f@) + * since the last time we checked then disconnect. + */ + +static int check_comms(struct tvec_state *tv, struct tvec_remotectx *r) +{ + if (r->kid < 0) + return (CONN_BROKEN); + else if (r->rc.f&TVRF_BROKEN) + { disconnect_remote(tv, r, DCF_KILL); return (CONN_FAILED); } + else + return (CONN_ESTABLISHED); +} + +/* --- @try_reconnect@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_remotectx *r@ = remote client context + * + * Returns: A @CONN_...@ code reflecting the new communication state. + * + * Use: Reconnects to the server according to the configured + * @TVRCN_...@ policy. + */ + +static int try_reconnect(struct tvec_state *tv, struct tvec_remotectx *r) +{ + int rc; + + switch (r->rc.f&TVRF_RCNMASK) { + case TVRCN_DEMAND: + rc = check_comms(tv, r); + if (rc < CONN_ESTABLISHED) { + close_comms(&r->rc); + if (connect_remote(tv, r)) rc = CONN_FAILED; + else rc = CONN_FRESH; + } + break; + case TVRCN_FORCE: + disconnect_remote(tv, r, DCF_KILL); + if (connect_remote(tv, r)) rc = CONN_FAILED; + else rc = CONN_FRESH; + break; + case TVRCN_SKIP: + rc = check_comms(tv, r); + break; + default: + abort(); + } + return (rc); +} + +/*----- Remote environment ------------------------------------------------*/ + +/* --- @reset_vars@ --- * + * + * Arguments: @struct tvec_remotectx *r@ = remote client context + * + * Returns: --- + * + * Use: Reset the pseudoregisters set through @tvec_remoteset@. + */ + +static void reset_vars(struct tvec_remotectx *r) +{ + const struct tvec_remoteenv *re = r->re; + + r->exwant = TVXST_RUN; + r->rc.f = (r->rc.f&~(TVRF_RCNMASK | TVRF_SETMASK)) | + (re->r.dflt_reconn&TVRF_RCNMASK); + DRESET(&r->prgwant); DPUTS(&r->prgwant, "%DONE"); +} + +/* --- @tvec_remotesetup@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @const struct tvec_env *env@ = environment description + * @void *pctx@ = parent context (ignored) + * @void *ctx@ = context pointer to initialize + * + * Returns: --- + * + * Use: Initialize a timeout environment context. + * + * The environment description should be a @struct + * tvec_remoteenv@ subclass suitable for use by the @connect@ + * function. + */ + +void tvec_remotesetup(struct tvec_state *tv, const struct tvec_env *env, + void *pctx, void *ctx) +{ + struct tvec_remotectx *r = ctx; + const struct tvec_remoteenv *re = (const struct tvec_remoteenv *)env; + const struct tvec_env *subenv = re->r.env; + + r->tv = tv; + init_comms(&r->rc); + r->re = re; r->kid = -1; + DCREATE(&r->prgwant); DCREATE(&r->progress); + if (connect_remote(tv, r)) + tvec_skipgroup(tv, "failed to connect to test backend"); + reset_vars(r); + if (subenv && subenv->ctxsz) r->subctx = xmalloc(subenv->ctxsz); + else r->subctx = 0; + if (subenv && subenv->setup) subenv->setup(tv, subenv, r, r->subctx); +} + +/* --- @tvec_remotefindvar@, @setvar_local@, @setvar_remote@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @const char *var@ = variable name to set + * @const union tvec_regval *rv@ = register value + * @void **ctx_out@ = where to put the @setvar@ context + * @void *ctx@ = context pointer + * + * Returns: @tvec_remotefindvar@ returns a pointer to the variable + * definition, or null; @remote_setvar@ returns zero on success + * or %$-1$% on error. + * + * Use: Set a special variable. The following special variables are + * supported. + * + * * %|@exit|% is the expected exit status; see @TVXF_...@ and + * @TVXST_...@. + * + * * %|progress|% is the expected progress token when the test + * completes. On successful completion, this will be + * %|%DONE|%; it's %|%RUN|% on entry to the test function, + * but that can call @tvec_setprogress@ to change it. + * + * * %|reconnect|% is a reconnection policy; see @TVRCN_...@. + */ + +static int setvar_local(struct tvec_state *tv, const char *var, + const union tvec_regval *rv, void *ctx) +{ + struct tvec_remotectx *r = ctx; + + if (STRCMP(var, ==, "@exit")) { + if (r->rc.f&TVRF_SETEXIT) return (tvec_dupreg(tv, var)); + r->exwant = rv->u; r->rc.f |= TVRF_SETEXIT; return (0); + } else if (STRCMP(var, ==, "@progress")) { + if (r->rc.f&TVRF_SETPRG) return (tvec_dupreg(tv, var)); + DRESET(&r->prgwant); DPUTM(&r->prgwant, rv->text.p, rv->text.sz); + DPUTZ(&r->prgwant); + r->rc.f |= TVRF_SETPRG; return (0); + } else if (STRCMP(var, ==, "@reconnect")) { + if (r->rc.f&TVRF_SETRCN) return (tvec_dupreg(tv, var)); + r->rc.f = (r->rc.f&~TVRF_RCNMASK) | (rv->u&TVRF_RCNMASK) | TVRF_SETRCN; + return (0); + } else assert(!"unknown var"); +} + +static int setvar_remote(struct tvec_state *tv, const char *var, + const union tvec_regval *rv, void *ctx) +{ + struct tvec_remotectx *r = ctx; + buf b; + int ch, rc; + + if (try_reconnect(tv, r) < 0) { rc = 0; goto end; } + + QUEUEPK(tv, &r->rc, QF_FORCE, TVPK_SETVAR) { + dbuf_putstr16l(&r->rc.bout, var); + r->vd.def.ty->tobuf(DBUF_BUF(&r->rc.bout), rv, &r->vd.def); + } else { rc = -1; goto end; } + + rc = handle_packets(tv, r, 0, TVPK_SETVAR | TVPF_ACK, &b); + if (rc) goto end; + ch = buf_getbyte(&b); + if (ch < 0) { rc = malformed(tv, &r->rc); goto end; } + if (BLEFT(&b)) { rc = malformed(tv, &r->rc); goto end; } + + rc = ch ? -1 : 0; +end: + return (rc); +} + +const struct tvec_vardef *tvec_remotefindvar + (struct tvec_state *tv, const char *var, void **ctx_out, void *ctx) +{ + struct tvec_remotectx *r = ctx; + const struct tvec_remoteenv *re = r->re; + const struct tvec_env *subenv = re->r.env; + const struct tvec_vardef *vd; void *varctx; + + if (STRCMP(var, ==, "@exit")) + { *ctx_out = r; return (&exit_var); } + else if (STRCMP(var, ==, "@progress")) + { *ctx_out = r; return (&progress_var); } + else if (STRCMP(var, ==, "@reconnect")) + { *ctx_out = r; return (&reconn_var); } + else if (subenv && subenv->findvar) { + vd = subenv->findvar(tv, var, &varctx, r->subctx); + if (!vd) return (0); + r->vd.regsz = vd->regsz; r->vd.setvar = setvar_remote; + r->vd.def = vd->def; + *ctx_out = r; return (&r->vd); + } else + return (0); +} + +/* --- @tvec_remotebefore@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @void *ctx@ = context pointer + * + * Returns: --- + * + * Use: Invoke the subordinate environment's @before@ function. + */ + +void tvec_remotebefore(struct tvec_state *tv, void *ctx) +{ + struct tvec_remotectx *r = ctx; + const struct tvec_remoteenv *re = r->re; + const struct tvec_env *subenv = re->r.env; + + if (subenv && subenv->before) subenv->before(tv, r->subctx); +} + +/* --- @tvec_remoterun@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @tvec_testfn *fn@ = test function to run + * @void *ctx@ = context pointer for the test function + * + * Returns: --- + * + * Use: Run a test on a remote server. + */ + +void tvec_remoterun(struct tvec_state *tv, tvec_testfn *fn, void *ctx) +{ + struct tvec_remotectx *r = ctx; + union tvec_regval rv; + unsigned f = 0; +#define f_exit 1u +#define f_progress 2u +#define f_fail 4u + buf b; + int rc; + + /* Reconnect to the server according to policy. */ + switch (try_reconnect(tv, r)) { + case CONN_FAILED: + tvec_skip(tv, "failed to connect to test backend"); return; + case CONN_BROKEN: + tvec_skip(tv, "no connection"); return; + } + + /* Set initial progress state. */ + DRESET(&r->progress); DPUTS(&r->progress, "%IDLE"); + + /* Send the command to the server and handle output. */ + QUEUEPK(tv, &r->rc, QF_FORCE, TVPK_TEST) + tvec_serialize(tv->in, DBUF_BUF(&r->rc.bout), + tv->test->regs, tv->nreg, tv->regsz); + else { goto fail; } + rc = handle_packets(tv, r, RCVF_ALLOWEOF, TVPK_TEST | TVPF_ACK, &b); + + /* Deal with the outcome. */ + switch (rc) { + + case RECV_FAIL: + /* Some kind of error. Abandon ship. */ + + fail: + tvec_skip(tv, "remote test runner communications failed"); + disconnect_remote(tv, r, 0); + break; + + case RECV_EOF: + /* End-of-file at a packet boundary. The server crashed trying to run + * our test. Collect the exit status and continue. + */ + reap_kid(tv, r); + /* fall through */ + + case RECV_OK: + /* Successful completion (or EOF). */ + + /* Notice if the exit status isn't right. */ + if (r->exit != r->exwant) f |= f_exit; + + /* Notice if the progress token isn't right. */ + if (r->progress.len != r->prgwant.len || + MEMCMP(r->progress.buf, !=, r->prgwant.buf, r->progress.len)) + f |= f_progress; + + /* If we found something wrong but the test is passing so far, then + * report the failure and dump the input registers. + */ + if (f && (tv->f&TVSF_ACTIVE)) + { tvec_fail(tv, 0); tvec_mismatch(tv, TVMF_IN); } + + /* If the test failed, then report the exit and progress states + * relative to their expectations. + */ + if (!(tv->f&TVSF_ACTIVE) && + (tv->f&TVSF_OUTMASK) == (TVOUT_LOSE << TVSF_OUTSHIFT)) { + + /* Note here that the test failed. */ + f |= f_fail; + + /* Report exit status. */ + rv.u = r->exit; + tvec_dumpreg(tv, f&f_exit ? TVRD_FOUND : TVRD_MATCH, + &rv, &exit_var.def); + if (f&f_exit) { + rv.u = r->exwant; + tvec_dumpreg(tv, TVRD_EXPECT, &rv, &exit_var.def); + } + + /* Report progress token. */ + rv.text.p = r->progress.buf; rv.text.sz = r->progress.len; + tvec_dumpreg(tv, f&f_progress ? TVRD_FOUND : TVRD_MATCH, + &rv, &progress_var.def); + if (f&f_progress) { + rv.text.p = r->prgwant.buf; rv.text.sz = r->prgwant.len; + tvec_dumpreg(tv, TVRD_EXPECT, &rv, &progress_var.def); + } + } + + /* If we received end-of-file, then close the connection. Suppress + * error output if the test passed: it was presumably expected. + */ + if (rc == RECV_EOF) + disconnect_remote(tv, r, f ? 0 : ERF_SILENT); + break; + } + +#undef f_exit +#undef f_progress +#undef f_fail +} + +/* --- @tvec_remoteafter@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @void *ctx@ = context pointer + * + * Returns: --- + * + * Use: Reset variables to their default values. + */ + +void tvec_remoteafter(struct tvec_state *tv, void *ctx) +{ + struct tvec_remotectx *r = ctx; + const struct tvec_remoteenv *re = r->re; + const struct tvec_env *subenv = re->r.env; + + reset_vars(r); + if (subenv && subenv->after) subenv->after(tv, r->subctx); +} + +/* --- @tvec_remoteteardown@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @void *ctx@ = context pointer + * + * Returns: --- + * + * Use: Tear down the remote environment. + */ + +void tvec_remoteteardown(struct tvec_state *tv, void *ctx) +{ + struct tvec_remotectx *r = ctx; + const struct tvec_remoteenv *re = r->re; + const struct tvec_env *subenv = re->r.env; + buf b; + + if (subenv && subenv->teardown) subenv->teardown(tv, r->subctx); + xfree(r->subctx); + if (r->rc.outfd >= 0) { + QUEUEPK(tv, &r->rc, QF_FORCE, TVPK_EGROUP); + if (!handle_packets(tv, r, RCVF_ALLOWEOF, TVPK_EGROUP | TVPF_ACK, &b)) + if (BLEFT(&b)) malformed(tv, &r->rc); + } + disconnect_remote(tv, r, 0); release_comms(&r->rc); + DDESTROY(&r->prgwant); DDESTROY(&r->progress); +} + +/*----- Connectors --------------------------------------------------------*/ + +/* --- @fork_common@ --- * + * + * Arguments: @pid_t *kid_out@ = where to put child process-id + * @int *infd_out, *outfd_out, *errfd_out@ = where to put file + * descriptors + * @struct tvec_state *tv@ = test vector state + * + * Returns: Zero on success, %$-1$% on failure. + * + * Use: Common @fork@ machinery for the connectors. Create a + * subprocess. On successful return, in the subprocess, + * @*kid_out@ is zero, and the error descriptor replaces the + * standard-error descriptor; in the parent, @*kid_out@ is the + * child process-id, and @*errfd_out@ is a descriptor on which + * the child's standard-error output can be read; in both + * @*infd_out@ and @*outfd_out@ are descriptors for input and + * output respectively -- they're opposite ends of pipes, but + * obviously they're crossed over so that the parent's output + * matches the child's input and %%\emph{vice versa}%%. + */ + +static int fork_common(pid_t *kid_out, int *infd_out, int *outfd_out, + int *errfd_out, struct tvec_state *tv) +{ + int p0[2] = { -1, -1 }, p1[2] = { -1, -1 }, pe[2] = { -1, -1 }; + pid_t kid = -1; + int rc; + + /* Try to create the pipes. */ + if (pipe(p0) || pipe(p1) || pipe(pe) || + fdflags(p0[1], 0, 0, FD_CLOEXEC, FD_CLOEXEC) || + fdflags(p1[0], 0, 0, FD_CLOEXEC, FD_CLOEXEC) || + fdflags(pe[0], 0, 0, FD_CLOEXEC, FD_CLOEXEC)) { + tvec_error(tv, "pipe failed: %s", strerror(errno)); + rc = -1; goto end; + } + + /* Flush all of the stream buffers so that we don't get duplicated + * output. + */ + fflush(0); + + /* Try to set up the child process. */ + kid = fork(); + if (kid < 0) { + tvec_error(tv, "fork failed: %s", strerror(errno)); + rc = -1; goto end; + } + + if (!kid) { + /* Child process. */ + + *kid_out = 0; + *infd_out = p0[0]; p0[0] = -1; + *outfd_out = p1[1]; p1[1] = -1; + if (pe[1] != STDERR_FILENO && dup2(pe[1], STDERR_FILENO) < 0) { + fprintf(stderr, "failed to establish child stderr: %s", + strerror(errno)); + _exit(127); + } + } else { + /* Parent process. */ + + *kid_out = kid; kid = -1; + *infd_out = p1[0]; p1[0] = -1; + *outfd_out = p0[1]; p0[1] = -1; + *errfd_out = pe[0]; pe[0] = -1; + } + + /* All done. */ + rc = 0; + +end: + /* Clean up. So much of this... */ + if (p0[0] >= 0) close(p0[0]); + if (p0[1] >= 0) close(p0[1]); + if (p1[0] >= 0) close(p1[0]); + if (p1[1] >= 0) close(p1[1]); + if (pe[0] >= 0) close(pe[0]); + if (pe[1] >= 0) close(pe[1]); + return (rc); +} + +/* --- @tvec_fork@ --- * + * + * Arguments: @pid_t *kid_out@ = where to put child process-id + * @int *infd_out, *outfd_out, *errfd_out@ = where to put file + * descriptors + * @struct tvec_state *tv@ = test vector state + * @const struct tvec_remoteenv@ = the remote environment + * + * Returns: Zero on success, %$-1$% on failure. + * + * Use: Starts a remote server running in a fork of the main + * process. This is useful for testing functions which might -- + * or are even intended to -- crash. + */ + +int tvec_fork(pid_t *kid_out, int *infd_out, int *outfd_out, int *errfd_out, + struct tvec_state *tv, const struct tvec_remoteenv *env) +{ + struct tvec_config config; + const struct tvec_remotefork *rf = (const struct tvec_remotefork *)env; + pid_t kid = -1; + int infd = -1, outfd = -1, errfd = -1; + int rc; + + if (fork_common(&kid, &infd, &outfd, &errfd, tv)) { rc = -1; goto end; } + if (!kid) { + if (tv->fp) fclose(tv->fp); + config.tests = rf->f.tests ? rf->f.tests : tv->tests; + config.nrout = tv->nrout; config.nreg = tv->nreg; + config.regsz = tv->regsz; + _exit(tvec_remoteserver(infd, outfd, &config)); + } + + *kid_out = kid; *infd_out = infd; *outfd_out = outfd; *errfd_out = errfd; + rc = 0; +end: + return (rc); +} + +/* --- @tvec_exec@ --- * + * + * Arguments: @pid_t *kid_out@ = where to put child process-id + * @int *infd_out, *outfd_out, *errfd_out@ = where to put file + * descriptors + * @struct tvec_state *tv@ = test vector state + * @const struct tvec_remoteenv@ = the remote environment + * + * Returns: Zero on success, %$-1$% on failure. + * + * Use: Starts a remote server by running some program. The command + * given in the environment description will probably some hairy + * shell rune allowing for configuration via files or + * environment variables. + */ + +int tvec_exec(pid_t *kid_out, int *infd_out, int *outfd_out, int *errfd_out, + struct tvec_state *tv, const struct tvec_remoteenv *env) +{ + const struct tvec_remoteexec *rx = (const struct tvec_remoteexec *)env; + pid_t kid = -1; + int infd = -1, outfd = -1, errfd = -1; + mdup_fd v[2]; + int rc; + + if (fork_common(&kid, &infd, &outfd, &errfd, tv)) { rc = -1; goto end; } + if (!kid) { + v[0].cur = infd; v[0].want = STDIN_FILENO; + v[1].cur = outfd; v[1].want = STDOUT_FILENO; + if (mdup(v, 2)) { + fprintf(stderr, "failed to establish standard file descriptors: %s", + strerror(errno)); + exit(127); + } + execvp(rx->x.args[0], (/*uncosnt*/ char *const *)rx->x.args); + fprintf(stderr, "failed to invoke test runner: %s", strerror(errno)); + exit(127); + } + + *kid_out = kid; *infd_out = infd; *outfd_out = outfd; *errfd_out = errfd; + rc = 0; +end: + return (rc); +} /*----- That's all, folks -------------------------------------------------*/