@@@ tvec doc wip
[mLib] / test / tvec-output.c
index af120d9..8c9c214 100644 (file)
 
 /*----- Header files ------------------------------------------------------*/
 
+#include "config.h"
+
 #include <assert.h>
+#include <ctype.h>
 #include <stdarg.h>
 #include <stdio.h>
 #include <string.h>
 #include "alloc.h"
 #include "bench.h"
 #include "dstr.h"
+#include "macros.h"
 #include "quis.h"
 #include "report.h"
 #include "tvec.h"
 
 /*----- Common machinery --------------------------------------------------*/
 
-enum { INPUT, OUTPUT, MATCH, EXPECT, FOUND };
-struct mismatchfns {
-  void (*report_status)(unsigned /*disp*/, int /*st*/,
-                       struct tvec_state */*tv*/);
-  void (*report_register)(unsigned /*disp*/,
-                         const struct tvec_reg */*r*/,
-                         const struct tvec_regdef */*rd*/,
-                         struct tvec_state */*tv*/);
-};
-
-static const char *stdisp(unsigned disp)
-{
-  switch (disp) {
-    case MATCH: return "final";
-    case EXPECT: return "expected";
-    case FOUND: return "actual";
-    default: abort();
-  }
-}
+/* --- @regdisp@ --- *
+ *
+ * Arguments:  @unsigned disp@ = a @TVRD_...@ disposition code
+ *
+ * Returns:    A human-readable adjective describing the register
+ *             disposition.
+ */
 
 static const char *regdisp(unsigned disp)
 {
   switch (disp) {
-    case INPUT: return "input";
-    case OUTPUT: return "output";
-    case MATCH: return "matched";
-    case EXPECT: return "expected";
-    case FOUND: return "computed";
+    case TVRD_INPUT: return "input";
+    case TVRD_OUTPUT: return "output";
+    case TVRD_MATCH: return "matched";
+    case TVRD_EXPECT: return "expected";
+    case TVRD_FOUND: return "found";
     default: abort();
   }
 }
 
+/* --- @getenv_boolean@ --- *
+ *
+ * Arguments:  @const char *var@ = environment variable name
+ *             @int dflt@ = default value
+ *
+ * Returns:    @0@ if the variable is set to something falseish, @1@ if it's
+ *             set to something truish, or @dflt@ otherwise.
+ */
+
 static int getenv_boolean(const char *var, int dflt)
 {
   const char *p;
@@ -88,191 +89,351 @@ static int getenv_boolean(const char *var, int dflt)
           STRCMP(p, ==, "1"))
     return (1);
   else if (STRCMP(p, ==, "n") || STRCMP(p, ==, "no") ||
+          STRCMP(p, ==, "f") || STRCMP(p, ==, "false") ||
           STRCMP(p, ==, "nil") || STRCMP(p, ==, "off") ||
           STRCMP(p, ==, "0"))
     return (0);
   else {
-    moan("unexpected value `%s' for boolean environment variable `%s'",
+    moan("ignoring unexpected value `%s' for environment variable `%s'",
         var, p);
     return (dflt);
   }
 }
 
-static void basic_report_status(unsigned disp, int st, struct tvec_state *tv)
-  { tvec_write(tv, "  %8s status = `%c'\n", stdisp(disp), st); }
+/* --- @register_maxnamelen@ --- *
+ *
+ * Arguments:  @const struct tvec_state *tv@ = test vector state
+ *
+ * Returns:    The maximum length of a register name in the current test.
+ */
 
-static void basic_report_register(unsigned disp,
-                                 const struct tvec_reg *r,
-                                 const struct tvec_regdef *rd,
-                                 struct tvec_state *tv)
+static int register_maxnamelen(const struct tvec_state *tv)
 {
-  tvec_write(tv, "  %8s %s = ", regdisp(disp), rd->name);
-  if (r->f&TVRF_LIVE) rd->ty->dump(&r->v, rd, tv, 0);
-  else tvec_write(tv, "#<unset>");
-  tvec_write(tv, "\n");
+  const struct tvec_regdef *rd;
+  int maxlen = 10, n;
+
+  for (rd = tv->test->regs; rd->name; rd++)
+    { n = strlen(rd->name); if (n > maxlen) maxlen = n; }
+  return (maxlen);
 }
 
-static const struct mismatchfns basic_mismatchfns =
-  { basic_report_status, basic_report_register };
+/*----- Output layout -----------------------------------------------------*/
+
+/* We have two main jobs in output layout: trimming trailing blanks; and
+ * adding a prefix to each line.
+ *
+ * This is somehow much more complicated than it ought to be.
+ */
 
-static void dump_inreg(const struct tvec_regdef *rd,
-                      const struct mismatchfns *fns, struct tvec_state *tv)
-  { fns->report_register(INPUT, TVEC_REG(tv, in, rd->i), rd, tv); }
+struct layout {
+  FILE *fp;                             /* output file */
+  const char *prefix, *pfxtail, *pfxlim; /* prefix pointers */
+  dstr w;                              /* trailing whitespace */
+  unsigned f;                          /* flags */
+#define LYTF_NEWL 1u                   /*   start of output line */
+};
 
-static void dump_outreg(const struct tvec_regdef *rd,
-                       const struct mismatchfns *fns, struct tvec_state *tv)
+/* Support macros.  These assume `lyt' is defined as a pointer to the `struct
+ * layout' state.
+ */
+
+#define SPLIT_RANGE(tail, base, limit) do {                            \
+  /* Set TAIL to point just after the last nonspace character between  \
+   * BASE and LIMIT.  If there are no nonspace characters, then set    \
+   * TAIL to equal BASE.                                               \
+   */                                                                  \
+                                                                       \
+  for (tail = limit; tail > base && ISSPACE(tail[-1]); tail--);                \
+} while (0)
+
+#define PUT_RANGE(base, limit) do {                                    \
+  /* Write the range of characters between BASE and LIMIT to the output \
+   * file.  Return immediately on error.                               \
+   */                                                                  \
+                                                                       \
+  size_t _n = limit - base;                                            \
+  if (_n && fwrite(base, 1, _n, lyt->fp) < _n) return (-1);            \
+} while (0)
+
+#define PUT_CHAR(ch) do {                                              \
+  /* Write CH to the output. Return immediately on error. */           \
+                                                                       \
+  if (putc(ch, lyt->fp) == EOF) return (-1);                           \
+} while (0)
+
+#define PUT_PREFIX do {                                                        \
+  /* Output the prefix, if there is one.  Return immediately on error. */ \
+                                                                       \
+  if (lyt->prefix) PUT_RANGE(lyt->prefix, lyt->pfxlim);                        \
+} while (0)
+
+#define PUT_SAVED do {                                                 \
+  /* Output the saved trailing blank material in the buffer. */                \
+                                                                       \
+  size_t _n = lyt->w.len;                                              \
+  if (_n && fwrite(lyt->w.buf, 1, _n, lyt->fp) < _n) return (-1);      \
+} while (0)
+
+#define PUT_PFXINB do {                                                        \
+  /* Output the initial nonblank portion of the prefix, if there is    \
+   * one.  Return immediately on error.                                        \
+   */                                                                  \
+                                                                       \
+  if (lyt->prefix) PUT_RANGE(lyt->prefix, lyt->pfxtail);               \
+} while (0)
+
+#define SAVE_PFXTAIL do {                                              \
+  /* Save the trailing blank portion of the prefix. */                 \
+                                                                       \
+  if (lyt->prefix)                                                     \
+    DPUTM(&lyt->w, lyt->pfxtail, lyt->pfxlim - lyt->pfxtail);          \
+} while (0)
+
+/* --- @set_layout_prefix@ --- *
+ *
+ * Arguments:  @struct layout *lyt@ = layout state
+ *             @const char *prefix@ = new prefix string or null
+ *
+ * Returns:    ---
+ *
+ * Use:                Change the configured prefix string.  The change takes effect
+ *             at the start of the next line (or the current line if it's
+ *             empty or only whitespace so far).
+ */
+
+static void set_layout_prefix(struct layout *lyt, const char *prefix)
 {
-  const struct tvec_reg
-    *rin = TVEC_REG(tv, in, rd->i), *rout = TVEC_REG(tv, out, rd->i);
+  const char *q, *l;
 
-  if (tv->st == '.') {
-    if (!(rout->f&TVRF_LIVE)) {
-      if (!(rin->f&TVRF_LIVE))
-       fns->report_register(INPUT, rin, rd, tv);
-      else {
-       fns->report_register(FOUND, rout, rd, tv);
-       fns->report_register(EXPECT, rin, rd, tv);
-      }
-    } else {
-      if (!(rin->f&TVRF_LIVE)) fns->report_register(OUTPUT, rout, rd, tv);
-      else if (rd->ty->eq(&rin->v, &rout->v, rd))
-       fns->report_register(MATCH, rin, rd, tv);
-      else {
-       fns->report_register(FOUND, rout, rd, tv);
-       fns->report_register(EXPECT, rin, rd, tv);
-      }
-    }
+ if (!prefix || !*prefix)
+    lyt->prefix = lyt->pfxtail = lyt->pfxlim = 0;
+  else {
+    lyt->prefix = prefix;
+    l = lyt->pfxlim = prefix + strlen(prefix);
+    SPLIT_RANGE(q, prefix, l); lyt->pfxtail = q;
   }
 }
 
-static void mismatch(const struct mismatchfns *fns, struct tvec_state *tv)
+/* --- @init_layout@ --- *
+ *
+ * Arguments:  @struct layout *lyt@ = layout state to initialize
+ *             @FILE *fp@ = output file
+ *             @const char *prefix@ = prefix string (or null if empty)
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize a layout state.
+ */
+
+static void init_layout(struct layout *lyt, FILE *fp, const char *prefix)
 {
-  const struct tvec_regdef *rd;
+  lyt->fp = fp;
+  lyt->f = LYTF_NEWL;
+  dstr_create(&lyt->w);
+  set_layout_prefix(lyt, prefix);
+}
 
-  if (tv->st != tv->expst) {
-    fns->report_status(FOUND, tv->st, tv);
-    fns->report_status(EXPECT, tv->expst, tv);
-  } else if (tv->st != '.')
-    fns->report_status(MATCH, tv->st, tv);
+/* --- @destroy_layout@ --- *
+ *
+ * Arguments:  @struct layout *lyt@ = layout state
+ *             @unsigned f@ = flags (@DLF_...@)
+ *
+ * Returns:    ---
+ *
+ * Use:                Releases a layout state and the resources it holds.
+ *             Close the file if @DLF_CLOSE@ is set in @f@; otherwise leave
+ *             it open (in case it's @stderr@ or something).
+ */
 
-  for (rd = tv->test->regs; rd->name; rd++) {
-    if (rd->i < tv->nrout) dump_outreg(rd, fns, tv);
-    else dump_inreg(rd, fns, tv);
-  }
+#define DLF_CLOSE 1u
+static void destroy_layout(struct layout *lyt, unsigned f)
+{
+  if (f&DLF_CLOSE) fclose(lyt->fp);
+  dstr_destroy(&lyt->w);
 }
 
-static void bench_summary(struct tvec_state *tv)
+/* --- @layout_char@ --- *
+ *
+ * Arguments:  @struct layout *lyt@ = layout state
+ *             @int ch@ = character to write
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Write a single character to the output.
+ */
+
+static int layout_char(struct layout *lyt, int ch)
 {
-  const struct tvec_regdef *rd;
-  unsigned f = 0;
-#define f_any 1u
+  if (ch == '\n') {
+    if (lyt->f&LYTF_NEWL) PUT_PFXINB;
+    PUT_CHAR('\n'); lyt->f |= LYTF_NEWL; DRESET(&lyt->w);
+  } else if (isspace(ch))
+    DPUTC(&lyt->w, ch);
+  else {
+    if (lyt->f&LYTF_NEWL) { PUT_PFXINB; lyt->f &= ~LYTF_NEWL; }
+    PUT_SAVED; PUT_CHAR(ch); DRESET(&lyt->w);
+  }
+  return (0);
+}
 
-  for (rd = tv->test->regs; rd->name; rd++)
-    if (rd->f&TVRF_ID) {
-      if (f&f_any) tvec_write(tv, ", ");
-      else f |= f_any;
-      tvec_write(tv, "%s = ", rd->name);
-      rd->ty->dump(&TVEC_REG(tv, in, rd->i)->v, rd, tv, TVSF_COMPACT);
+/* --- @layout_string@ --- *
+ *
+ * Arguments:  @struct layout *lyt@ = layout state
+ *             @const char *p@ = string to write
+ *             @size_t sz@ = length of string
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Write a string to the output.
+ */
+
+static int layout_string(struct layout *lyt, const char *p, size_t sz)
+{
+  const char *q, *r, *l = p + sz;
+
+  /* This is rather vexing.  There are a small number of jobs to do, but the
+   * logic for deciding which to do when gets rather hairy if, as I've tried
+   * here, one aims to minimize the number of decisions being checked, so
+   * it's worth canning them into macros.
+   *
+   * Here, a `blank' is a whitespace character other than newline.  The input
+   * buffer consists of one or more `segments', each of which consists of:
+   *
+   *   * an initial portion, which is either empty or ends with a nonblank
+   *    character;
+   *
+   *   * a suffix which consists only of blanks; and
+   *
+   *   * an optional newline.
+   *
+   * All segments except the last end with a newline.
+   */
+
+#define SPLIT_SEGMENT do {                                             \
+  /* Determine the bounds of the current segment.  If there is a final \
+   * newline, then q is non-null and points to this newline; otherwise,        \
+   * q is null.  The initial portion of the segment lies between p .. r        \
+   * and the blank suffix lies between r .. q (or r .. l if q is null).        \
+   * This sounds awkward, but the suffix is only relevant if there is  \
+   * no newline.                                                       \
+   */                                                                  \
+                                                                       \
+  q = memchr(p, '\n', l - p); SPLIT_RANGE(r, p, q ? q : l);            \
+} while (0)
+
+#define PUT_NONBLANK do {                                              \
+  /* Output the initial portion of the segment. */                     \
+                                                                       \
+  PUT_RANGE(p, r);                                                     \
+} while (0)
+
+#define PUT_NEWLINE do {                                               \
+  /* Write a newline, and advance to the next segment. */              \
+                                                                       \
+  PUT_CHAR('\n'); p = q + 1;                                           \
+} while (0)
+
+#define SAVE_TAIL do {                                                 \
+  /* Save the trailing blank portion of the segment in the buffer.     \
+   * Assumes that there is no newline, since otherwise the suffix would        \
+   * be omitted.                                                       \
+   */                                                                  \
+                                                                       \
+  DPUTM(&lyt->w, r, l - r);                                            \
+} while (0)
+
+  /* Determine the bounds of the first segment.  Handling this is the most
+   * complicated part of this function.
+   */
+  SPLIT_SEGMENT;
+
+  if (!q) {
+    /* This is the only segment.  We'll handle the whole thing here.
+     *
+     * If there's an initial nonblank portion, then we need to write that
+     * out.  Furthermore, if we're at the start of the line then we'll need
+     * to write the prefix, and if there's saved blank material then we'll
+     * need to write that.  Otherwise, there's only blank stuff, which we
+     * accumulate in the buffer.
+     *
+     * If we're at the start of a line here, then put the prefix followed by
+     * any saved whitespace, and then our initial nonblank portion.  Then
+     * save our new trailing space.
+     */
+
+    if (r > p) {
+      if (lyt->f&LYTF_NEWL) { PUT_PREFIX; lyt->f &= ~LYTF_NEWL; }
+      PUT_SAVED; PUT_NONBLANK; DRESET(&lyt->w);
     }
+    SAVE_TAIL;
+    return (0);
+  }
 
-#undef f_any
-}
+  /* There is at least one more segment, so we know that there'll be a line
+   * to output.
+   */
+  if (r > p) {
+    if (lyt->f&LYTF_NEWL) PUT_PREFIX;
+    PUT_SAVED; PUT_NONBLANK;
+  } else if (lyt->f&LYTF_NEWL)
+    PUT_PFXINB;
+  PUT_NEWLINE; DRESET(&lyt->w);
+  SPLIT_SEGMENT;
+
+  /* Main loop over whole segments with trailing newlines.  For each one, we
+   * know that we're starting at the beginning of a line and there's a final
+   * newline, so we write the initial prefix and drop the trailing blanks.
+   */
+  while (q) {
+    if (r > p) { PUT_PREFIX; PUT_NONBLANK; }
+    else PUT_PFXINB;
+    PUT_NEWLINE;
+    SPLIT_SEGMENT;
+  }
 
-static void normalize(double *x_inout, const char **unit_out, double scale)
-{
-  static const char
-    *const nothing = "",
-    *const big[] = { "k", "M", "G", "T", "P", "E", 0 },
-    *const little[] = { "m", "ยต", "n", "p", "f", "a", 0 };
-  const char *const *u;
-  double x = *x_inout;
+  /* At the end, there's no final newline.  If there's nonblank material,
+   * then we can write the prefix and the nonblank stuff.  Otherwise, stash
+   * the blank stuff (including the trailing blanks of the prefix) and leave
+   * the newline flag set.
+   */
+  if (r > p) { PUT_PREFIX; PUT_NONBLANK; lyt->f &= ~LYTF_NEWL; }
+  else { lyt->f |= LYTF_NEWL; SAVE_PFXTAIL; }
+  SAVE_TAIL;
 
-  if (x < 1)
-    for (u = little, x *= scale; x < 1 && u[1]; u++, x *= scale);
-  else if (x >= scale)
-    for (u = big, x /= scale; x >= scale && u[1]; u++, x /= scale);
-  else
-    u = &nothing;
+#undef SPLIT_SEGMENT
+#undef PUT_NONBLANK
+#undef PUT_NEWLINE
+#undef SAVE_TAIL
 
-  *x_inout = x; *unit_out = *u;
+  return (0);
 }
 
-static void bench_report(struct tvec_state *tv,
-                        const struct bench_timing *tm)
-{
-  const struct tvec_bench *b = tv->test->arg.p;
-  double n = (double)tm->n*b->niter;
-  double x, scale;
-  const char *u, *what, *whats;
-
-  assert(tm->f&BTF_TIMEOK);
+#undef SPLIT_RANGE
+#undef PUT_RANGE
+#undef PUT_PREFIX
+#undef PUT_PFXINB
+#undef PUT_SAVED
+#undef PUT_CHAR
+#undef SAVE_PFXTAIL
 
-  if (b->rbuf == -1) {
-    tvec_write(tv, " -- %.0f iterations ", n);
-    what = "op"; whats = "ops"; scale = 1000;
-  } else {
-    n *= TVEC_REG(tv, in, b->rbuf)->v.bytes.sz;
-    x = n; normalize(&x, &u, 1024); tvec_write(tv, " -- %.3f %sB ", x, u);
-    what = whats = "B"; scale = 1024;
-  }
-  x = tm->t; normalize(&x, &u, 1000);
-  tvec_write(tv, "in %.3f %ss", x, u);
-  if (tm->f&BTF_CYOK) {
-    x = tm->cy; normalize(&x, &u, 1000);
-    tvec_write(tv, " (%.3f %scy)", x, u);
-  }
-  tvec_write(tv, ": ");
-
-  x = n/tm->t; normalize(&x, &u, scale);
-  tvec_write(tv, "%.3f %s%s/s", x, u, whats);
-  x = tm->t/n; normalize(&x, &u, 1000);
-  tvec_write(tv, ", %.3f %ss/%s", x, u, what);
-  if (tm->f&BTF_CYOK) {
-    x = tm->cy/n; normalize(&x, &u, 1000);
-    tvec_write(tv, " (%.3f %scy/%s)", x, u, what);
-  }
-  tvec_write(tv, "\n");
-}
-
-/*----- Skeleton ----------------------------------------------------------*/
-/*
-static void ..._error(struct tvec_output *o, const char *msg, va_list *ap)
-static void ..._notice(struct tvec_output *o, const char *msg, va_list *ap)
-static void ..._write(struct tvec_output *o, const char *p, size_t sz)
-static void ..._bsession(struct tvec_output *o)
-static int ..._esession(struct tvec_output *o)
-static void ..._bgroup(struct tvec_output *o)
-static void ..._egroup(struct tvec_output *o, unsigned outcome)
-static void ..._skipgroup(struct tvec_output *o,
-                         const char *excuse, va_list *ap)
-static void ..._btest(struct tvec_output *o)
-static void ..._skip(struct tvec_output *o, const char *detail, va_list *ap)
-static void ..._fail(struct tvec_output *o, const char *detail, va_list *ap)
-static void ..._mismatch(struct tvec_output *o)
-static void ..._etest(struct tvec_output *o, unsigned outcome)
-static void ..._bbench(struct tvec_output *o)
-static void ..._ebench(struct tvec_output *o, const struct tvec_timing *t)
-static void ..._destroy(struct tvec_output *o)
-
-static const struct tvec_outops ..._ops = {
-  ..._error, ..._notice, ..._write,
-  ..._bsession, ..._esession,
-  ..._bgroup, ..._egroup, ..._skip,
-  ..._btest, ..._skip, ..._fail, ..._mismatch, ..._etest,
-  ..._bbench, ..._ebench,
-  ..._destroy
-};
-*/
 /*----- Human-readable output ---------------------------------------------*/
 
-#define HAF_FGMASK 0x0f
-#define HAF_FGSHIFT 0
-#define HAF_BGMASK 0xf0
-#define HAF_BGSHIFT 4
-#define HAF_FG 256u
-#define HAF_BG 512u
-#define HAF_BOLD 1024u
-#define HCOL_BLACK 0u
+/* Attributes for colour output.  This should be done better, but @terminfo@
+ * is a disaster.
+ *
+ * An attribute byte holds a foreground colour in the low nibble, a
+ * background colour in the next nibble, and some flags in the next few
+ * bits.  A colour is expressed in classic 1-bit-per-channel style, with red,
+ * green, and blue in bits 0, 1, and 2, and a `bright' flag in bit 3.
+ */
+#define HAF_FGMASK 0x0f                        /* foreground colour mask */
+#define HAF_FGSHIFT 0                  /* foreground colour shift */
+#define HAF_BGMASK 0xf0                        /* background colour mask */
+#define HAF_BGSHIFT 4                  /* background colour shift */
+#define HAF_FG 256u                    /* set foreground? */
+#define HAF_BG 512u                    /* set background? */
+#define HAF_BOLD 1024u                 /* set bold? */
+#define HCOL_BLACK 0u                  /* colour codes... */
 #define HCOL_RED 1u
 #define HCOL_GREEN 2u
 #define HCOL_YELLOW 3u
@@ -280,26 +441,65 @@ static const struct tvec_outops ..._ops = {
 #define HCOL_MAGENTA 5u
 #define HCOL_CYAN 6u
 #define HCOL_WHITE 7u
-#define HCF_BRIGHT 8u
-#define HFG(col) (HAF_FG | (HCOL_##col) << HAF_FGSHIFT)
-#define HBG(col) (HAF_BG | (HCOL_##col) << HAF_BGSHIFT)
-
-#define HA_WIN (HFG(GREEN))
-#define HA_LOSE (HFG(RED) | HAF_BOLD)
-#define HA_SKIP (HFG(YELLOW))
+#define HCF_BRIGHT 8u                  /* bright colour flag */
+#define HFG(col) (HAF_FG | (HCOL_##col) << HAF_FGSHIFT) /* set foreground */
+#define HBG(col) (HAF_BG | (HCOL_##col) << HAF_BGSHIFT) /* set background */
+
+/* Predefined attributes. */
+#define HA_PLAIN 0                  /* nothing special: terminal defaults */
+#define HA_LOC (HFG(CYAN))             /* filename or line number */
+#define HA_LOCSEP (HFG(BLUE))          /* location separator `:' */
+#define HA_ERR (HFG(MAGENTA) | HAF_BOLD) /* error messages */
+#define HA_NOTE (HFG(YELLOW))          /* notices */
+#define HA_UNKLEV (HFG(WHITE) | HBG(RED) | HAF_BOLD) /* unknown level */
+#define HA_UNSET (HFG(YELLOW))         /* register not set */
+#define HA_FOUND (HFG(RED))            /* incorrect output value */
+#define HA_EXPECT (HFG(GREEN))         /* what the value should have been */
+#define HA_WIN (HFG(GREEN))            /* reporting success */
+#define HA_LOSE (HFG(RED) | HAF_BOLD)  /* reporting failure */
+#define HA_XFAIL (HFG(BLUE) | HAF_BOLD)        /* reporting expected failure */
+#define HA_SKIP (HFG(YELLOW))          /* reporting a skipped test/group */
+
+/* Scoreboard indicators. */
+#define HSB_WIN '.'                    /* test passed */
+#define HSB_LOSE 'x'                   /* test failed */
+#define HSB_XFAIL 'o'                  /* test failed expectedly */
+#define HSB_SKIP '_'                   /* test wasn't run */
 
 struct human_output {
-  struct tvec_output _o;
-  FILE *fp;
-  dstr scoreboard;
-  unsigned attr;
-  unsigned f;
-#define HOF_TTY 1u
-#define HOF_DUPERR 2u
-#define HOF_COLOUR 4u
-#define HOF_PROGRESS 8u
+  struct tvec_output _o;               /* output base class */
+  struct tvec_state *tv;               /* stashed testing state */
+  struct layout lyt;                   /* output layout */
+  char *outbuf; size_t outsz;          /* buffer for formatted output */
+  dstr scoreboard;                     /* history of test group results */
+  unsigned attr;                       /* current terminal attributes */
+  int maxlen;                          /* longest register name */
+  unsigned f;                          /* flags */
+#define HOF_TTY 1u                     /*   writing to terminal */
+#define HOF_DUPERR 2u                  /*   duplicate errors to stderr */
+#define HOF_COLOUR 4u                  /*   print in angry fruit salad */
+#define HOF_PROGRESS 8u                        /*   progress display is active */
 };
 
+/* --- @set_colour@ --- *
+ *
+ * Arguments:  @FILE *fp@ = output stream to write on
+ *             @int *sep_inout@ = where to maintain separator
+ *             @const char *norm@ = prefix for normal colour
+ *             @const char *bright@ = prefix for bright colour
+ *             @unsigned colour@ = four bit colour code
+ *
+ * Returns:    ---
+ *
+ * Use:                Write to the output stream @fp@, the current character at
+ *             @*sep_inout@, if that's not zero, followed by either @norm@
+ *             or @bright@, according to whether the @HCF_BRIGHT@ flag is
+ *             set in @colour@, followed by the plain colour code from
+ *             @colour@; finally, update @*sep_inout@ to be a `%|;|%'.
+ *
+ *             This is an internal subroutine for @setattr@ below.
+ */
+
 static void set_colour(FILE *fp, int *sep_inout,
                       const char *norm, const char *bright,
                       unsigned colour)
@@ -309,303 +509,531 @@ static void set_colour(FILE *fp, int *sep_inout,
   *sep_inout = ';';
 }
 
+/* --- @setattr@ --- *
+ *
+ * Arguments:  @struct human_output *h@ = output state
+ *             @unsigned attr@ = attribute code to set
+ *
+ * Returns:    ---
+ *
+ * Use:                Send a control sequence to the output stream so that
+ *             subsequent text is printed with the given attributes.
+ *
+ *             Some effort is taken to avoid unnecessary control sequences.
+ *             In particular, if @attr@ matches the current terminal
+ *             settings already, then nothing is written.
+ */
+
 static void setattr(struct human_output *h, unsigned attr)
 {
   unsigned diff = h->attr ^ attr;
   int sep = 0;
 
+  /* If there's nothing to do, we might as well stop now. */
   if (!diff || !(h->f&HOF_COLOUR)) return;
-  fputs("\x1b[", h->fp);
 
+  /* Start on the control command. */
+  fputs("\x1b[", h->lyt.fp);
+
+  /* Change the boldness if necessary. */
   if (diff&HAF_BOLD) {
-    if (attr&HAF_BOLD) putc('1', h->fp);
-    else { putc('0', h->fp); diff = h->attr; }
+    if (attr&HAF_BOLD) putc('1', h->lyt.fp);
+    else { putc('0', h->lyt.fp); diff = h->attr; }
     sep = ';';
   }
+
+  /* Change the foreground colour if necessary. */
   if (diff&(HAF_FG | HAF_FGMASK)) {
     if (attr&HAF_FG)
-      set_colour(h->fp, &sep, "3", "9", (attr&HAF_FGMASK) >> HAF_FGSHIFT);
-    else
-      { if (sep) putc(sep, h->fp); fputs("39", h->fp); sep = ';'; }
+      set_colour(h->lyt.fp, &sep, "3", "9",
+                (attr&HAF_FGMASK) >> HAF_FGSHIFT);
+    else {
+      if (sep) putc(sep, h->lyt.fp);
+      fputs("39", h->lyt.fp); sep = ';';
+    }
   }
+
+  /* Change the background colour if necessary. */
   if (diff&(HAF_BG | HAF_BGMASK)) {
     if (attr&HAF_BG)
-      set_colour(h->fp, &sep, "4", "10", (attr&HAF_BGMASK) >> HAF_BGSHIFT);
-    else
-      { if (sep) putc(sep, h->fp); fputs("49", h->fp); sep = ';'; }
+      set_colour(h->lyt.fp, &sep, "4", "10",
+                (attr&HAF_BGMASK) >> HAF_BGSHIFT);
+    else {
+      if (sep) putc(sep, h->lyt.fp);
+      fputs("49", h->lyt.fp); sep = ';';
+    }
   }
 
-  putc('m', h->fp); h->attr = attr;
-
-#undef f_any
+  /* Terminate the control command and save the new attributes. */
+  putc('m', h->lyt.fp); h->attr = attr;
 }
 
+/* --- @clear_progress@ --- *
+ *
+ * Arguments:  @struct human_output *h@ = output state
+ *
+ * Returns:    ---
+ *
+ * Use:                Remove the progress display from the terminal.
+ *
+ *             If the progress display isn't active then do nothing.
+ */
+
 static void clear_progress(struct human_output *h)
 {
   size_t i, n;
 
   if (h->f&HOF_PROGRESS) {
-    n = strlen(h->_o.tv->test->name) + 2 + h->scoreboard.len;
-    for (i = 0; i < n; i++) fputs("\b \b", h->fp);
+    n = strlen(h->tv->test->name) + 2 + h->scoreboard.len;
+    for (i = 0; i < n; i++) fputs("\b \b", h->lyt.fp);
     h->f &= ~HOF_PROGRESS;
   }
 }
 
+/* --- @write_scoreboard_char@ --- *
+ *
+ * Arguments:  @struct human_output *h@ = output state
+ *             @int ch@ = scoreboard character to print
+ *
+ * Returns:    ---
+ *
+ * Use:                Write a scoreboard character, indicating the outcome of a
+ *             test, to the output stream, with appropriate highlighting.
+ */
+
 static void write_scoreboard_char(struct human_output *h, int ch)
 {
   switch (ch) {
-    case 'x': setattr(h, HA_LOSE); break;
-    case '_': setattr(h, HA_SKIP); break;
-    default: setattr(h, 0); break;
+    case HSB_LOSE: setattr(h, HA_LOSE); break;
+    case HSB_SKIP: setattr(h, HA_SKIP); break;
+    case HSB_XFAIL: setattr(h, HA_XFAIL); break;
+    default: setattr(h, HA_PLAIN); break;
   }
-  putc(ch, h->fp); setattr(h, 0);
+  putc(ch, h->lyt.fp); setattr(h, HA_PLAIN);
 }
 
+/* --- @show_progress@ --- *
+ *
+ * Arguments:  @struct human_output *h@ = output state
+ *
+ * Returns:    ---
+ *
+ * Use:                Show the progress display, with the record of outcomes for
+ *             the current test group.
+ *
+ *             If the progress display is already active, or the output
+ *             stream is not interactive, then nothing happens.
+ */
+
 static void show_progress(struct human_output *h)
 {
+  struct tvec_state *tv = h->tv;
   const char *p, *l;
 
-  if ((h->f&HOF_TTY) && !(h->f&HOF_PROGRESS)) {
-    fprintf(h->fp, "%s: ", h->_o.tv->test->name);
+  if (tv->test && (h->f&HOF_TTY) && !(h->f&HOF_PROGRESS)) {
+    fprintf(h->lyt.fp, "%s: ", tv->test->name);
     if (!(h->f&HOF_COLOUR))
-      dstr_write(&h->scoreboard, h->fp);
+      dstr_write(&h->scoreboard, h->lyt.fp);
     else for (p = h->scoreboard.buf, l = p + h->scoreboard.len; p < l; p++)
       write_scoreboard_char(h, *p);
-    fflush(h->fp); h->f |= HOF_PROGRESS;
+    fflush(h->lyt.fp); h->f |= HOF_PROGRESS;
   }
 }
 
-static void report_location(struct human_output *h, FILE *fp,
-                           const char *file, unsigned lno)
-{
-  unsigned f = 0;
-#define f_flush 1u
-
-#define FLUSH(fp) do if (f&f_flush) fflush(fp); while (0)
-
-  if (fp != h->fp) f |= f_flush;
+/* --- @human_writech@, @human_write@, @human_writef@ --- *
+ *
+ * Arguments:  @void *go@ = output sink, secretly a @struct human_output@
+ *             @int ch@ = character to write
+ *             @const char *@p@, @size_t sz@ = string (with explicit length)
+ *                     to write
+ *             @const char *p, ...@ = format control string and arguments to
+ *                     write
+ *
+ * Returns:    ---
+ *
+ * Use:                Write characters, strings, or formatted strings to the
+ *             output, applying appropriate layout.
+ *
+ *             For the human output driver, the layout machinery just strips
+ *             trailing spaces.
+ */
 
-  if (file) {
-    setattr(h, HFG(CYAN)); FLUSH(h->fp); fputs(file, fp); FLUSH(fp);
-    setattr(h, HFG(BLUE)); FLUSH(h->fp); fputc(':', fp); FLUSH(fp);
-    setattr(h, HFG(CYAN)); FLUSH(h->fp); fprintf(fp, "%u", lno); FLUSH(fp);
-    setattr(h, HFG(BLUE)); FLUSH(h->fp); fputc(':', fp); FLUSH(fp);
-    setattr(h, 0); FLUSH(h->fp); fputc(' ', fp);
-  }
+static int human_writech(void *go, int ch)
+  { struct human_output *h = go; return (layout_char(&h->lyt, ch)); }
 
-#undef f_flush
-#undef FLUSH
-}
+static int human_writem(void *go, const char *p, size_t sz)
+  { struct human_output *h = go; return (layout_string(&h->lyt, p, sz)); }
 
-static void human_report(struct human_output *h,
-                        const char *msg, va_list *ap)
+static int human_nwritef(void *go, size_t maxsz, const char *p, ...)
 {
-  struct tvec_state *tv = h->_o.tv;
-
-  fprintf(stderr, "%s: ", QUIS);
-  report_location(h, stderr, tv->infile, tv->lno);
-  vfprintf(stderr, msg, *ap);
-  fputc('\n', stderr);
-
-  if (h->f&HOF_DUPERR) {
-    report_location(h, stderr, tv->infile, tv->lno);
-    vfprintf(h->fp, msg, *ap);
-    fputc('\n', h->fp);
-  }
+  struct human_output *h = go;
+  size_t n;
+  va_list ap;
+
+  va_start(ap, p);
+  n = gprintf_memputf(&h->outbuf, &h->outsz, maxsz, p, ap);
+  va_end(ap);
+  return (layout_string(&h->lyt, h->outbuf, n));
 }
 
-static void human_error(struct tvec_output *o, const char *msg, va_list *ap)
-{
-  struct human_output *h = (struct human_output *)o;
+static const struct gprintf_ops human_printops =
+  { human_writech, human_writem, human_nwritef };
 
-  if (h->f&HOF_PROGRESS) fputc('\n', h->fp);
-  human_report(h, msg, ap);
-}
+/* --- @human_bsession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     human_output@
+ *             @struct tvec_state *tv@ = the test state producing output
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test session.
+ *
+ *             The human driver just records the test state for later
+ *             reference.
+ */
 
-static void human_notice(struct tvec_output *o, const char *msg, va_list *ap)
-{
-  struct human_output *h = (struct human_output *)o;
-  clear_progress(h); human_report(h, msg, ap); show_progress(h);
-}
+static void human_bsession(struct tvec_output *o, struct tvec_state *tv)
+  { struct human_output *h = (struct human_output *)o; h->tv = tv; }
+
+/* --- @report_unusual@ --- *
+ *
+ * Arguments:  @struct human_output *h@ = output sink
+ *             @unsigned nxfail, nskip@ = number of expected failures and
+ *                     skipped tests
+ *
+ * Returns:    ---
+ *
+ * Use:                Write (directly on the output stream) a note about expected
+ *             failures and/or skipped tests, if there were any.
+ */
 
-static void human_write(struct tvec_output *o, const char *p, size_t sz)
+static void report_unusual(struct human_output *h,
+                          unsigned nxfail, unsigned nskip)
 {
-  struct human_output *h = (struct human_output *)o;
-  fwrite(p, 1, sz, h->fp);
-}
+  const char *sep = " (";
+  unsigned f = 0;
+#define f_any 1u
 
-static void human_bsession(struct tvec_output *o) { ; }
+  if (nxfail) {
+    fprintf(h->lyt.fp, "%s%u ", sep, nxfail);
+    setattr(h, HA_XFAIL);
+    fprintf(h->lyt.fp, "expected %s", nxfail == 1 ? "failure" : "failures");
+    setattr(h, HA_PLAIN);
+    sep = ", "; f |= f_any;
+  }
 
-static void report_skipped(struct human_output *h, unsigned n)
-{
-  if (n) {
-    fprintf(h->fp, " (%u ", n);
-    setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0);
-    fputc(')', h->fp);
+  if (nskip) {
+    fprintf(h->lyt.fp, "%s%u ", sep, nskip);
+    setattr(h, HA_SKIP); fputs("skipped", h->lyt.fp); setattr(h, HA_PLAIN);
+    sep = ", "; f |= f_any;
   }
+
+  if (f&f_any) fputc(')', h->lyt.fp);
+
+#undef f_any
 }
 
+/* --- @human_esession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     human_output@
+ *
+ * Returns:    Suggested exit code.
+ *
+ * Use:                End a test session.
+ *
+ *             The human driver prints a final summary of the rest results
+ *             and returns a suitable exit code.
+ */
+
 static int human_esession(struct tvec_output *o)
 {
   struct human_output *h = (struct human_output *)o;
-  struct tvec_state *tv = h->_o.tv;
+  struct tvec_state *tv = h->tv;
   unsigned
     all_win = tv->all[TVOUT_WIN], grps_win = tv->grps[TVOUT_WIN],
+    all_xfail = tv->all[TVOUT_XFAIL],
     all_lose = tv->all[TVOUT_LOSE], grps_lose = tv->grps[TVOUT_LOSE],
     all_skip = tv->all[TVOUT_SKIP], grps_skip = tv->grps[TVOUT_SKIP],
-    all_run = all_win + all_lose, grps_run = grps_win + grps_lose;
+    all_pass = all_win + all_xfail, all_run = all_pass + all_lose,
+    grps_run = grps_win + grps_lose;
 
   if (!all_lose) {
-    setattr(h, HA_WIN); fputs("PASSED", h->fp); setattr(h, 0);
-    fprintf(h->fp, " %s%u %s",
+    setattr(h, HA_WIN); fputs("PASSED", h->lyt.fp); setattr(h, HA_PLAIN);
+    fprintf(h->lyt.fp, " %s%u %s",
            !(all_skip || grps_skip) ? "all " : "",
-           all_win, all_win == 1 ? "test" : "tests");
-    report_skipped(h, all_skip);
-    fprintf(h->fp, " in %u %s",
+           all_pass, all_pass == 1 ? "test" : "tests");
+    report_unusual(h, all_xfail, all_skip);
+    fprintf(h->lyt.fp, " in %u %s",
            grps_win, grps_win == 1 ? "group" : "groups");
-    report_skipped(h, grps_skip);
+    report_unusual(h, 0, grps_skip);
   } else {
-    setattr(h, HA_LOSE); fputs("FAILED", h->fp); setattr(h, 0);
-    fprintf(h->fp, " %u out of %u %s",
+    setattr(h, HA_LOSE); fputs("FAILED", h->lyt.fp); setattr(h, HA_PLAIN);
+    fprintf(h->lyt.fp, " %u out of %u %s",
            all_lose, all_run, all_run == 1 ? "test" : "tests");
-    report_skipped(h, all_skip);
-    fprintf(h->fp, " in %u out of %u %s",
+    report_unusual(h, all_xfail, all_skip);
+    fprintf(h->lyt.fp, " in %u out of %u %s",
            grps_lose, grps_run, grps_run == 1 ? "group" : "groups");
-    report_skipped(h, grps_skip);
+    report_unusual(h, 0, grps_skip);
+  }
+  fputc('\n', h->lyt.fp);
+
+  if (tv->f&TVSF_ERROR) {
+    setattr(h, HA_ERR); fputs("ERRORS", h->lyt.fp); setattr(h, HA_PLAIN);
+    fputs(" found in input; tests may not have run correctly\n", h->lyt.fp);
   }
-  fputc('\n', h->fp);
 
-  return (tv->all[TVOUT_LOSE] ? 1 : 0);
+  h->tv = 0; return (tv->f&TVSF_ERROR ? 2 : all_lose ? 1 : 0);
 }
 
+/* --- @human_bgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     human_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test group.
+ *
+ *             The human driver determines the length of the longest
+ *             register name, resets the group progress scoreboard, and
+ *             activates the progress display.
+ */
+
 static void human_bgroup(struct tvec_output *o)
 {
   struct human_output *h = (struct human_output *)o;
+
+  h->maxlen = register_maxnamelen(h->tv);
   dstr_reset(&h->scoreboard); show_progress(h);
 }
 
-static void human_grpsumm(struct human_output *h, unsigned outcome)
+/* --- @human_skipgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     human_output@
+ *             @const char *excuse@, @va_list *ap@ = reason for skipping the
+ *                     group, or null
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group is being skipped.
+ *
+ *             The human driver just reports the situation to its output
+ *             stream.
+ */
+
+static void human_skipgroup(struct tvec_output *o,
+                           const char *excuse, va_list *ap)
 {
-  struct tvec_state *tv = h->_o.tv;
-  unsigned win = tv->curr[TVOUT_WIN], lose = tv->curr[TVOUT_LOSE],
-    skip = tv->curr[TVOUT_SKIP], run = win + lose;
+  struct human_output *h = (struct human_output *)o;
 
-  if (lose) {
-    assert(outcome == TVOUT_LOSE);
-    fprintf(h->fp, " %u/%u ", lose, run);
-    setattr(h, HA_LOSE); fputs("FAILED", h->fp); setattr(h, 0);
-    report_skipped(h, skip);
-  } else {
-    assert(outcome == TVOUT_WIN);
-    fputc(' ', h->fp); setattr(h, HA_WIN); fputs("ok", h->fp); setattr(h, 0);
-    report_skipped(h, skip);
+  if (!(h->f&HOF_TTY))
+    fprintf(h->lyt.fp, "%s ", h->tv->test->name);
+  else {
+    show_progress(h); h->f &= ~HOF_PROGRESS;
+    if (h->scoreboard.len) putc(' ', h->lyt.fp);
   }
-  fputc('\n', h->fp);
+  setattr(h, HA_SKIP); fputs("skipped", h->lyt.fp); setattr(h, HA_PLAIN);
+  if (excuse) { fputs(": ", h->lyt.fp); vfprintf(h->lyt.fp, excuse, *ap); }
+  fputc('\n', h->lyt.fp);
 }
 
-static void human_egroup(struct tvec_output *o, unsigned outcome)
+/* --- @human_egroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     human_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group has finished.
+ *
+ *             The human driver reports a summary of the group's tests.
+ */
+
+static void human_egroup(struct tvec_output *o)
 {
   struct human_output *h = (struct human_output *)o;
+  struct tvec_state *tv = h->tv;
+  unsigned win = tv->curr[TVOUT_WIN], xfail = tv->curr[TVOUT_XFAIL],
+    lose = tv->curr[TVOUT_LOSE], skip = tv->curr[TVOUT_SKIP],
+    run = win + lose + xfail;
 
   if (h->f&HOF_TTY) h->f &= ~HOF_PROGRESS;
-  else fprintf(h->fp, "%s:", h->_o.tv->test->name);
-  human_grpsumm(h, outcome);
-}
+  else fprintf(h->lyt.fp, "%s:", h->tv->test->name);
 
-static void human_skipgroup(struct tvec_output *o,
-                           const char *excuse, va_list *ap)
-{
-  struct human_output *h = (struct human_output *)o;
-
-  if (!(~h->f&(HOF_TTY | HOF_PROGRESS))) {
-    h->f &= ~HOF_PROGRESS;
-    setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0);
+  if (lose) {
+    fprintf(h->lyt.fp, " %u/%u ", lose, run);
+    setattr(h, HA_LOSE); fputs("FAILED", h->lyt.fp); setattr(h, HA_PLAIN);
+    report_unusual(h, xfail, skip);
   } else {
-    fprintf(h->fp, "%s: ", h->_o.tv->test->name);
-    setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0);
+    fputc(' ', h->lyt.fp); setattr(h, HA_WIN);
+    fputs("ok", h->lyt.fp); setattr(h, HA_PLAIN);
+    report_unusual(h, xfail, skip);
   }
-  if (excuse) { fputs(": ", h->fp); vfprintf(h->fp, excuse, *ap); }
-  fputc('\n', h->fp);
+  fputc('\n', h->lyt.fp);
 }
 
+/* --- @human_btest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     human_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test is starting.
+ *
+ *             The human driver makes sure the progress display is active.
+ */
+
 static void human_btest(struct tvec_output *o)
   { struct human_output *h = (struct human_output *)o; show_progress(h); }
 
-static void human_skip(struct tvec_output *o,
-                      const char *excuse, va_list *ap)
+/* --- @report_location@ --- *
+ *
+ * Arguments:  @struct human_output *h@ = output state
+ *             @FILE *fp@ = stream to write the location on
+ *             @const char *file@ = filename
+ *             @unsigned lno@ = line number
+ *
+ * Returns:    ---
+ *
+ * Use:                Print the filename and line number to the output stream @fp@.
+ *             Also, if appropriate, print interleaved highlighting control
+ *             codes to our usual output stream.  If @file@ is null then do
+ *             nothing.
+ */
+
+static void report_location(struct human_output *h, FILE *fp,
+                           const char *file, unsigned lno)
 {
-  struct human_output *h = (struct human_output *)o;
-  struct tvec_state *tv = h->_o.tv;
+  unsigned f = 0;
+#define f_flush 1u
 
-  clear_progress(h);
-  report_location(h, h->fp, tv->infile, tv->test_lno);
-  fprintf(h->fp, "`%s' ", tv->test->name);
-  setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0);
-  if (excuse) { fputs(": ", h->fp); vfprintf(h->fp, excuse, *ap); }
-  fputc('\n', h->fp);
-}
+  /* We emit highlighting if @fp@ is our usual output stream, or the
+   * duplicate-errors flag is clear indicating that (we assume) they're
+   * secretly going to the same place anyway.  If they're different streams,
+   * though, we have to be careful to keep the highlighting and the actual
+   * text synchronized.
+   */
+
+  if (!file)
+    /* nothing to do */;
+  else if (fp != h->lyt.fp && (h->f&HOF_DUPERR))
+    fprintf(fp, "%s:%u: ", file, lno);
+  else {
+    if (fp != h->lyt.fp) f |= f_flush;
 
-static void human_fail(struct tvec_output *o,
-                      const char *detail, va_list *ap)
-{
-  struct human_output *h = (struct human_output *)o;
-  struct tvec_state *tv = h->_o.tv;
+#define FLUSH(fp) do if (f&f_flush) fflush(fp); while (0)
 
-  clear_progress(h);
-  report_location(h, h->fp, tv->infile, tv->test_lno);
-  fprintf(h->fp, "`%s' ", tv->test->name);
-  setattr(h, HA_LOSE); fputs("FAILED", h->fp); setattr(h, 0);
-  if (detail) { fputs(": ", h->fp); vfprintf(h->fp, detail, *ap); }
-  fputc('\n', h->fp);
-}
+    setattr(h, HA_LOC);                FLUSH(h->lyt.fp);
+    fputs(file, fp);           FLUSH(fp);
+    setattr(h, HA_LOCSEP);     FLUSH(h->lyt.fp);
+    fputc(':', fp);            FLUSH(fp);
+    setattr(h, HA_LOC);                FLUSH(h->lyt.fp);
+    fprintf(fp, "%u", lno);    FLUSH(fp);
+    setattr(h, HA_LOCSEP);     FLUSH(h->lyt.fp);
+    fputc(':', fp);            FLUSH(fp);
+    setattr(h, HA_PLAIN);      FLUSH(h->lyt.fp);
+    fputc(' ', fp);
 
-static void set_dispattr(struct human_output *h, unsigned disp)
-{
-  switch (disp) {
-    case EXPECT: setattr(h, HFG(GREEN)); break;
-    case FOUND: setattr(h, HFG(RED)); break;
-    default: setattr(h, 0); break;
+#undef FLUSH
   }
-}
 
-static void human_report_status(unsigned disp, int st, struct tvec_state *tv)
-{
-  struct human_output *h = (struct human_output *)tv->output;
-
-  fprintf(h->fp, "  %8s status = ", stdisp(disp));
-  set_dispattr(h, disp); fprintf(h->fp, "`%c'", st); setattr(h, 0);
-  fputc('\n', h->fp);
+#undef f_flush
 }
 
-static void human_report_register(unsigned disp,
-                                 const struct tvec_reg *r,
-                                 const struct tvec_regdef *rd,
-                                 struct tvec_state *tv)
+/* --- @human_outcome@, @human_skip@, @human_fail@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     human_output@
+ *             @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 human driver reports the situation on its output stream.
+ */
+
+static void human_outcome(struct tvec_output *o,
+                         unsigned attr, const char *outcome,
+                         const char *detail, va_list *ap)
 {
-  struct human_output *h = (struct human_output *)tv->output;
+  struct human_output *h = (struct human_output *)o;
+  struct tvec_state *tv = h->tv;
 
-  fprintf(h->fp, "  %8s %s = ", regdisp(disp), rd->name);
-  if (!(r->f&TVRF_LIVE))
-    tvec_write(tv, "#<unset>");
-  else {
-    set_dispattr(h, disp);
-    rd->ty->dump(&r->v, rd, tv, 0);
-    setattr(h, 0);
-  }
-  tvec_write(tv, "\n");
+  clear_progress(h);
+  report_location(h, h->lyt.fp, tv->infile, tv->test_lno);
+  fprintf(h->lyt.fp, "`%s' ", tv->test->name);
+  setattr(h, attr); fputs(outcome, h->lyt.fp); setattr(h, HA_PLAIN);
+  if (detail) { fputs(": ", h->lyt.fp); vfprintf(h->lyt.fp, detail, *ap); }
+  fputc('\n', h->lyt.fp);
 }
 
-static const struct mismatchfns human_mismatchfns =
-  { human_report_status, human_report_register };
+static void human_skip(struct tvec_output *o,
+                      const char *excuse, va_list *ap)
+  { human_outcome(o, HA_SKIP, "skipped", excuse, ap); }
+static void human_fail(struct tvec_output *o,
+                      const char *detail, va_list *ap)
+  { human_outcome(o, HA_LOSE, "FAILED", detail, ap); }
+
+/* --- @human_dumpreg@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     human_output@
+ *             @unsigned disp@ = register disposition
+ *             @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register.
+ *
+ *             The human driver applies highlighting to mismatching output
+ *             registers, but otherwise delegates to the register type
+ *             handler and the layout machinery.
+ */
 
-static void human_mismatch(struct tvec_output *o)
+static void human_dumpreg(struct tvec_output *o,
+                         unsigned disp, const union tvec_regval *rv,
+                         const struct tvec_regdef *rd)
 {
   struct human_output *h = (struct human_output *)o;
+  const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name);
 
-  if (h->f&HOF_COLOUR) mismatch(&human_mismatchfns, h->_o.tv);
-  else mismatch(&basic_mismatchfns, h->_o.tv);
+  clear_progress(h);
+  gprintf(&human_printops, h, "%*s%s %s = ",
+         10 + h->maxlen - n, "", ds, rd->name);
+  if (h->f&HOF_COLOUR) {
+    if (!rv) setattr(h, HA_UNSET);
+    else if (disp == TVRD_FOUND) setattr(h, HA_FOUND);
+    else if (disp == TVRD_EXPECT) setattr(h, HA_EXPECT);
+  }
+  if (!rv) gprintf(&human_printops, h, "#unset");
+  else rd->ty->dump(rv, rd, 0, &human_printops, h);
+  setattr(h, HA_PLAIN); layout_char(&h->lyt, '\n');
 }
 
+/* --- @human_etest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     human_output@
+ *             @unsigned outcome@ = the test outcome
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test has finished.
+ *
+ *             The human driver reactivates the progress display, if
+ *             necessary, and adds a new character for the completed test.
+ */
+
 static void human_etest(struct tvec_output *o, unsigned outcome)
 {
   struct human_output *h = (struct human_output *)o;
@@ -614,51 +1042,172 @@ static void human_etest(struct tvec_output *o, unsigned outcome)
   if (h->f&HOF_TTY) {
     show_progress(h);
     switch (outcome) {
-      case TVOUT_WIN: ch = '.'; break;
-      case TVOUT_LOSE: ch = 'x'; break;
-      case TVOUT_SKIP: ch = '_'; break;
+      case TVOUT_WIN: ch = HSB_WIN; break;
+      case TVOUT_LOSE: ch = HSB_LOSE; break;
+      case TVOUT_XFAIL: ch = HSB_XFAIL; break;
+      case TVOUT_SKIP: ch = HSB_SKIP; break;
       default: abort();
     }
     dstr_putc(&h->scoreboard, ch);
-    write_scoreboard_char(h, ch); fflush(h->fp);
+    write_scoreboard_char(h, ch); fflush(h->lyt.fp);
   }
 }
 
-static void human_bbench(struct tvec_output *o)
+/* --- @human_bbench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     human_output@
+ *             @const char *ident@ = identifying register values
+ *             @unsigned unit@ = measurement unit (@TVBU_...@)
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a benchmark has started.
+ *
+ *             The human driver just prints the start of the benchmark
+ *             report.
+ */
+
+static void human_bbench(struct tvec_output *o,
+                        const char *ident, unsigned unit)
 {
   struct human_output *h = (struct human_output *)o;
-  struct tvec_state *tv = h->_o.tv;
+  struct tvec_state *tv = h->tv;
 
   clear_progress(h);
-  fprintf(h->fp, "%s: ", tv->test->name);
-  bench_summary(tv); fflush(h->fp);
+  fprintf(h->lyt.fp, "%s: %s: ", tv->test->name, ident); fflush(h->lyt.fp);
 }
 
+/* --- @human_ebench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     human_output@
+ *             @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 human driver just delegates to the default benchmark
+ *             reporting, via the layout machinery.
+ */
+
 static void human_ebench(struct tvec_output *o,
+                        const char *ident, unsigned unit,
                         const struct bench_timing *tm)
 {
   struct human_output *h = (struct human_output *)o;
-  bench_report(h->_o.tv, tm);
+
+  tvec_benchreport(&human_printops, h, unit, tm);
+  fputc('\n', h->lyt.fp);
+}
+
+/* --- @human_report@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     human_output@
+ *             @unsigned level@ = message level (@TVLEV_...@)
+ *             @const char *msg@, @va_list *ap@ = format string and
+ *                     arguments
+ *
+ * Returns:    ---
+ *
+ * Use:                Report a message to the user.
+ *
+ *             The human driver arranges to show the message on @stderr@ as
+ *             well as the usual output, with a certain amount of
+ *             intelligence in case they're both actually the same device.
+ */
+
+static void human_report(struct tvec_output *o, unsigned level,
+                        const char *msg, va_list *ap)
+{
+  struct human_output *h = (struct human_output *)o;
+  struct tvec_state *tv = h->tv;
+  const char *levstr; unsigned levattr;
+  dstr d = DSTR_INIT;
+  unsigned f = 0;
+#define f_flush 1u
+#define f_progress 2u
+
+  dstr_vputf(&d, msg, ap); dstr_putc(&d, '\n');
+
+  switch (level) {
+#define CASE(tag, name, val)                                           \
+    case TVLEV_##tag: levstr = name; levattr = HA_##tag; break;
+    TVEC_LEVELS(CASE)
+    default: levstr = "??"; levattr = HA_UNKLEV; break;
+  }
+
+  if (h->lyt.fp != stderr && !(h->f&HOF_DUPERR)) f |= f_flush;
+
+#define FLUSH do if (f&f_flush) fflush(h->lyt.fp); while (0)
+
+  if (h->f^HOF_PROGRESS)
+    { clear_progress(h); fflush(h->lyt.fp); f |= f_progress; }
+  fprintf(stderr, "%s: ", QUIS);
+  report_location(h, stderr, tv->infile, tv->lno);
+  setattr(h, levattr); FLUSH; fputs(levstr, stderr); setattr(h, 0); FLUSH;
+  fputs(": ", stderr); fwrite(d.buf, 1, d.len, stderr);
+
+#undef FLUSH
+
+  if (h->f&HOF_DUPERR) {
+    report_location(h, h->lyt.fp, tv->infile, tv->lno);
+    fprintf(h->lyt.fp, "%s: ", levstr);
+    fwrite(d.buf, 1, d.len, h->lyt.fp);
+  }
+  if (f&f_progress) show_progress(h);
+
+#undef f_flush
+#undef f_progress
 }
 
+/* --- @human_destroy@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     human_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Release the resources held by the output driver.
+ */
+
 static void human_destroy(struct tvec_output *o)
 {
   struct human_output *h = (struct human_output *)o;
 
-  if (h->f&HOF_DUPERR) fclose(h->fp);
+  destroy_layout(&h->lyt,
+                h->lyt.fp == stdout || h->lyt.fp == stderr ? 0 : DLF_CLOSE);
   dstr_destroy(&h->scoreboard);
-  xfree(h);
+  xfree(h->outbuf); xfree(h);
 }
 
 static const struct tvec_outops human_ops = {
-  human_error, human_notice, human_write,
   human_bsession, human_esession,
-  human_bgroup, human_egroup, human_skipgroup,
-  human_btest, human_skip, human_fail, human_mismatch, human_etest,
+  human_bgroup, human_skipgroup, human_egroup,
+  human_btest, human_skip, human_fail, human_dumpreg, human_etest,
   human_bbench, human_ebench,
+  human_report,
   human_destroy
 };
 
+/* --- @tvec_humanoutput@ --- *
+ *
+ * Arguments:  @FILE *fp@ = output file to write on
+ *
+ * Returns:    An output formatter.
+ *
+ * Use:                Return an output formatter which writes on @fp@ with the
+ *             expectation that a human will be watching and interpreting
+ *             the output.  If @fp@ denotes a terminal, the display shows a
+ *             `scoreboard' indicating the outcome of each test case
+ *             attempted, and may in addition use colour and other
+ *             highlighting.
+ */
+
 struct tvec_output *tvec_humanoutput(FILE *fp)
 {
   struct human_output *h;
@@ -667,8 +1216,8 @@ struct tvec_output *tvec_humanoutput(FILE *fp)
   h = xmalloc(sizeof(*h)); h->_o.ops = &human_ops;
   h->f = 0; h->attr = 0;
 
-  h->fp = fp;
-  if (fp != stdout && fp != stderr) h->f |= HOF_DUPERR;
+  init_layout(&h->lyt, fp, 0);
+  h->outbuf = 0; h->outsz = 0;
 
   switch (getenv_boolean("TVEC_TTY", -1)) {
     case 1: h->f |= HOF_TTY; break;
@@ -688,6 +1237,7 @@ struct tvec_output *tvec_humanoutput(FILE *fp)
       break;
   }
 
+  if (fp != stderr && (fp != stdout || !(h->f&HOF_TTY))) h->f |= HOF_DUPERR;
   dstr_create(&h->scoreboard);
   return (&h->_o);
 }
@@ -695,173 +1245,458 @@ struct tvec_output *tvec_humanoutput(FILE *fp)
 /*----- Perl's `Test Anything Protocol' -----------------------------------*/
 
 struct tap_output {
-  struct tvec_output _o;
-  FILE *fp;
-  unsigned f;
-#define TOF_FRESHLINE 1u
+  struct tvec_output _o;               /* output base class */
+  struct tvec_state *tv;               /* stashed testing state */
+  struct layout lyt;                   /* output layout */
+  char *outbuf; size_t outsz;          /* buffer for formatted output */
+  unsigned grpix, testix;              /* group and test indices */
+  unsigned previx;                     /* previously reported test index */
+  int maxlen;                          /* longest register name */
 };
 
-static void tap_report(struct tap_output *t, const char *msg, va_list *ap)
-{
-  struct tvec_state *tv = t->_o.tv;
+/* --- @tap_writech@, @tap_write@, @tap_writef@ --- *
+ *
+ * Arguments:  @void *go@ = output sink, secretly a @struct tap_output@
+ *             @int ch@ = character to write
+ *             @const char *@p@, @size_t sz@ = string (with explicit length)
+ *                     to write
+ *             @const char *p, ...@ = format control string and arguments to
+ *                     write
+ *
+ * Returns:    ---
+ *
+ * Use:                Write characters, strings, or formatted strings to the
+ *             output, applying appropriate layout.
+ *
+ *             For the TAP output driver, the layout machinery prefixes each
+ *             line with `    ## ' and strips trailing spaces.
+ */
 
-  if (tv->infile) fprintf(t->fp, "%s:%u: ", tv->infile, tv->lno);
-  vfprintf(t->fp, msg, *ap); fputc('\n', t->fp);
-}
+static int tap_writech(void *go, int ch)
+  { struct tap_output *t = go; return (layout_char(&t->lyt, ch)); }
 
-static void tap_error(struct tvec_output *o, const char *msg, va_list *ap)
-{
-  struct tap_output *t = (struct tap_output *)o;
-  fputs("Bail out!  ", t->fp); tap_report(t, msg, ap);
-}
+static int tap_writem(void *go, const char *p, size_t sz)
+  { struct tap_output *t = go; return (layout_string(&t->lyt, p, sz)); }
 
-static void tap_notice(struct tvec_output *o, const char *msg, va_list *ap)
+static int tap_nwritef(void *go, size_t maxsz, const char *p, ...)
 {
-  struct tap_output *t = (struct tap_output *)o;
-  fputs("## ", t->fp); tap_report(t, msg, ap);
+  struct tap_output *t = go;
+  size_t n;
+  va_list ap;
+
+  va_start(ap, p);
+  n = gprintf_memputf(&t->outbuf, &t->outsz, maxsz, p, ap);
+  va_end(ap);
+  return (layout_string(&t->lyt, t->outbuf, n));
 }
 
-static void tap_write(struct tvec_output *o, const char *p, size_t sz)
-{
-  struct tap_output *t = (struct tap_output *)o;
-  const char *q, *l = p + sz;
-
-  if (p == l) return;
-  if (t->f&TOF_FRESHLINE) fputs("## ", t->fp);
-  for (;;) {
-    q = memchr(p, '\n', l - p); if (!q) break;
-    fwrite(p, 1, q + 1 - p, t->fp); p = q + 1;
-    if (p == l) { t->f |= TOF_FRESHLINE; return; }
-    fputs("## ", t->fp);
-  }
-  fwrite(p, 1, l - p, t->fp); t->f &= ~TOF_FRESHLINE;
-}
+static const struct gprintf_ops tap_printops =
+  { tap_writech, tap_writem, tap_nwritef };
 
-static void tap_bsession(struct tvec_output *o) { ; }
+/* --- @tap_bsession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     tap_output@
+ *             @struct tvec_state *tv@ = the test state producing output
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test session.
+ *
+ *             The TAP driver records the test state for later reference,
+ *             initializes the group index counter, and prints the version
+ *             number.
+ */
 
-static unsigned tap_grpix(struct tap_output *t)
+static void tap_bsession(struct tvec_output *o, struct tvec_state *tv)
 {
-  struct tvec_state *tv = t->_o.tv;
+  struct tap_output *t = (struct tap_output *)o;
 
-  return (tv->grps[TVOUT_WIN] +
-         tv->grps[TVOUT_LOSE] +
-         tv->grps[TVOUT_SKIP]);
+  t->tv = tv; t->grpix = 0;
+  fputs("TAP version 13\n", t->lyt.fp); /* but secretly 14 really */
 }
 
+/* --- @tap_esession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     tap_output@
+ *
+ * Returns:    Suggested exit code.
+ *
+ * Use:                End a test session.
+ *
+ *             The TAP driver prints a final summary of the rest results
+ *             and returns a suitable exit code.  If errors occurred, it
+ *             instead prints a `Bail out!' line forcing the reader to
+ *             report a failure.
+ */
+
 static int tap_esession(struct tvec_output *o)
 {
   struct tap_output *t = (struct tap_output *)o;
+  struct tvec_state *tv = t->tv;
 
-  fprintf(t->fp, "1..%u\n", tap_grpix(t));
-  return (0);
+  if (tv->f&TVSF_ERROR) {
+    fputs("Bail out!  "
+         "Errors found in input; tests may not have run correctly\n",
+         t->lyt.fp);
+    return (2);
+  }
+
+  fprintf(t->lyt.fp, "1..%u\n", t->grpix);
+  t->tv = 0; return (tv->all[TVOUT_LOSE] ? 1 : 0);
 }
 
-static void tap_bgroup(struct tvec_output *o) { ; }
+/* --- @tap_bgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     tap_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test group.
+ *
+ *             The TAP driver determines the length of the longest
+ *             register name, resets the group progress scoreboard, and
+ *             activates the progress display.
+ */
 
-static void tap_egroup(struct tvec_output *o, unsigned outcome)
+static void tap_bgroup(struct tvec_output *o)
 {
   struct tap_output *t = (struct tap_output *)o;
-  struct tvec_state *tv = t->_o.tv;
-  unsigned
-    grpix = tap_grpix(t),
-    win = tv->curr[TVOUT_WIN],
-    lose = tv->curr[TVOUT_LOSE],
-    skip = tv->curr[TVOUT_SKIP];
+  struct tvec_state *tv = t->tv;
 
-  if (lose) {
-    assert(outcome == TVOUT_LOSE);
-    fprintf(t->fp, "not ok %u %s: FAILED %u/%u",
-           grpix, tv->test->name, lose, win + lose);
-    if (skip) fprintf(t->fp, " (skipped %u)", skip);
-  } else {
-    assert(outcome == TVOUT_WIN);
-    fprintf(t->fp, "ok %u %s: passed %u", grpix, tv->test->name, win);
-    if (skip) fprintf(t->fp, " (skipped %u)", skip);
-  }
-  fputc('\n', t->fp);
+  t->grpix++; t->testix = t->previx = 0;
+  t->maxlen = register_maxnamelen(t->tv);
+  fprintf(t->lyt.fp, "# Subtest: %s\n", tv->test->name);
 }
 
+/* --- @tap_skipgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     tap_output@
+ *             @const char *excuse@, @va_list *ap@ = reason for skipping the
+ *                     group, or null
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group is being skipped.
+ *
+ *             The TAP driver just reports the situation to its output
+ *             stream.
+ */
+
 static void tap_skipgroup(struct tvec_output *o,
                          const char *excuse, va_list *ap)
 {
   struct tap_output *t = (struct tap_output *)o;
 
-  fprintf(t->fp, "ok %u %s # SKIP", tap_grpix(t), t->_o.tv->test->name);
-  if (excuse)
-    { fputc(' ', t->fp); vfprintf(t->fp, excuse, *ap); }
-  fputc('\n', t->fp);
+  fprintf(t->lyt.fp, "    1..%u\n", t->testix);
+  fprintf(t->lyt.fp, "ok %u %s # SKIP", t->grpix, t->tv->test->name);
+  if (excuse) { fputc(' ', t->lyt.fp); vfprintf(t->lyt.fp, excuse, *ap); }
+  fputc('\n', t->lyt.fp);
 }
 
-static void tap_btest(struct tvec_output *o) { ; }
+/* --- @tap_egroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     tap_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group has finished.
+ *
+ *             The TAP driver reports a summary of the group's tests.
+ */
 
-static void tap_skip(struct tvec_output *o, const char *excuse, va_list *ap)
+static void tap_egroup(struct tvec_output *o)
 {
   struct tap_output *t = (struct tap_output *)o;
-  struct tvec_state *tv = t->_o.tv;
+  struct tvec_state *tv = t->tv;
+
+  fprintf(t->lyt.fp, "    1..%u\n", t->testix);
+  fprintf(t->lyt.fp, "%s %u - %s\n",
+         tv->curr[TVOUT_LOSE] ? "not ok" : "ok",
+         t->grpix, tv->test->name);
+}
+
+/* --- @tap_btest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     tap_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test is starting.
+ *
+ *             The TAP driver advances its test counter.  (We could do this
+ *             by adding up up the counters in @tv->curr@, and add on the
+ *             current test, but it's easier this way.)
+ */
+
+static void tap_btest(struct tvec_output *o)
+  { struct tap_output *t = (struct tap_output *)o; t->testix++; }
+
+/* --- @tap_outcome@, @tap_skip@, @tap_fail@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     tap_output@
+ *             @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 TAP driver reports the situation on its output stream.
+ *             TAP only allows us to report a single status for each
+ *             subtest, so we notice when we've already reported a status
+ *             for the current test and convert the second report as a
+ *             comment.  This should only happen in the case of multiple
+ *             failures.
+ */
 
-  fprintf(t->fp, "## %s:%u: `%s' skipped",
-         tv->infile, tv->test_lno, tv->test->name);
-  if (excuse) { fputs(": ", t->fp); vfprintf(t->fp, excuse, *ap); }
-  fputc('\n', t->fp);
+static void tap_outcome(struct tvec_output *o,
+                       const char *head, const char *tail,
+                       const char *detail, va_list *ap)
+{
+  struct tap_output *t = (struct tap_output *)o;
+  struct tvec_state *tv = t->tv;
+
+  fprintf(t->lyt.fp, "    %s %u - %s:%u%s",
+         t->testix == t->previx ? "##" : head,
+         t->testix, tv->infile, tv->test_lno, tail);
+  if (detail)
+    { fputc(' ', t->lyt.fp); vfprintf(t->lyt.fp, detail, *ap); }
+  fputc('\n', t->lyt.fp);
+  t->previx = t->testix;
 }
 
+static void tap_skip(struct tvec_output *o, const char *excuse, va_list *ap)
+  { tap_outcome(o, "ok", " # SKIP", excuse, ap); }
 static void tap_fail(struct tvec_output *o, const char *detail, va_list *ap)
+  { tap_outcome(o, "not ok", "", detail, ap); }
+
+/* --- @tap_dumpreg@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     tap_output@
+ *             @unsigned disp@ = register disposition
+ *             @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register.
+ *
+ *             The TAP driver applies highlighting to mismatching output
+ *             registers, but otherwise delegates to the register type
+ *             handler and the layout machinery.  The result is that the
+ *             register dump is marked as a comment and indented.
+ */
+
+static void tap_dumpreg(struct tvec_output *o,
+                       unsigned disp, const union tvec_regval *rv,
+                       const struct tvec_regdef *rd)
 {
   struct tap_output *t = (struct tap_output *)o;
-  struct tvec_state *tv = t->_o.tv;
+  const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name);
+
+  set_layout_prefix(&t->lyt, "    ## ");
+  gprintf(&tap_printops, t, "%*s%s %s = ",
+         10 + t->maxlen - n, "", ds, rd->name);
+  if (!rv) gprintf(&tap_printops, t, "#<unset>");
+  else rd->ty->dump(rv, rd, 0, &tap_printops, t);
+  layout_char(&t->lyt, '\n');
+}
+
+/* --- @tap_etest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     tap_output@
+ *             @unsigned outcome@ = the test outcome
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test has finished.
+ *
+ *             The TAP driver reports the outcome of the test, if that's not
+ *             already decided.
+ */
 
-  fprintf(t->fp, "## %s:%u: `%s' FAILED",
-         tv->infile, tv->test_lno, tv->test->name);
-  if (detail) { fputs(": ", t->fp); vfprintf(t->fp, detail, *ap); }
-  fputc('\n', t->fp);
+static void tap_etest(struct tvec_output *o, unsigned outcome)
+{
+  switch (outcome) {
+    case TVOUT_WIN:
+      tap_outcome(o, "ok", "", 0, 0);
+      break;
+    case TVOUT_XFAIL:
+      tap_outcome(o, "not ok", " # TODO expected failure", 0, 0);
+      break;
+  }
 }
 
-static void tap_mismatch(struct tvec_output *o)
-  { mismatch(&basic_mismatchfns, o->tv); }
+/* --- @tap_bbench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     tap_output@
+ *             @const char *ident@ = identifying register values
+ *             @unsigned unit@ = measurement unit (@TVBU_...@)
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a benchmark has started.
+ *
+ *             The TAP driver does nothing here.  All of the reporting
+ *             happens in @tap_ebench@.
+ */
 
-static void tap_etest(struct tvec_output *o, unsigned outcome) { ; }
+static void tap_bbench(struct tvec_output *o,
+                      const char *ident, unsigned unit)
+  { ; }
 
-static void tap_bbench(struct tvec_output *o) { ; }
+/* --- @tap_ebench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     tap_output@
+ *             @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 TAP driver just delegates to the default benchmark
+ *             reporting, via the layout machinery so that the result is
+ *             printed as a comment.
+ */
 
 static void tap_ebench(struct tvec_output *o,
+                      const char *ident, unsigned unit,
                       const struct bench_timing *tm)
 {
   struct tap_output *t = (struct tap_output *)o;
-  struct tvec_state *tv = t->_o.tv;
+  struct tvec_state *tv = t->tv;
 
-  tvec_write(tv, "%s: ", tv->test->name); bench_summary(tv);
-  bench_report(tv, tm);
+  set_layout_prefix(&t->lyt, "    ## ");
+  gprintf(&tap_printops, t, "%s: %s: ", tv->test->name, ident);
+  tvec_benchreport(&tap_printops, t, unit, tm);
+  layout_char(&t->lyt, '\n');
 }
 
+/* --- @tap_report@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     tap_output@
+ *             @unsigned level@ = message level (@TVLEV_...@)
+ *             @const char *msg@, @va_list *ap@ = format string and
+ *                     arguments
+ *
+ * Returns:    ---
+ *
+ * Use:                Report a message to the user.
+ *
+ *             Messages are reported as comments, so that they can be
+ *             accumulated by the reader.  An error will cause a later
+ *             bailout or, if we crash before then, a missing plan line,
+ *             either of which will cause the reader to report a serious
+ *             problem.
+ */
+
+static void tap_report(struct tvec_output *o, unsigned level,
+                      const char *msg, va_list *ap)
+{
+  struct tap_output *t = (struct tap_output *)o;
+  struct tvec_state *tv = t->tv;
+
+  if (tv->test) set_layout_prefix(&t->lyt, "    ## ");
+  else set_layout_prefix(&t->lyt, "## ");
+
+  if (tv->infile) gprintf(&tap_printops, t, "%s:%u: ", tv->infile, tv->lno);
+  gprintf(&tap_printops, t, "%s: ", tvec_strlevel(level));
+  vgprintf(&tap_printops, t, msg, ap);
+  layout_char(&t->lyt, '\n');
+}
+
+/* --- @tap_destroy@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     tap_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Release the resources held by the output driver.
+ */
+
 static void tap_destroy(struct tvec_output *o)
 {
   struct tap_output *t = (struct tap_output *)o;
 
-  if (t->fp != stdout && t->fp != stderr) fclose(t->fp);
-  xfree(t);
+  destroy_layout(&t->lyt,
+                t->lyt.fp == stdout || t->lyt.fp == stderr ? 0 : DLF_CLOSE);
+  xfree(t->outbuf); xfree(t);
 }
 
 static const struct tvec_outops tap_ops = {
-  tap_error, tap_notice, tap_write,
   tap_bsession, tap_esession,
-  tap_bgroup, tap_egroup, tap_skipgroup,
-  tap_btest, tap_skip, tap_fail, tap_mismatch, tap_etest,
+  tap_bgroup, tap_skipgroup, tap_egroup,
+  tap_btest, tap_skip, tap_fail, tap_dumpreg, tap_etest,
   tap_bbench, tap_ebench,
+  tap_report,
   tap_destroy
 };
 
+/* --- @tvec_tapoutput@ --- *
+ *
+ * Arguments:  @FILE *fp@ = output file to write on
+ *
+ * Returns:    An output formatter.
+ *
+ * Use:                Return an output formatter which writes on @fp@ in `TAP'
+ *             (`Test Anything Protocol') format.
+ *
+ *             TAP comes from the Perl community, but has spread rather
+ *             further.  This driver produces TAP version 14, but pretends
+ *             to be version 13.  The driver produces a TAP `test point' --
+ *             i.e., a result reported as `ok' or `not ok' -- for each input
+ *             test group.  Failure reports and register dumps are produced
+ *             as diagnostic messages before the final group result.  (TAP
+ *             permits structuerd YAML data after the test-point result,
+ *             which could be used to report details, but (a) postponing the
+ *             details until after the report is inconvenient, and (b) there
+ *             is no standardization for the YAML anyway, so in practice
+ *             it's no more useful than the unstructured diagnostics.
+ */
+
 struct tvec_output *tvec_tapoutput(FILE *fp)
 {
   struct tap_output *t;
 
   t = xmalloc(sizeof(*t)); t->_o.ops = &tap_ops;
-  t->f = TOF_FRESHLINE;
-  t->fp = fp;
+  init_layout(&t->lyt, fp, 0);
+  t->outbuf = 0; t->outsz = 0;
   return (&t->_o);
 }
 
 /*----- Default output ----------------------------------------------------*/
 
+/* --- @tvec_dfltoutput@ --- *
+ *
+ * Arguments:  @FILE *fp@ = output file to write on
+ *
+ * Returns:    An output formatter.
+ *
+ * Use:                Selects and instantiates an output formatter suitable for
+ *             writing on @fp@.  The policy is subject to change, but
+ *             currently the `human' output format is selected if @fp@ is
+ *             interactive (i.e., if @isatty(fileno(fp))@ is true), and
+ *             otherwise the `tap' format is used.
+ */
+
 struct tvec_output *tvec_dfltout(FILE *fp)
 {
   int ttyp = getenv_boolean("TVEC_TTY", -1);