@@@ misc wip
[mLib] / test / tvec-output.c
index add820d..46c3442 100644 (file)
@@ -61,8 +61,8 @@ static const char *regdisp(unsigned disp)
     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";
+    case TVRD_EXPECT: return "expected";
     default: abort();
   }
 }
@@ -151,8 +151,8 @@ struct layout {
    * file.  Return immediately on error.                               \
    */                                                                  \
                                                                        \
-  size_t n = limit - base;                                             \
-  if (fwrite(base, 1, n, lyt->fp) < n) return (-1);                    \
+  size_t _n = limit - base;                                            \
+  if (_n && fwrite(base, 1, _n, lyt->fp) < _n) return (-1);            \
 } while (0)
 
 #define PUT_CHAR(ch) do {                                              \
@@ -170,8 +170,8 @@ struct layout {
 #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);         \
+  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 {                                                        \
@@ -189,6 +189,31 @@ struct layout {
     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 char *q, *l;
+
+ 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;
+  }
+}
+
 /* --- @init_layout@ --- *
  *
  * Arguments:  @struct layout *lyt@ = layout state to initialize
@@ -202,21 +227,10 @@ struct layout {
 
 static void init_layout(struct layout *lyt, FILE *fp, const char *prefix)
 {
-  const char *q, *l;
-
-  /* Basics. */
   lyt->fp = fp;
   lyt->f = LYTF_NEWL;
   dstr_create(&lyt->w);
-
-  /* Prefix portions. */
-  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;
-  }
+  set_layout_prefix(lyt, prefix);
 }
 
 /* --- @destroy_layout@ --- *
@@ -402,38 +416,6 @@ static int layout_string(struct layout *lyt, const char *p, size_t sz)
 #undef PUT_CHAR
 #undef SAVE_PFXTAIL
 
-/*----- Skeleton ----------------------------------------------------------*/
-/*
-static void ..._bsession(struct tvec_output *o, struct tvec_state *tv)
-static int ..._esession(struct tvec_output *o)
-static void ..._bgroup(struct tvec_output *o)
-static void ..._skipgroup(struct tvec_output *o,
-                         const char *excuse, va_list *ap)
-static void ..._egroup(struct tvec_output *o)
-static void ..._btest(struct tvec_output *o)
-static void ..._skip(struct tvec_output *o, const char *excuse, va_list *ap)
-static void ..._fail(struct tvec_output *o, const char *detail, va_list *ap)
-static void ..._dumpreg(struct tvec_output *o, unsigned disp,
-                       union tvec_regval *rv, const struct tvec_regdef *rd)
-static void ..._etest(struct tvec_output *o, unsigned outcome)
-static void ..._bbench(struct tvec_output *o,
-                      const char *ident, unsigned unit)
-static void ..._ebench(struct tvec_output *o,
-                      const char *ident, unsigned unit,
-                      const struct tvec_timing *t)
-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 ..._destroy(struct tvec_output *o)
-
-static const struct tvec_outops ..._ops = {
-  ..._bsession, ..._esession,
-  ..._bgroup, ..._egroup, ..._skip,
-  ..._btest, ..._skip, ..._fail, ..._dumpreg, ..._etest,
-  ..._bbench, ..._ebench,
-  ..._error, ..._notice,
-  ..._destroy
-};
-*/
 /*----- Human-readable output ---------------------------------------------*/
 
 /* Attributes for colour output.  This should be done better, but @terminfo@
@@ -467,6 +449,9 @@ static const struct tvec_outops ..._ops = {
 #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 */
@@ -474,7 +459,6 @@ static const struct tvec_outops ..._ops = {
 #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 */
-#define HA_ERR (HFG(MAGENTA) | HAF_BOLD) /* reporting an error */
 
 /* Scoreboard indicators. */
 #define HSB_WIN '.'                    /* test passed */
@@ -656,62 +640,24 @@ static void show_progress(struct human_output *h)
   }
 }
 
-/* --- @report_location@ --- *
+/* --- @human_writech@, @human_write@, @human_writef@ --- *
  *
- * Arguments:  @struct human_output *h@ = output state
- *             @FILE *fp@ = stream to write the location on
- *             @const char *file@ = filename
- *             @unsigned lno@ = line number
+ * 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:                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.
+ * 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.
  */
 
-static void report_location(struct human_output *h, FILE *fp,
-                           const char *file, unsigned lno)
-{
-  unsigned f = 0;
-#define f_flush 1u
-
-  /* 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;
-
-#define FLUSH(fp) do if (f&f_flush) fflush(fp); while (0)
-
-    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);
-
-#undef FLUSH
-  }
-
-#undef f_flush
-}
-
-/* Output layout.  Pass everything along to the layout machinery above. */
-
 static int human_writech(void *go, int ch)
   { struct human_output *h = go; return (layout_char(&h->lyt, ch)); }
 
@@ -727,17 +673,42 @@ static int human_nwritef(void *go, size_t maxsz, const char *p, ...)
   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));
+  if (layout_string(&h->lyt, h->outbuf, n)) return (-1);
+  return (n);
 }
 
 static const struct gprintf_ops human_printops =
   { human_writech, human_writem, human_nwritef };
 
-/* Output methods. */
+/* --- @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_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 report_unusual(struct human_output *h,
                           unsigned nxfail, unsigned nskip)
 {
@@ -764,6 +735,19 @@ static void report_unusual(struct human_output *h,
 #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;
@@ -804,6 +788,20 @@ static int human_esession(struct tvec_output *o)
   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;
@@ -812,6 +810,21 @@ static void human_bgroup(struct tvec_output *o)
   dstr_reset(&h->scoreboard); show_progress(h);
 }
 
+/* --- @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)
 {
@@ -828,6 +841,18 @@ static void human_skipgroup(struct tvec_output *o,
   fputc('\n', h->lyt.fp);
 }
 
+/* --- @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;
@@ -851,15 +876,101 @@ static void human_egroup(struct tvec_output *o)
   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); }
 
+/* --- @human_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 human_report_location(struct human_output *h, FILE *fp,
+                                 const char *file, unsigned lno)
+{
+  unsigned f = 0;
+#define f_flush 1u
+
+  /* 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;
+
+#define FLUSH(fp) do if (f&f_flush) fflush(fp); while (0)
+
+    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);
+
+#undef FLUSH
+  }
+
+#undef f_flush
+}
+
+/* --- @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 *)o;
   struct tvec_state *tv = h->tv;
 
   clear_progress(h);
-  report_location(h, h->lyt.fp, tv->infile, tv->test_lno);
+  human_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); }
@@ -873,6 +984,22 @@ 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_dumpreg(struct tvec_output *o,
                          unsigned disp, const union tvec_regval *rv,
@@ -894,6 +1021,20 @@ static void human_dumpreg(struct tvec_output *o,
   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;
@@ -913,6 +1054,21 @@ static void human_etest(struct tvec_output *o, unsigned outcome)
   }
 }
 
+/* --- @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)
 {
@@ -920,40 +1076,107 @@ static void human_bbench(struct tvec_output *o,
   struct tvec_state *tv = h->tv;
 
   clear_progress(h);
-  fprintf(h->lyt.fp, "%s: %s: ", tv->test->name, ident); fflush(h->lyt.fp);
+  gprintf(&human_printops, h, "%s %s: ", tv->test->name, ident);
+  if (h->f&HOF_TTY) 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;
 
-  tvec_benchreport(&human_printops, h->lyt.fp, unit, tm);
-  fputc('\n', h->lyt.fp);
+  tvec_benchreport(&human_printops, h, unit, 0, tm);
+  layout_char(&h->lyt, '\n');
 }
 
+/* --- @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');
 
-  clear_progress(h); fflush(h->lyt.fp);
+  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);
-  fwrite(d.buf, 1, d.len, stderr);
+  human_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);
+    human_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);
   }
-  show_progress(h);
+  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;
@@ -973,6 +1196,20 @@ static const struct tvec_outops human_ops = {
   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;
@@ -1007,22 +1244,574 @@ struct tvec_output *tvec_humanoutput(FILE *fp)
   return (&h->_o);
 }
 
-/*----- Perl's `Test Anything Protocol' -----------------------------------*/
+/*----- Machine-readable output -------------------------------------------*/
 
-struct tap_output {
-  struct tvec_output _o;
-  struct tvec_state *tv;
-  struct layout lyt;
-  unsigned last;
-  char *outbuf; size_t outsz;
-  int maxlen;
+struct machine_output {
+  struct tvec_output _o;               /* output base class */
+  struct tvec_state *tv;               /* stashed testing state */
+  FILE *fp;                            /* output stream */
+  char *outbuf; size_t outsz;          /* buffer for formatted output */
+  unsigned grpix, testix;              /* group and test indices */
+  unsigned f;                          /* flags */
+#define MF_BENCH 1u
 };
 
-static int tap_writech(void *go, int ch)
-  { struct tap_output *t = go; return (layout_char(&t->lyt, ch)); }
-
-static int tap_writem(void *go, const char *p, size_t sz)
-  { struct tap_output *t = go; return (layout_string(&t->lyt, p, sz)); }
+/* --- @machine_writech@, @machine_write@, @machine_writef@ --- *
+ *
+ * Arguments:  @void *go@ = output sink, secretly a @struct machine_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 quoting.
+ */
+
+static void machine_escape(struct machine_output *m, int ch)
+{
+  switch (ch) {
+    case 0: fputs("\\0", m->fp); break;
+    case '\a': fputs("\\a", m->fp); break;
+    case '\b': fputs("\\b", m->fp); break;
+    case '\x1b': fputs("\\e", m->fp); break;
+    case '\f': fputs("\\f", m->fp); break;
+    case '\n': fputs("\\n", m->fp); break;
+    case '\r': fputs("\\r", m->fp); break;
+    case '\t': fputs("\\t", m->fp); break;
+    case '\v': fputs("\\v", m->fp); break;
+    case '"': fputs("\\\"", m->fp); break;
+    case '\\': fputs("\\\\", m->fp); break;
+    default: fprintf(m->fp, "\\x{%02x}", ch);
+  }
+}
+
+static int machine_writech(void *go, int ch)
+{
+  struct machine_output *m = go;
+
+  if (ISPRINT(ch)) putc(ch, m->fp);
+  else machine_escape(m, ch);
+  return (1);
+}
+
+static int machine_writem(void *go, const char *p, size_t sz)
+{
+  struct machine_output *m = go;
+  const char *q, *l = p + sz;
+
+  for (;;) {
+    q = p;
+    for (;;) {
+      if (q >= l) goto final;
+      if (!ISPRINT(*q) || *q == '\\' || *q == '"') break;
+      q++;
+    }
+    if (p < q) fwrite(p, 1, q - p, m->fp);
+    p = q; machine_escape(m, (unsigned char)*p++);
+  }
+final:
+  if (p < l) fwrite(p, 1, l - p, m->fp);
+  return (sz);
+}
+
+static int machine_nwritef(void *go, size_t maxsz, const char *p, ...)
+{
+  struct machine_output *m = go;
+  int n;
+  va_list ap;
+
+  va_start(ap, p);
+  n = gprintf_memputf(&m->outbuf, &m->outsz, maxsz, p, ap);
+  va_end(ap);
+  return (machine_writem(m, m->outbuf, n));
+}
+
+static const struct gprintf_ops machine_printops =
+  { machine_writech, machine_writem, machine_nwritef };
+
+/* --- @machine_maybe_quote@ --- *
+ *
+ * Arguments:  @struct machine_output *m@ = output sink
+ *             @const char *p@ = pointer to string
+ *
+ * Returns:    ---
+ *
+ * Use:                Print the string @p@, quoting it if necessary.
+ */
+
+static void machine_maybe_quote(struct machine_output *m, const char *p)
+{
+  const char *q;
+
+  for (q = p; *q; q++)
+    if (!ISPRINT(*q)) goto quote;
+  fputs(p, m->fp); return;
+quote:
+  putc('"', m->fp); machine_writem(m, p, strlen(p)); putc('"', m->fp);
+}
+
+/* --- @machine_bsession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     machine_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 void machine_bsession(struct tvec_output *o, struct tvec_state *tv)
+{
+  struct machine_output *m = (struct machine_output *)o;
+
+  m->tv = tv; m->grpix = 0;
+}
+
+/* --- @machine_show_stats@ --- *
+ *
+ * Arguments:  @struct machine_output *m@ = output sink
+ *             @unsigned *out[TVOUT_LIMIT]@ = outcome counter table
+ *
+ * Returns:    ---
+ *
+ * Use:                Print a machine readable outcome statistics table
+ */
+
+static void machine_show_stats(struct machine_output *m,
+                              unsigned out[TVOUT_LIMIT])
+{
+  static const char *outtab[] = { "lose", "skip", "xfail", "win" };
+  unsigned i;
+
+  for (i = 0; i < TVOUT_LIMIT; i++) {
+    if (i) putc(' ', m->fp);
+    fprintf(m->fp, "%s=%u", outtab[i], out[i]);
+  }
+}
+
+/* --- @machine_esession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     machine_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 machine_esession(struct tvec_output *o)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  struct tvec_state *tv = m->tv;
+
+  fputs("END groups: ", m->fp);
+  machine_show_stats(m, tv->grps);
+  fputs("; tests: ", m->fp);
+  machine_show_stats(m, tv->all);
+  putc('\n', m->fp);
+  m->tv = 0; return (tv->f&TVSF_ERROR ? 2 : tv->all[TVOUT_LOSE] ? 1 : 0);
+}
+
+/* --- @machine_bgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     machine_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 machine_bgroup(struct tvec_output *o)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  struct tvec_state *tv = m->tv;
+
+  fputs("BGROUP ", m->fp);
+  machine_maybe_quote(m, tv->test->name);
+  putc('\n', m->fp);
+  m->grpix++; m->testix = 0;
+}
+
+/* --- @machine_skipgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     machine_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 machine_skipgroup(struct tvec_output *o,
+                         const char *excuse, va_list *ap)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  struct tvec_state *tv = m->tv;
+
+  fputs("BGROUP ", m->fp);
+  machine_maybe_quote(m, tv->test->name);
+  if (excuse) {
+    fputs(" \"", m->fp);
+    vgprintf(&machine_printops, m, excuse, ap);
+    putc('\"', m->fp);
+  }
+  putc('\n', m->fp);
+}
+
+/* --- @machine_egroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     machine_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group has finished.
+ *
+ *             The TAP driver reports a summary of the group's tests.
+ */
+
+static void machine_egroup(struct tvec_output *o)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  struct tvec_state *tv = m->tv;
+
+  fputs("EGROUP ", m->fp); machine_maybe_quote(m, tv->test->name);
+  putc(' ', m->fp); machine_show_stats(m, tv->curr);
+  putc('\n', m->fp);
+}
+
+/* --- @machine_btest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     machine_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 machine_btest(struct tvec_output *o) { ; }
+
+/* --- @machine_report_location@ --- *
+ *
+ * Arguments:  @struct human_output *h@ = output state
+ *             @const char *file@ = filename
+ *             @unsigned lno@ = line number
+ *
+ * Returns:    ---
+ *
+ * Use:                Print the filename and line number to the output stream.
+ */
+
+static void machine_report_location(struct machine_output *m,
+                                   const char *file, unsigned lno)
+  { machine_maybe_quote(m, file); fprintf(m->fp, ":%u", lno); }
+
+/* --- @machine_outcome@, @machine_skip@, @machine_fail@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     machine_output@
+ *             @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.
+ */
+
+static void machine_outcome(struct tvec_output *o, const char *outcome,
+                           const char *detail, va_list *ap)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  struct tvec_state *tv = m->tv;
+
+  fprintf(m->fp, "%s %u ", outcome, m->testix);
+  machine_report_location(m, tv->infile, tv->test_lno);
+  if (detail)
+    { putc(' ', m->fp); vgprintf(&machine_printops, m, detail, ap); }
+  putc('\n', m->fp);
+}
+
+static void machine_skip(struct tvec_output *o,
+                        const char *excuse, va_list *ap)
+  { machine_outcome(o, "SKIP", excuse, ap); }
+static void machine_fail(struct tvec_output *o,
+                        const char *detail, va_list *ap)
+  { machine_outcome(o, "FAIL", detail, ap); }
+
+/* --- @machine_dumpreg@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     machine_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 machine_dumpreg(struct tvec_output *o,
+                       unsigned disp, const union tvec_regval *rv,
+                       const struct tvec_regdef *rd)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  unsigned f = 0;
+#define f_reg 1u
+#define f_nl 2u
+
+  switch (disp) {
+    case TVRD_INPUT: fputs("\tINPUT ", m->fp); f |= f_reg | f_nl; break;
+    case TVRD_OUTPUT: fputs("\tOUTPUT ", m->fp); f |= f_reg | f_nl; break;
+    case TVRD_MATCH: fputs("\tMATCH ", m->fp); f |= f_reg | f_nl; break;
+    case TVRD_FOUND: fputs("\tMISMATCH ", m->fp); f |= f_reg; break;
+    case TVRD_EXPECT: fputs(" /= ", m->fp); f |= f_nl; break;
+    default: abort();
+  }
+
+  if (f&f_reg) fprintf(m->fp, "%s = ", rd->name);
+  rd->ty->dump(rv, rd, TVSF_RAW, &file_printops, m->fp);
+  if (f&f_nl) putc('\n', m->fp);
+
+#undef f_reg
+#undef f_newline
+}
+
+/* --- @machine_etest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     machine_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.
+ */
+
+static void machine_etest(struct tvec_output *o, unsigned outcome)
+{
+  struct machine_output *m = (struct machine_output *)o;
+
+  if (!(m->f&MF_BENCH)) switch (outcome) {
+    case TVOUT_WIN: machine_outcome(o, "WIN", 0, 0); break;
+    case TVOUT_XFAIL: machine_outcome(o, "XFAIL", 0, 0); break;
+  }
+  m->testix++; m->f &= ~MF_BENCH;
+}
+
+/* --- @machine_bbench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     machine_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 @machine_ebench@.
+ */
+
+static void machine_bbench(struct tvec_output *o,
+                          const char *ident, unsigned unit)
+  { ; }
+
+/* --- @machine_ebench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     machine_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 machine_ebench(struct tvec_output *o,
+                          const char *ident, unsigned unit,
+                          const struct bench_timing *tm)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  struct tvec_state *tv = m->tv;
+
+  fprintf(m->fp, "BENCH %u ", m->testix);
+  machine_report_location(m, tv->infile, tv->test_lno);
+  fprintf(m->fp, " %s: ", ident);
+  tvec_benchreport(&file_printops, m->fp, unit, TVSF_RAW, tm);
+  putc('\n', m->fp); m->f |= MF_BENCH;
+}
+
+/* --- @machine_report@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     machine_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 machine_report(struct tvec_output *o, unsigned level,
+                          const char *msg, va_list *ap)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  const char *p;
+
+  for (p = tvec_strlevel(level); *p; p++) putc(TOUPPER(*p), m->fp);
+  putc(' ', m->fp);
+  putc('"', m->fp);
+  vgprintf(&machine_printops, m, msg, ap);
+  putc('"', m->fp);
+  putc('\n', m->fp);
+}
+
+/* --- @machine_destroy@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a @struct
+ *                     machine_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Release the resources held by the output driver.
+ */
+
+static void machine_destroy(struct tvec_output *o)
+{
+  struct machine_output *m = (struct machine_output *)o;
+
+  if (m->fp != stdout && m->fp != stderr) fclose(m->fp);
+  xfree(m->outbuf); xfree(m);
+}
+
+static const struct tvec_outops machine_ops = {
+  machine_bsession, machine_esession,
+  machine_bgroup, machine_skipgroup, machine_egroup,
+  machine_btest, machine_skip, machine_fail, machine_dumpreg, machine_etest,
+  machine_bbench, machine_ebench,
+  machine_report,
+  machine_destroy
+};
+
+/* --- @tvec_machineoutput@ --- *
+ *
+ * Arguments:  @FILE *fp@ = output file to write on
+ *
+ * Returns:    An output formatter.
+ *
+ * Use:                Return an output formatter which writes on @fp@ in a
+ *             moderately simple machine-readable format.
+ */
+
+struct tvec_output *tvec_machineoutput(FILE *fp)
+{
+  struct machine_output *m;
+
+  m = xmalloc(sizeof(*m)); m->_o.ops = &machine_ops;
+  m->fp = fp; m->outbuf = 0; m->outsz = 0; m->testix = 0;
+  return (&m->_o);
+}
+
+/*----- Perl's `Test Anything Protocol' -----------------------------------*/
+
+struct tap_output {
+  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 */
+};
+
+/* --- @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.
+ */
+
+static int tap_writech(void *go, int ch)
+{
+  struct tap_output *t = go;
+
+  if (layout_char(&t->lyt, ch)) return (-1);
+  else return (1);
+}
+
+static int tap_writem(void *go, const char *p, size_t sz)
+{
+  struct tap_output *t = go;
+
+  if (layout_string(&t->lyt, p, sz)) return (-1);
+  else return (sz);
+}
 
 static int tap_nwritef(void *go, size_t maxsz, const char *p, ...)
 {
@@ -1033,12 +1822,28 @@ static int tap_nwritef(void *go, size_t maxsz, const char *p, ...)
   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));
+  if (layout_string(&t->lyt, t->outbuf, n)) return (-1);
+  return (n);
 }
 
 static const struct gprintf_ops tap_printops =
   { tap_writech, tap_writem, tap_nwritef };
 
+/* --- @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 void tap_bsession(struct tvec_output *o, struct tvec_state *tv)
 {
   struct tap_output *t = (struct tap_output *)o;
@@ -1047,6 +1852,20 @@ static void tap_bsession(struct tvec_output *o, struct tvec_state *tv)
   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)
 {
@@ -1064,6 +1883,20 @@ static int tap_esession(struct tvec_output *o)
   t->tv = 0; return (tv->all[TVOUT_LOSE] ? 1 : 0);
 }
 
+/* --- @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_bgroup(struct tvec_output *o)
 {
   struct tap_output *t = (struct tap_output *)o;
@@ -1074,6 +1907,21 @@ static void tap_bgroup(struct tvec_output *o)
   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)
 {
@@ -1085,6 +1933,18 @@ static void tap_skipgroup(struct tvec_output *o,
   fputc('\n', t->lyt.fp);
 }
 
+/* --- @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_egroup(struct tvec_output *o)
 {
   struct tap_output *t = (struct tap_output *)o;
@@ -1096,9 +1956,44 @@ static void tap_egroup(struct tvec_output *o)
          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@
+ *             @const char *head, *tail@ = outcome strings 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.
+ */
+
 static void tap_outcome(struct tvec_output *o,
                        const char *head, const char *tail,
                        const char *detail, va_list *ap)
@@ -1120,6 +2015,24 @@ static void tap_skip(struct tvec_output *o, const char *excuse, va_list *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)
@@ -1127,6 +2040,7 @@ static void tap_dumpreg(struct tvec_output *o,
   struct tap_output *t = (struct tap_output *)o;
   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>");
@@ -1134,6 +2048,20 @@ static void tap_dumpreg(struct tvec_output *o,
   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.
+ */
+
 static void tap_etest(struct tvec_output *o, unsigned outcome)
 {
   switch (outcome) {
@@ -1146,10 +2074,42 @@ static void tap_etest(struct tvec_output *o, unsigned outcome)
   }
 }
 
+/* --- @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_bbench(struct tvec_output *o,
                       const char *ident, unsigned unit)
   { ; }
 
+/* --- @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)
@@ -1157,28 +2117,56 @@ static void tap_ebench(struct tvec_output *o,
   struct tap_output *t = (struct tap_output *)o;
   struct tvec_state *tv = t->tv;
 
-  gprintf(&tap_printops, t, "%s: %s: ", tv->test->name, ident);
-  tvec_benchreport(&tap_printops, t, unit, tm);
+  set_layout_prefix(&t->lyt, "    ## ");
+  gprintf(&tap_printops, t, "%s %s: ", tv->test->name, ident);
+  tvec_benchreport(&tap_printops, t, unit, 0, 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;
-  const struct gprintf_ops *gops; void *go;
 
-  if (level >= TVLEV_ERR) {
-    fputs("Bail out!  ", t->lyt.fp);
-    gops = &file_printops; go = t->lyt.fp;
-  } else {
-    gops = &tap_printops; go = t;
-  }
-  if (tv->infile) gprintf(gops, go, "%s:%u: ", tv->infile, tv->lno);
-  gprintf(gops, go, msg, ap); gops->putch(go, '\n');
+  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;
@@ -1197,18 +2185,53 @@ static const struct tvec_outops tap_ops = {
   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;
-  init_layout(&t->lyt, 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);