X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/mLib/blobdiff_plain/31d0247cc58abc0b0720aa7e9972011c5a66995c..c81c35dfd10050ffef85d57dc2ad73f52f38a3f2:/test/tvec-remote.c?ds=sidebyside diff --git a/test/tvec-remote.c b/test/tvec-remote.c index 62dd150..052ce13 100644 --- a/test/tvec-remote.c +++ b/test/tvec-remote.c @@ -99,8 +99,13 @@ static void init_comms(struct tvec_remotecomms *rc) static void close_comms(struct tvec_remotecomms *rc) { - if (rc->infd >= 0) { close(rc->infd); rc->infd = -1; } - if (rc->outfd >= 0) { close(rc->outfd); rc->outfd = -1; } + 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@ --- * @@ -203,7 +208,7 @@ end: * @size_t min@ = minimum acceptable size to read * @size_t *n_out@ = size read * - * Returns: An @RECV_...@ code. + * 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 @@ -239,7 +244,7 @@ static int recv_all(struct tvec_state *tv, struct tvec_remotecomms *rc, p += n; sz -= n; tot += n; if (tot >= min) break; } else if (!n && !tot && (f&RCVF_ALLOWEOF)) - return (RECV_EOF); + { rc->f |= TVRF_BROKEN; return (RECV_EOF); } else return (ioerr(tv, rc, "failed to receive: %s", n ? strerror(errno) : "unexpected end-of-file")); @@ -326,7 +331,7 @@ end: * @unsigned f@ = flags (@RCVF_...@) * @size_t want@ = data block size required * - * Returns: An @RECV_...@ code. + * Returns: A @RECV_...@ code. * * Use: Reads a block of data from the input descriptor into the * input buffer. @@ -403,7 +408,7 @@ static int receive_buffered(struct tvec_state *tv, * @unsigned f@ = flags (@RCVF_...@) * @buf *b_out@ = buffer to establish around the packet contents * - * Returns: An @RECV_...@ code. + * Returns: A @RECV_...@ code. * * Use: Receive a packet into the input buffer @rc->bin@ and * establish @*b_out@ to read from it. @@ -780,6 +785,63 @@ bad: /*----- 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) + { 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"); } + +/* --- @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) { @@ -787,6 +849,51 @@ static void remote_skipgroup(struct tvec_output *o, dbuf_vputstrf16l(&srvrc.bout, excuse, ap); } +/* --- @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. + */ + +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) { @@ -806,6 +913,22 @@ static void remote_fail(struct tvec_output *o, } } +/* --- @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) @@ -831,6 +954,34 @@ found: } } +/* --- @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) { @@ -840,6 +991,20 @@ static void remote_bbench(struct tvec_output *o, } } +/* --- @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) @@ -858,6 +1023,23 @@ static void remote_ebench(struct tvec_output *o, } } +/* --- @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) { @@ -871,21 +1053,19 @@ static void remote_report(struct tvec_output *o, unsigned level, } } -static void remote_bsession(struct tvec_output *o, struct tvec_state *tv) - { ; } -static int remote_esession(struct tvec_output *o) - { return (srvtv.f&TVSF_ERROR ? 2 : 0); } +/* --- @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 void remote_etest(struct tvec_output *o, unsigned outcome) - { ; } - -static void remote_bgroup(struct tvec_output *o) - { assert(!"remote_bgroup"); } -static void remote_egroup(struct tvec_output *o) - { assert(!"remote_egroup"); } -static void remote_btest(struct tvec_output *o) - { assert(!"remote_btest"); } static const struct tvec_outops remote_ops = { remote_bsession, remote_esession, @@ -896,21 +1076,20 @@ static const struct tvec_outops remote_ops = { remote_destroy }; -/*----- Client ------------------------------------------------------------*/ - -#define TVXF_VALMASK 0x0fffu -#define TVXF_SIG 0x1000u -#define TVXF_CAUSEMASK 0xe000u -#define TVXST_RUN 0x0000u -#define TVXST_EXIT 0x2000u -#define TVXST_KILL 0x4000u -#define TVXST_CONT 0x6000u -#define TVXST_STOP 0x8000u -#define TVXST_DISCONN 0xa000u -#define TVXST_UNK 0xc000u -#define TVXST_ERR 0xe000u +/*----- Pseudoregister definitions ----------------------------------------*/ 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 @@ -1060,17 +1239,9 @@ static const struct tvec_flag exit_flags[] = { #endif /***END***/ + /* This should be folded into the signal entries above. */ { "signal", TVXF_SIG, TVXF_SIG }, - { "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 }, - TVEC_ENDFLAGS }; @@ -1079,8 +1250,12 @@ static const struct tvec_flaginfo exit_flaginfo = static const struct tvec_regdef exit_regdef = { "@exit", 0, &tvty_flags, 0, { &exit_flaginfo } }; +/* Progress. */ + static const struct tvec_regdef progress_regdef = - { "@progress", 0, &tvty_string, 0 }; + { "@progress", 0, &tvty_text, 0 }; + +/* Reconnection. */ static const struct tvec_uassoc reconn_assocs[] = { { "on-demand", TVRCN_DEMAND }, @@ -1089,6 +1264,14 @@ static const struct tvec_uassoc reconn_assocs[] = { TVEC_ENDENUM }; +static const struct tvec_uenuminfo reconn_enuminfo = + { "remote-reconnection", reconn_assocs, &tvrange_uint }; +static const struct tvec_regdef reconn_regdef = + { "@reconnect", 0, &tvty_uenum, 0, { &reconn_enuminfo } }; + +/*----- Client ------------------------------------------------------------*/ + +/* Connection state. */ enum { CONN_BROKEN = -2, /* previously broken */ CONN_FAILED = -1, /* attempt freshly failed */ @@ -1096,10 +1279,35 @@ enum { CONN_FRESH = 1 /* freshly connected */ }; -static const struct tvec_uenuminfo reconn_enuminfo = - { "remote-reconnection", reconn_assocs, &tvrange_uint }; -static const struct tvec_regdef reconn_regdef = - { "@reconnect", 0, &tvty_uenum, 0, { &reconn_enuminfo } }; +/* --- @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) @@ -1116,13 +1324,21 @@ static int handle_packets(struct tvec_state *tv, struct tvec_remotectx *r, int rc; for (;;) { - rc = remote_recv(tv, &r->rc, f, b); if (rc) goto end; + + /* 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) break; + 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; @@ -1130,6 +1346,8 @@ static int handle_packets(struct tvec_state *tv, struct tvec_remotectx *r, 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; @@ -1139,14 +1357,22 @@ static int handle_packets(struct tvec_state *tv, struct tvec_remotectx *r, 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; - DRESET(&d); DPUTM(&d, p, n); DPUTZ(&d); 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; @@ -1160,6 +1386,10 @@ static int handle_packets(struct tvec_state *tv, struct tvec_remotectx *r, 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", @@ -1181,6 +1411,9 @@ static int handle_packets(struct tvec_state *tv, struct tvec_remotectx *r, 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; @@ -1194,6 +1427,9 @@ static int handle_packets(struct tvec_state *tv, struct tvec_remotectx *r, 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); @@ -1209,6 +1445,8 @@ static int handle_packets(struct tvec_state *tv, struct tvec_remotectx *r, 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; @@ -1222,6 +1460,8 @@ static int handle_packets(struct tvec_state *tv, struct tvec_remotectx *r, 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) { @@ -1238,12 +1478,13 @@ static int handle_packets(struct tvec_state *tv, struct tvec_remotectx *r, break; default: + /* Something else. This is unexpected. */ + rc = ioerr(tv, &r->rc, "unexpected packet type 0x%04x", pk); goto end; } } - rc = RECV_OK; end: DDESTROY(&d); xfree(reg); @@ -1252,11 +1493,36 @@ 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) { @@ -1285,6 +1551,20 @@ static void reap_kid(struct tvec_state *tv, struct tvec_remotectx *r) } } +/* --- @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; @@ -1294,6 +1574,29 @@ static void report_errline(char *p, size_t n, void *ctx) 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, @@ -1303,6 +1606,10 @@ static int drain_errfd(struct tvec_state *tv, struct tvec_remotectx *r, 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; @@ -1312,6 +1619,7 @@ static int drain_errfd(struct tvec_state *tv, struct tvec_remotectx *r, 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); @@ -1326,6 +1634,8 @@ static int drain_errfd(struct tvec_state *tv, struct tvec_remotectx *r, } lbuf_flush(&r->errbuf, p, n); } + + /* Done. */ rc = 0; end: if (f&ERF_CLOSE) { @@ -1335,6 +1645,24 @@ end: 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) @@ -1344,6 +1672,16 @@ static void disconnect_remote(struct tvec_state *tv, 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; @@ -1352,19 +1690,26 @@ static int connect_remote(struct tvec_state *tv, struct tvec_remotectx *r) uint16 v; int infd = -1, outfd = -1, errfd = -1, rc; - DRESET(&r->progress); DPUTS(&r->progress, "%INIT"); + /* 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; @@ -1373,14 +1718,18 @@ static int connect_remote(struct tvec_state *tv, struct tvec_remotectx *r) 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; } - r->ver = v; rc = 0; + + /* Done. */ + rc = 0; end: if (rc) disconnect_remote(tv, r, DCF_KILL); return (rc); @@ -1388,6 +1737,19 @@ 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) @@ -1398,6 +1760,17 @@ static int check_comms(struct tvec_state *tv, struct tvec_remotectx *r) 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; @@ -1425,6 +1798,17 @@ static int try_reconnect(struct tvec_state *tv, struct tvec_remotectx *r) 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) { r->exwant = TVXST_RUN; @@ -1432,6 +1816,22 @@ static void reset_vars(struct tvec_remotectx *r) 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) { @@ -1449,6 +1849,29 @@ void tvec_remotesetup(struct tvec_state *tv, const struct tvec_env *env, reset_vars(r); } +/* --- @tvec_remoteset@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @const char *var@ = variable name to set + * @void *ctx@ = context pointer + * + * Returns: %$+1$% on success, %$0$% if the variable name was not + * recognized, or %$-1$% on any other 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_...@. + */ + int tvec_remoteset(struct tvec_state *tv, const char *var, void *ctx) { struct tvec_remotectx *r = ctx; @@ -1461,13 +1884,13 @@ int tvec_remoteset(struct tvec_state *tv, const char *var, void *ctx) r->exwant = rv.u; r->rc.f |= TVRF_SETEXIT; rc = 1; } else if (STRCMP(var, ==, "@progress")) { if (r->rc.f&TVRF_SETPRG) { rc = tvec_dupreg(tv, var); goto end; } - tvty_string.init(&rv, &progress_regdef); - rc = tvty_string.parse(&rv, &progress_regdef, tv); + tvty_text.init(&rv, &progress_regdef); + rc = tvty_text.parse(&rv, &progress_regdef, tv); if (!rc) { - DRESET(&r->prgwant); DPUTM(&r->prgwant, rv.str.p, rv.str.sz); + DRESET(&r->prgwant); DPUTM(&r->prgwant, rv.text.p, rv.text.sz); r->rc.f |= TVRF_SETPRG; } - tvty_string.release(&rv, &progress_regdef); + tvty_text.release(&rv, &progress_regdef); if (rc) { rc = -1; goto end; } rc = 1; } else if (STRCMP(var, ==, "@reconnect")) { @@ -1482,12 +1905,16 @@ end: return (rc); } -void tvec_remoteafter(struct tvec_state *tv, void *ctx) -{ - struct tvec_remotectx *r = ctx; - - reset_vars(r); -} +/* --- @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) { @@ -1500,6 +1927,7 @@ void tvec_remoterun(struct tvec_state *tv, tvec_testfn *fn, void *ctx) 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; @@ -1507,29 +1935,61 @@ void tvec_remoterun(struct tvec_state *tv, tvec_testfn *fn, void *ctx) 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 { rc = -1; goto end; } + else { goto fail; } rc = handle_packets(tv, r, RCVF_ALLOWEOF, TVPK_TEST | TVPF_ACK, &b); + + /* Deal with the outcome. */ switch (rc) { + case RECV_FAIL: - goto end; + /* 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_regdef); @@ -1538,32 +1998,56 @@ void tvec_remoterun(struct tvec_state *tv, tvec_testfn *fn, void *ctx) tvec_dumpreg(tv, TVRD_EXPECT, &rv, &exit_regdef); } - rv.str.p = r->progress.buf; rv.str.sz = r->progress.len; + /* 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_regdef); if (f&f_progress) { - rv.str.p = r->prgwant.buf; rv.str.sz = r->prgwant.len; + rv.text.p = r->prgwant.buf; rv.text.sz = r->prgwant.len; tvec_dumpreg(tv, TVRD_EXPECT, &rv, &progress_regdef); } } + /* 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; } -end: - if (rc) { - if ((tv->f&TVSF_ACTIVE) && f) - tvec_skip(tv, "remote test runner communications failed"); - disconnect_remote(tv, r, 0); - } - #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; + + reset_vars(r); +} + +/* --- @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; @@ -1580,6 +2064,27 @@ void tvec_remoteteardown(struct tvec_state *tv, void *ctx) /*----- 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) { @@ -1587,6 +2092,7 @@ static int fork_common(pid_t *kid_out, int *infd_out, int *outfd_out, 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) || @@ -1595,8 +2101,12 @@ static int fork_common(pid_t *kid_out, int *infd_out, int *outfd_out, 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)); @@ -1604,23 +2114,30 @@ static int fork_common(pid_t *kid_out, int *infd_out, int *outfd_out, } 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); + _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]); @@ -1630,6 +2147,21 @@ end: 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) { @@ -1654,6 +2186,22 @@ 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) {