@@@ tvec doc wip
[mLib] / test / tvec-remote.c
index 62dd150..052ce13 100644 (file)
@@ -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)
 {