chiark / gitweb /
@@@ more mess
[mLib] / test / tvec-output.c
index 4e2ea64582ede7a8af21564807b740a27ff4fe72..014423e0a831defd07a54ae0be9398ec322f430e 100644 (file)
@@ -36,6 +36,7 @@
 #include <string.h>
 
 #include <unistd.h>
+#include <sys/stat.h>
 
 #include "alloc.h"
 #include "bench.h"
@@ -43,6 +44,7 @@
 #include "macros.h"
 #include "quis.h"
 #include "report.h"
+#include "ttycolour.h"
 
 #include "tvec.h"
 #include "tvec-bench.h"
@@ -70,37 +72,51 @@ static const char *regdisp(unsigned disp)
   }
 }
 
-/* --- @getenv_boolean@ --- *
+/* --- @interpret_boolean@, @getenv_boolean@ --- *
  *
- * Arguments:  @const char *var@ = environment variable name
+ * Arguments:  @const char *val@ = a string
+ *             @const char *var@ = environment variable name
  *             @int dflt@ = default value
+ *             @const char *what, ...@ = format string describing where the
+ *                     setting came from
  *
- * Returns:    @0@ if the variable is set to something falseish, @1@ if it's
- *             set to something truish, or @dflt@ otherwise.
+ * Returns:    @0@ if the string, or 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)
+static PRINTF_LIKE(3, 4)
+  int interpret_boolean(const char *val, int dflt, const char *what, ...)
 {
-  const char *p;
-
-  p = getenv(var);
-  if (!p)
-    return (dflt);
-  else if (STRCMP(p, ==, "y") || STRCMP(p, ==, "yes") ||
-          STRCMP(p, ==, "t") || STRCMP(p, ==, "true") ||
-          STRCMP(p, ==, "on") || STRCMP(p, ==, "force") ||
-          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);
+  dstr d = DSTR_INIT;
+  va_list ap;
+  int rc;
+
+  if (!val)
+    rc = dflt;
+  else if (STRCMP(val, ==, "y") || STRCMP(val, ==, "yes") ||
+          STRCMP(val, ==, "t") || STRCMP(val, ==, "true") ||
+          STRCMP(val, ==, "on") || STRCMP(val, ==, "force") ||
+          STRCMP(val, ==, "1"))
+    rc = 1;
+  else if (STRCMP(val, ==, "n") || STRCMP(val, ==, "no") ||
+          STRCMP(val, ==, "nil") || STRCMP(val, ==, "f") ||
+            STRCMP(val, ==, "false") ||
+          STRCMP(val, ==, "off") || STRCMP(val, ==, "inhibit") ||
+          STRCMP(val, ==, "0"))
+    rc = 0;
   else {
-    moan("ignoring unexpected value `%s' for environment variable `%s'",
-        var, p);
-    return (dflt);
+    va_start(ap, what); dstr_vputf(&d, what, &ap); va_end(ap);
+    moan("ignoring unexpected value `%s' for %s", val, d.buf);
+    rc = dflt;
   }
+  dstr_destroy(&d); return (rc);
+}
+
+static int getenv_boolean(const char *var, int dflt)
+{
+  return (interpret_boolean(getenv(var), dflt,
+                           "environment variable `%s'", var));
 }
 
 /* --- @register_maxnamelen@ --- *
@@ -166,6 +182,7 @@ struct layout {
   FILE *fp;                             /* output file */
   const char *prefix, *pfxtail, *pfxlim; /* prefix pointers */
   dstr w;                              /* trailing whitespace */
+  dstr ctrl;                           /* control sequence for next word */
   unsigned f;                          /* flags */
 #define LYTF_NEWL 1u                   /*   start of output line */
 };
@@ -211,6 +228,13 @@ struct layout {
   if (_n && fwrite(lyt->w.buf, 1, _n, lyt->fp) < _n) return (-1);      \
 } while (0)
 
+#define PUT_CTRL do {                                                  \
+  /* Output the accumulated control string. */                         \
+                                                                       \
+  size_t _n = lyt->ctrl.len;                                           \
+  if (_n && fwrite(lyt->ctrl.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.                                        \
@@ -266,7 +290,7 @@ static void init_layout(struct layout *lyt, FILE *fp, const char *prefix)
 {
   lyt->fp = fp;
   lyt->f = LYTF_NEWL;
-  dstr_create(&lyt->w);
+  dstr_create(&lyt->w); dstr_create(&lyt->ctrl);
   set_layout_prefix(lyt, prefix);
 }
 
@@ -286,7 +310,7 @@ static void init_layout(struct layout *lyt, FILE *fp, const char *prefix)
 static void destroy_layout(struct layout *lyt, unsigned f)
 {
   if (f&DLF_CLOSE) fclose(lyt->fp);
-  dstr_destroy(&lyt->w);
+  dstr_destroy(&lyt->w); dstr_destroy(&lyt->ctrl);
 }
 
 /* --- @layout_char@ --- *
@@ -400,7 +424,8 @@ static int layout_string(struct layout *lyt, const char *p, size_t sz)
 
     if (r > p) {
       if (lyt->f&LYTF_NEWL) { PUT_PREFIX; lyt->f &= ~LYTF_NEWL; }
-      PUT_SAVED; PUT_NONBLANK; DRESET(&lyt->w);
+      PUT_SAVED; PUT_CTRL; PUT_NONBLANK;
+      DRESET(&lyt->w); DRESET(&lyt->ctrl);
     }
     SAVE_TAIL;
     return (0);
@@ -411,7 +436,8 @@ static int layout_string(struct layout *lyt, const char *p, size_t sz)
    */
   if (r > p) {
     if (lyt->f&LYTF_NEWL) PUT_PREFIX;
-    PUT_SAVED; PUT_NONBLANK;
+    PUT_SAVED; PUT_CTRL; PUT_NONBLANK;
+    DRESET(&lyt->ctrl);
   } else if (lyt->f&LYTF_NEWL)
     PUT_PFXINB;
   PUT_NEWLINE; DRESET(&lyt->w);
@@ -422,7 +448,7 @@ static int layout_string(struct layout *lyt, const char *p, size_t sz)
    * newline, so we write the initial prefix and drop the trailing blanks.
    */
   while (q) {
-    if (r > p) { PUT_PREFIX; PUT_NONBLANK; }
+    if (r > p) { PUT_PREFIX; PUT_CTRL; PUT_NONBLANK; DRESET(&lyt->ctrl); }
     else PUT_PFXINB;
     PUT_NEWLINE;
     SPLIT_SEGMENT;
@@ -455,54 +481,45 @@ static int layout_string(struct layout *lyt, const char *p, size_t sz)
 
 /*----- Human-readable output ---------------------------------------------*/
 
-/* 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
-#define HCOL_BLUE 4u
-#define HCOL_MAGENTA 5u
-#define HCOL_CYAN 6u
-#define HCOL_WHITE 7u
-#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_INFO 0                      /* information */
-#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 */
+#define HIGHLIGHTS(_st, _)                                             \
+  _(_st, LOCFN,           "lf", TC_FG(CYAN))   /* location filename */         \
+  _(_st, LOCLN,           "ln", TC_FG(CYAN))   /* location line number */      \
+  _(_st, LOCSEP,   "ls", TC_FG(BRBLUE))        /* location separator `:' */    \
+  _(_st, INFO,    "mi", 0)             /* information */               \
+  _(_st, NOTE,    "mn", TC_FG(YELLOW)) /* notices */                   \
+  _(_st, ERR,     "me", TC_FG(MAGENTA) | TCAF_BOLD) /* error messages */       \
+  _(_st, UNKLEV,   "mu", TC_FG(WHITE) | TC_BG(RED) | TCAF_BOLD)                \
+  _(_st, DSINPUT,  "di", 0)            /* disposition for input value */ \
+  _(_st, DSOUTPUT, "do", 0)            /* ... unsolicited output */    \
+  _(_st, DSMATCH,  "dm", 0)            /* ... matching output */       \
+  _(_st, DSFOUND,  "df", 0)            /* ... incorrect output */      \
+  _(_st, DSEXPECT, "dx", 0)            /* ... reference output */      \
+  _(_st, RNINPUT,  "ri", 0)            /* register name for input value */ \
+  _(_st, RNOUTPUT, "ro", 0)            /* ... unsolicited output */    \
+  _(_st, RNMATCH,  "rm", 0)            /* ... matching output */       \
+  _(_st, RNFOUND,  "rf", 0)            /* ... incorrect output */      \
+  _(_st, RNEXPECT, "rx", 0)            /* ... reference output */      \
+  _(_st, VINPUT,   "vi", 0)            /* input value */               \
+  _(_st, VOUTPUT,  "vo", 0)            /* unsolicited output value */  \
+  _(_st, VMATCH,   "vm", 0)            /* matching output value */     \
+  _(_st, VFOUND,   "vf", TC_FG(BRRED)) /* incorrect output value */    \
+  _(_st, VEXPECT,  "vx", TC_FG(GREEN)) /* reference output value */    \
+  _(_st, VUNSET,   "vu", TC_FG(YELLOW))        /* register not set */          \
+  _(_st, LOSE,    "ol", TC_FG(RED) | TCAF_BOLD) /* report failure */   \
+  _(_st, SKIP,    "os", TC_FG(YELLOW)) /* report a skipped test/group */ \
+  _(_st, XFAIL,           "ox", TC_FG(BLUE) | TCAF_BOLD) /* report expected fail */ \
+  _(_st, WIN,     "ow", TC_FG(GREEN))  /* report success */            \
+  _(_st, SBLOSE,   "sl", TC_FG(RED) | TCAF_BOLD) /* scoreboard failure */      \
+  _(_st, SBSKIP,   "ss", TC_FG(YELLOW))        /* scoreboard skipped test */   \
+  _(_st, SBXFAIL,  "sx", TC_FG(BLUE) | TCAF_BOLD) /* scoreboard xfail */       \
+  _(_st, SBWIN,           "sw", 0)             /* scoreboard success */
+
+TTYCOLOUR_DEFENUM(HIGHLIGHTS, HL_);
+#define HL_PLAIN (-1)
 
 /* 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 */
+static const char scoreboard[] = { 'x', '_', 'o', '.' };
 
 struct human_output {
   struct tvec_output _o;               /* output base class */
@@ -511,101 +528,38 @@ struct human_output {
   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 */
+  unsigned short attr[HL__LIMIT];      /* highlight attribute map */
+  struct ttycolour_state tc;           /* terminal colour state */
   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 */
+                                       /*   bits 0--7 from @TVHF_...@ */
+#define HOF_DUPERR 0x0100u             /*   duplicate errors to stderr */
+#define HOF_PROGRESS 0x0200u           /*   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)
-{
-  if (*sep_inout) putc(*sep_inout, fp);
-  fprintf(fp, "%s%d", colour&HCF_BRIGHT ? bright : norm, colour&7);
-  *sep_inout = ';';
-}
-
-/* --- @setattr@ --- *
+/* --- @setattr@, @setattr_layout@ --- *
  *
  * Arguments:  @struct human_output *h@ = output state
- *             @unsigned attr@ = attribute code to set
+ *             @int hi@ = highlight 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)
+static void setattr_common(struct human_output *h,
+                          const struct gprintf_ops *gops, void *go, int hl)
 {
-  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;
-
-  /* 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->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->lyt.fp, &sep, "3", "9",
-                (attr&HAF_FGMASK) >> HAF_FGSHIFT);
-    else {
-      if (sep) putc(sep, h->lyt.fp);
-      fputs("39", h->lyt.fp); sep = ';';
-    }
-  }
+  if (h->f&TVHF_COLOUR)
+    ttycolour_setattr(gops, go, &h->tc, hl < 0 ? 0 : h->attr[hl]);
+}
 
-  /* Change the background colour if necessary. */
-  if (diff&(HAF_BG | HAF_BGMASK)) {
-    if (attr&HAF_BG)
-      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 = ';';
-    }
-  }
+static void setattr(struct human_output *h, int hl)
+  { setattr_common(h, &file_printops, h->lyt.fp, hl); }
 
-  /* Terminate the control command and save the new attributes. */
-  putc('m', h->lyt.fp); h->attr = attr;
-}
+static void setattr_layout(struct human_output *h, int hl)
+  { setattr_common(h, &dstr_printops, &h->lyt.ctrl, hl); }
 
 /* --- @clear_progress@ --- *
  *
@@ -642,13 +596,8 @@ static void clear_progress(struct human_output *h)
 
 static void write_scoreboard_char(struct human_output *h, int ch)
 {
-  switch (ch) {
-    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->lyt.fp); setattr(h, HA_PLAIN);
+  assert(0 <= ch && ch < TVOUT_LIMIT);
+  setattr(h, HL_SBLOSE + ch); putc(scoreboard[ch], h->lyt.fp);
 }
 
 /* --- @show_progress@ --- *
@@ -669,12 +618,11 @@ static void show_progress(struct human_output *h)
   struct tvec_state *tv = h->tv;
   const char *p, *l;
 
-  if (tv->test && (h->f&HOF_TTY) && !(h->f&HOF_PROGRESS)) {
+  if (tv->test && (h->f&TVHF_TTY) && !(h->f&HOF_PROGRESS)) {
     fprintf(h->lyt.fp, "%s: ", tv->test->name);
-    if (!(h->f&HOF_COLOUR))
-      dstr_write(&h->scoreboard, h->lyt.fp);
-    else for (p = h->scoreboard.buf, l = p + h->scoreboard.len; p < l; p++)
+    for (p = h->scoreboard.buf, l = p + h->scoreboard.len; p < l; p++)
       write_scoreboard_char(h, *p);
+    setattr(h, HL_PLAIN);
     fflush(h->lyt.fp); h->f |= HOF_PROGRESS;
   }
 }
@@ -736,7 +684,7 @@ static const struct gprintf_ops human_printops =
 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@ --- *
+/* --- @human_report_unusual@ --- *
  *
  * Arguments:  @struct human_output *h@ = output sink
  *             @unsigned nxfail, nskip@ = number of expected failures and
@@ -748,25 +696,24 @@ static void human_bsession(struct tvec_output *o, struct tvec_state *tv)
  *             failures and/or skipped tests, if there were any.
  */
 
-static void report_unusual(struct human_output *h,
-                          unsigned nxfail, unsigned nskip)
+static void human_report_unusual(struct human_output *h,
+                                unsigned nxfail, unsigned nskip)
 {
-  const char *sep = " (";
   unsigned f = 0;
 #define f_any 1u
 
   if (nxfail) {
-    fprintf(h->lyt.fp, "%s%u ", sep, nxfail);
-    setattr(h, HA_XFAIL);
+    fprintf(h->lyt.fp, "%s%u ", f&f_any ? ", " : " (", nxfail);
+    setattr(h, HL_XFAIL);
     fprintf(h->lyt.fp, "expected %s", nxfail == 1 ? "failure" : "failures");
-    setattr(h, HA_PLAIN);
-    sep = ", "; f |= f_any;
+    setattr(h, HL_PLAIN);
+    f |= f_any;
   }
 
   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;
+    fprintf(h->lyt.fp, "%s%u ", f&f_any ? ", " : " (", nskip);
+    setattr(h, HL_SKIP); fputs("skipped", h->lyt.fp); setattr(h, HL_PLAIN);
+    f |= f_any;
   }
 
   if (f&f_any) fputc(')', h->lyt.fp);
@@ -800,27 +747,27 @@ static int human_esession(struct tvec_output *o)
     grps_run = grps_win + grps_lose;
 
   if (!all_lose) {
-    setattr(h, HA_WIN); fputs("PASSED", h->lyt.fp); setattr(h, HA_PLAIN);
+    setattr(h, HL_WIN); fputs("PASSED", h->lyt.fp); setattr(h, HL_PLAIN);
     fprintf(h->lyt.fp, " %s%u %s",
            !(all_skip || grps_skip) ? "all " : "",
            all_pass, all_pass == 1 ? "test" : "tests");
-    report_unusual(h, all_xfail, all_skip);
+    human_report_unusual(h, all_xfail, all_skip);
     fprintf(h->lyt.fp, " in %u %s",
            grps_win, grps_win == 1 ? "group" : "groups");
-    report_unusual(h, 0, grps_skip);
+    human_report_unusual(h, 0, grps_skip);
   } else {
-    setattr(h, HA_LOSE); fputs("FAILED", h->lyt.fp); setattr(h, HA_PLAIN);
+    setattr(h, HL_LOSE); fputs("FAILED", h->lyt.fp); setattr(h, HL_PLAIN);
     fprintf(h->lyt.fp, " %u out of %u %s",
            all_lose, all_run, all_run == 1 ? "test" : "tests");
-    report_unusual(h, all_xfail, all_skip);
+    human_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_unusual(h, 0, grps_skip);
+    human_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);
+    setattr(h, HL_ERR); fputs("ERRORS", h->lyt.fp); setattr(h, HL_PLAIN);
     fputs(" found in input; tests may not have run correctly\n", h->lyt.fp);
   }
 
@@ -869,13 +816,13 @@ static void human_skipgroup(struct tvec_output *o,
 {
   struct human_output *h = (struct human_output *)o;
 
-  if (!(h->f&HOF_TTY))
+  if (!(h->f&TVHF_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);
   }
-  setattr(h, HA_SKIP); fputs("skipped", h->lyt.fp); setattr(h, HA_PLAIN);
+  setattr(h, HL_SKIP); fputs("skipped", h->lyt.fp); setattr(h, HL_PLAIN);
   if (excuse) { fputs(": ", h->lyt.fp); vfprintf(h->lyt.fp, excuse, *ap); }
   fputc('\n', h->lyt.fp);
 }
@@ -900,17 +847,17 @@ static void human_egroup(struct tvec_output *o)
     lose = tv->curr[TVOUT_LOSE], skip = tv->curr[TVOUT_SKIP],
     run = win + lose + xfail;
 
-  if (h->f&HOF_TTY) h->f &= ~HOF_PROGRESS;
+  if (h->f&TVHF_TTY) h->f &= ~HOF_PROGRESS;
   else fprintf(h->lyt.fp, "%s:", h->tv->test->name);
 
   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);
+    setattr(h, HL_LOSE); fputs("FAILED", h->lyt.fp); setattr(h, HL_PLAIN);
+    human_report_unusual(h, xfail, skip);
   } else {
-    fputc(' ', h->lyt.fp); setattr(h, HA_WIN);
-    fputs("ok", h->lyt.fp); setattr(h, HA_PLAIN);
-    report_unusual(h, xfail, skip);
+    fputc(' ', h->lyt.fp); setattr(h, HL_WIN);
+    fputs("ok", h->lyt.fp); setattr(h, HL_PLAIN);
+    human_report_unusual(h, xfail, skip);
   }
   fputc('\n', h->lyt.fp);
 }
@@ -948,40 +895,17 @@ static void human_btest(struct tvec_output *o)
 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))
+  else if (fp != h->lyt.fp || !(h->f&TVHF_COLOUR))
     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
+    setattr(h, HL_LOCFN);      fputs(file, fp);
+    setattr(h, HL_LOCSEP);     fputc(':', fp);
+    setattr(h, HL_LOCLN);      fprintf(fp, "%u", lno);
+    setattr(h, HL_LOCSEP);     fputc(':', fp);
+    setattr(h, HL_PLAIN);      fputc(' ', fp);
   }
-
-#undef f_flush
 }
 
 /* --- @human_outcome@, @human_skip@, @human_fail@ --- *
@@ -1011,17 +935,17 @@ static void human_outcome(struct tvec_output *o,
   clear_progress(h);
   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);
+  setattr(h, attr); fputs(outcome, h->lyt.fp); setattr(h, HL_PLAIN);
   if (detail) { fputs(": ", h->lyt.fp); vfprintf(h->lyt.fp, detail, *ap); }
   fputc('\n', h->lyt.fp);
 }
 
 static void human_skip(struct tvec_output *o,
                       const char *excuse, va_list *ap)
-  { human_outcome(o, HA_SKIP, "skipped", excuse, ap); }
+  { human_outcome(o, HL_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_outcome(o, HL_LOSE, "FAILED", detail, ap); }
 
 /* --- @human_dumpreg@ --- *
  *
@@ -1048,16 +972,21 @@ static void human_dumpreg(struct tvec_output *o,
   const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name);
 
   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);
+  gprintf(&human_printops, h, "%*s", 10 + h->maxlen - n, "");
+  setattr_layout(h, HL_DSINPUT + disp);
+  gprintf(&human_printops, h, "%s", ds);
+  setattr(h, HL_PLAIN); layout_char(&h->lyt, ' ');
+  setattr_layout(h, HL_RNINPUT + disp);
+  gprintf(&human_printops, h, "%s", rd->name);
+  setattr(h, HL_PLAIN); gprintf(&human_printops, h, " = ");
+  if (!rv) {
+    setattr_layout(h, HL_VUNSET);
+    gprintf(&human_printops, h, "#unset");
+  } else {
+    setattr_layout(h, HL_VINPUT + disp);
+    rd->ty->dump(rv, rd, 0, &human_printops, h);
   }
-  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');
+  setattr(h, HL_PLAIN); layout_char(&h->lyt, '\n');
 }
 
 /* --- @human_etest@ --- *
@@ -1077,19 +1006,11 @@ static void human_dumpreg(struct tvec_output *o,
 static void human_etest(struct tvec_output *o, unsigned outcome)
 {
   struct human_output *h = (struct human_output *)o;
-  int ch;
 
-  if (h->f&HOF_TTY) {
+  if (h->f&TVHF_TTY) {
     show_progress(h);
-    switch (outcome) {
-      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->lyt.fp);
+    dstr_putc(&h->scoreboard, outcome); write_scoreboard_char(h, outcome);
+    setattr(h, HL_PLAIN); fflush(h->lyt.fp);
   }
 }
 
@@ -1115,44 +1036,37 @@ static void human_report(struct tvec_output *o, unsigned level,
 {
   struct human_output *h = (struct human_output *)o;
   struct tvec_state *tv = h->tv;
-  const char *levstr; unsigned levattr;
+  const char *levstr; unsigned levhl;
   dstr d = DSTR_INIT;
   unsigned f = 0;
-#define f_flush 1u
-#define f_progress 2u
+#define f_progress 1u
 
   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;
+    case TVLEV_##tag: levstr = name; levhl = HL_##tag; break;
     TVEC_LEVELS(CASE)
-    default: levstr = "??"; levattr = HA_UNKLEV; break;
+    default: levstr = "??"; levhl = HL_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);
-  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_PROGRESS) { clear_progress(h); f |= f_progress; }
 
   if (h->f&HOF_DUPERR) {
-    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);
+    fprintf(stderr, "%s: ", QUIS);
+    human_report_location(h, stderr, tv->infile, tv->lno);
+    fprintf(stderr, "%s: ", levstr);
+    fwrite(d.buf, 1, d.len, stderr);
   }
-  if (f&f_progress) show_progress(h);
 
+  human_report_location(h, h->lyt.fp, tv->infile, tv->lno);
+  setattr(h, levhl); fputs(levstr, h->lyt.fp);
+  setattr(h, HL_PLAIN); fputs(": ", h->lyt.fp);
+  fwrite(d.buf, 1, d.len, h->lyt.fp);
+
+  if (f&f_progress) show_progress(h);
   dstr_destroy(&d);
 
-#undef f_flush
 #undef f_progress
 }
 
@@ -1182,7 +1096,7 @@ static void human_bbench(struct tvec_output *o,
   if (desc) gprintf(&human_printops, h, "%s", desc);
   else print_ident(tv, TVSF_COMPACT, &human_printops, h);
   gprintf(&human_printops, h, ": ");
-  if (h->f&HOF_TTY) fflush(h->lyt.fp);
+  if (h->f&TVHF_TTY) fflush(h->lyt.fp);
 }
 
 /* --- @human_ebench@ --- *
@@ -1259,47 +1173,94 @@ static const struct tvec_outops human_ops = {
 /* --- @tvec_humanoutput@ --- *
  *
  * Arguments:  @FILE *fp@ = output file to write on
+ *             @unsigned f, m@ = flags and mask
  *
  * 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.
+ *             expectation that a human will interpret the output.
+ *
+ *             The flags @f@ and mask @m@ operate together.  Flag bits not
+ *             covered by the mask must be left clear, i.e., @f&~m$ must be
+ *             zero; the semantics are that a set mask bit indicates that
+ *             the corresponding bit of @f@ should control the indicated
+ *             behaviour; a clear mask bit indicates that a suitable default
+ *             should be chosen based on environmental conditions.
+ *
+ *             If @TVHF_TTY@ is set, then the output shows a `scoreboard'
+ *             indicating the outcome of each test case attempted, providing
+ *             a visual indication of progress.  If @TVHF_COLOUR@ is set,
+ *             then the output uses control codes for colour and other
+ *             highlighting.  It is unusual to set @TVHF_COLOUR@ without
+ *             @TVHF_TTY@, this is permitted anyway.
+ *
+ *             The environment variables %|TVEC_TTY|% and %|TVEC_COLOUR|%
+ *             provide default values for these settings.  If they are not
+ *             set, then @TVHF_TTY@ is set if @fp@ refers to a terminal, and
+ *             @TVHF_COLOUR@ is set if @TVHF_TTY@ is set and, additionally,
+ *             the %|TERM|% environment variable is set to a value other
+ *             than %|dumb|%.
  */
 
-struct tvec_output *tvec_humanoutput(FILE *fp)
+struct tvec_output *tvec_humanoutput(FILE *fp, unsigned f, unsigned m)
 {
   struct human_output *h;
   const char *p;
+  struct stat st_out, st_err;
+  int rc_out, rc_err;
 
-  XNEW(h); h->a = arena_global; h->_o.ops = &human_ops;
-  h->f = 0; h->attr = 0;
+  static const struct ttycolour_style hltab[] =
+    TTYCOLOUR_INITTAB(HIGHLIGHTS);
 
-  init_layout(&h->lyt, fp, 0);
-  h->outbuf = 0; h->outsz = 0;
+  assert(!(f&~m));
 
-  switch (getenv_boolean("TVEC_TTY", -1)) {
-    case 1: h->f |= HOF_TTY; break;
-    case 0: break;
-    default:
-      if (isatty(fileno(fp))) h->f |= HOF_TTY;
-      break;
+  if (!(m&TVHF_TTY))
+    switch (getenv_boolean("TVEC_TTY", -1)) {
+      case 1: f |= TVHF_TTY; break;
+      case 0: break;
+      default:
+       if (isatty(fileno(fp))) f |= TVHF_TTY;
+       break;
+    }
+  if (!(m&TVHF_COLOUR))
+    switch (getenv_boolean("TVEC_COLOUR", -1)) {
+      case 1: f |= TVHF_COLOUR; break;
+      case 0: break;
+      default:
+       if (f&TVHF_TTY) {
+         p = getenv("TERM");
+         if (p && STRCMP(p, !=, "dumb")) f |= TVHF_COLOUR;
+       }
+       break;
+    }
+
+  /* Decide whether to write copies of reports to stderr.
+   *
+   * There's not much point if the output is already going to stderr.
+   * Otherwise, check to see whether stdout is the same underlying file.
+   */
+  if (fp != stderr) {
+    rc_out = fstat(fileno(fp), &st_out);
+    rc_err = fstat(STDERR_FILENO, &st_err);
+    if (!rc_err && (rc_out ||
+                   st_out.st_dev != st_err.st_dev ||
+                   st_out.st_ino != st_err.st_ino))
+      f |= HOF_DUPERR;
   }
-  switch (getenv_boolean("TVEC_COLOUR", -1)) {
-    case 1: h->f |= HOF_COLOUR; break;
-    case 0: break;
-    default:
-      if (h->f&HOF_TTY) {
-       p = getenv("TERM");
-       if (p && STRCMP(p, !=, "dumb")) h->f |= HOF_COLOUR;
-      }
-      break;
+
+  XNEW(h); h->a = arena_global; h->_o.ops = &human_ops;
+  h->f = f;
+
+  /* Initialize the colour tables. */
+  if (h->f&TVHF_COLOUR) {
+    ttycolour_config(h->attr, "TVEC_COLOURS", TCIF_GETENV | TCIF_REPORT,
+                    hltab);
+    ttycolour_init(&h->tc);
   }
 
-  if (fp != stderr && (fp != stdout || !(h->f&HOF_TTY))) h->f |= HOF_DUPERR;
+  init_layout(&h->lyt, fp, 0);
+  h->outbuf = 0; h->outsz = 0;
+
   dstr_create(&h->scoreboard);
   return (&h->_o);
 }
@@ -1314,7 +1275,7 @@ struct machine_output {
   char *outbuf; size_t outsz;          /* buffer for formatted output */
   unsigned grpix, testix;              /* group and test indices */
   unsigned f;                          /* flags */
-#define MF_BENCH 1u
+#define MF_BENCH 1u                    /*   current test is a benchmark */
 };
 
 /* --- @machine_writech@, @machine_write@, @machine_writef@ --- *
@@ -2262,6 +2223,430 @@ struct tvec_output *tvec_tapoutput(FILE *fp)
   return (&t->_o);
 }
 
+/*----- Automake support --------------------------------------------------*/
+
+struct automake_output {
+  struct tvec_output _o;
+  arena *a;                            /* arena */
+  struct tvec_state *tv;               /* test-vector state */
+  struct tvec_output *progress;                /* real-time progress output */
+  struct tvec_output *log;             /* log file output */
+  FILE *trs;                           /* test result stream */
+};
+
+/* --- @am_bsession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *             @struct tvec_state *tv@ = the test state producing output
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test session.
+ *
+ *             The Automake driver passes the event on to its subordinates.
+ */
+
+static void am_bsession(struct tvec_output *o, struct tvec_state *tv)
+{
+  struct automake_output *am = (struct automake_output *)o;
+
+  am->tv = tv;
+  human_bsession(am->progress, tv);
+  machine_bsession(am->log, tv);
+}
+
+/* --- @am_esession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *
+ * Returns:    Suggested exit code.
+ *
+ * Use:                End a test session.
+ *
+ *             The Automake driver completes the test-results file and
+ *             passes  the event on to its subordinates.
+ */
+
+static void am_report_unusual(struct automake_output *am,
+                             unsigned xfail, unsigned skip)
+{
+  unsigned f = 0;
+#define f_any 1u
+
+  if (xfail) {
+    fprintf(am->trs, "%s%u expected %s", f&f_any ? ", " : " (",
+           xfail, xfail == 1 ? "failure" : "failures");
+    f |= f_any;
+  }
+  if (skip) {
+    fprintf(am->trs, "%s%u skipped", f&f_any ? ", " : " (", skip);
+    f |= f_any;
+  }
+  if (f&f_any) fputc(')', am->trs);
+
+#undef f_any
+}
+
+static int am_esession(struct tvec_output *o)
+{
+  struct automake_output *am = (struct automake_output *)o;
+  struct tvec_state *tv = am->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_pass = all_win + all_xfail, all_run = all_pass + all_lose,
+    grps_run = grps_win + grps_lose;
+
+  human_esession(am->progress);
+  machine_esession(am->log);
+
+  fputs(":test-global-result: ", am->trs);
+  if (tv->f&TVSF_ERROR) fputs("ERRORS; ", am->trs);
+  if (!all_lose) {
+    fprintf(am->trs, "PASSED %s%u %s",
+           !(all_skip || grps_skip) ? "all " : "",
+           all_win, all_win == 1 ? "test" : "tests");
+    am_report_unusual(am, all_xfail, all_skip);
+    fprintf(am->trs, " in %u %s",
+           grps_win, grps_win == 1 ? "group" : "groups");
+    am_report_unusual(am, 0, grps_skip);
+  } else {
+    fprintf(am->trs, "FAILED %u out of %u %s",
+           all_lose, all_run, all_run == 1 ? "test" : "tests");
+    am_report_unusual(am, all_xfail, all_skip);
+    fprintf(am->trs, " in %u out of %u %s",
+           grps_lose, grps_run, grps_run == 1 ? "group" : "groups");
+    am_report_unusual(am, 0, grps_skip);
+  }
+  fputc('\n', am->trs);
+
+  fprintf(am->trs, ":copy-in-global-log: %s\n",
+         !all_lose && !(tv->f&TVSF_ERROR) ? "no" : "yes");
+  fprintf(am->trs, ":recheck: %s\n",
+         !all_lose && !(tv->f&TVSF_ERROR) ? "no" : "yes");
+
+  return (0);
+}
+
+/* --- @am_bgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test group.
+ *
+ *             The Automake driver passes the event on to its subordinates.
+ */
+
+static void am_bgroup(struct tvec_output *o)
+{
+  struct automake_output *am = (struct automake_output *)o;
+
+  human_bgroup(am->progress);
+  machine_bgroup(am->log);
+}
+
+/* --- @am_skipgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_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 Automake driver makes a note in the test-results file and
+ *             passes the event on to its subordinates.
+ */
+
+static void am_skipgroup(struct tvec_output *o,
+                        const char *excuse, va_list *ap)
+{
+  struct automake_output *am = (struct automake_output *)o;
+  struct tvec_state *tv = am->tv;
+
+  fprintf(am->trs, ":test-result: SKIP %s\n", tv->test->name);
+  human_skipgroup(am->progress, excuse, ap);
+  machine_skipgroup(am->log, excuse, ap);
+}
+
+/* --- @am_egroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group has finished.
+ *
+ *             The Automake driver makes a note in the test-results file and
+ *             passes the event on to its subordinates.
+ */
+
+static void am_egroup(struct tvec_output *o)
+{
+  struct automake_output *am = (struct automake_output *)o;
+  struct tvec_state *tv = am->tv;
+
+  fprintf(am->trs, ":test-result: %s %s\n",
+         tv->curr[TVOUT_LOSE] ? "FAIL" : "PASS", tv->test->name);
+  human_egroup(am->progress);
+  machine_egroup(am->log);
+}
+
+/* --- @am_btest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test is starting.
+ *
+ *             The Automake driver passes the event on to its subordinates.
+ */
+
+static void am_btest(struct tvec_output *o)
+{
+  struct automake_output *am = (struct automake_output *)o;
+
+  human_btest(am->progress);
+  machine_btest(am->log);
+}
+
+/* --- @am_skip@, @am_fail@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_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 Automake driver passes the event on to its subordinates.
+ */
+
+static void am_skip(struct tvec_output *o, const char *excuse, va_list *ap)
+{
+  struct automake_output *am = (struct automake_output *)o;
+
+  human_skip(am->progress, excuse, ap);
+  machine_skip(am->log, excuse, ap);
+}
+
+static void am_fail(struct tvec_output *o, const char *detail, va_list *ap)
+{
+  struct automake_output *am = (struct automake_output *)o;
+
+  human_fail(am->progress, detail, ap);
+  machine_fail(am->log, detail, ap);
+}
+
+/* --- @am_dumpreg@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_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 Automake driver passes the event on to its subordinates.
+ */
+
+static void am_dumpreg(struct tvec_output *o,
+                      unsigned disp, const union tvec_regval *rv,
+                      const struct tvec_regdef *rd)
+{
+  struct automake_output *am = (struct automake_output *)o;
+
+  human_dumpreg(am->progress, disp, rv, rd);
+  machine_dumpreg(am->log, disp, rv, rd);
+}
+
+/* --- @am_etest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *             @unsigned outcome@ = the test outcome
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test has finished.
+ *
+ *             The Automake driver passes the event on to its subordinates.
+ */
+
+static void am_etest(struct tvec_output *o, unsigned outcome)
+{
+  struct automake_output *am = (struct automake_output *)o;
+
+  human_etest(am->progress, outcome);
+  machine_etest(am->log, outcome);
+}
+
+/* --- @am_report@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_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 Automake driver passes the event on to its subordinates.
+ */
+
+static void am_report(struct tvec_output *o, unsigned level,
+                     const char *msg, va_list *ap)
+{
+  struct automake_output *am = (struct automake_output *)o;
+
+  human_report(am->progress, level, msg, ap);
+  machine_report(am->log, level, msg, ap);
+}
+
+/* --- @am_bbench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *             @const char *desc@ = adhoc test description
+ *             @unsigned unit@ = measurement unit (@BTU_...@)
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a benchmark has started.
+ *
+ *             The Automake driver passes the event on to its subordinates.
+ */
+
+static void am_bbench(struct tvec_output *o,
+                     const char *desc, unsigned unit)
+{
+  struct automake_output *am = (struct automake_output *)o;
+
+  human_bbench(am->progress, desc, unit);
+  machine_bbench(am->progress, desc, unit);
+}
+
+/* --- @am_ebench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *             @const char *desc@ = adhoc test description
+ *             @unsigned unit@ = measurement unit (@BTU_...@)
+ *             @const struct bench_timing *t@ = measurement
+ *
+ * Returns:    ---
+ *
+ * Use:                Report a benchmark's results.
+ *
+ *             The Automake driver passes the event on to its subordinates.
+ */
+
+static void am_ebench(struct tvec_output *o,
+                        const char *desc, unsigned unit,
+                        const struct bench_timing *t)
+{
+  struct automake_output *am = (struct automake_output *)o;
+
+  human_ebench(am->progress, desc, unit, t);
+  machine_ebench(am->progress, desc, unit, t);
+}
+
+static const struct tvec_benchoutops am_benchops =
+  { am_bbench, am_ebench };
+
+/* --- @am_extend@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *             @const char *name@ = extension name
+ *
+ * Returns:    A pointer to the extension implementation, or null.
+ */
+
+static const void *am_extend(struct tvec_output *o, const char *name)
+{
+  if (STRCMP(name, ==, TVEC_BENCHOUTEXT)) return (&am_benchops);
+  else return (0);
+}
+
+/* --- @am_destroy@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Release the resources held by the output driver.
+ */
+
+static void am_destroy(struct tvec_output *o)
+{
+  struct automake_output *am = (struct automake_output *)o;
+
+  human_destroy(am->progress);
+  machine_destroy(am->log);
+  fclose(am->trs); x_free(am->a, am);
+}
+
+static const struct tvec_outops automake_ops = {
+  am_bsession, am_esession,
+  am_bgroup, am_skipgroup, am_egroup,
+  am_btest, am_skip, am_fail, am_dumpreg, am_etest,
+  am_report, am_extend, am_destroy
+};
+
+/* --- @tvec_amoutput@ --- *
+ *
+ * Arguments:  @const struct tvec_amargs *a@ = arguments from Automake
+ *                     command-line protocol
+ *
+ * Returns:    An output formatter.
+ *
+ * Use:                Returns an output formatter which writes on standard output
+ *             in human format, pretending that the output is to a terminal
+ *             (in order to cope with %%\manpage{make}{1}%%'s output-
+ *             buffering behaviour, writes to the log file @a->log@ in
+ *             machine-readable format, and writes an Automake rST-format
+ *             test result file to @a->trs@.  The `test name' is currently
+ *             ignored, because the framework has its own means of
+ *             determining test names.
+ */
+
+struct tvec_output *tvec_amoutput(const struct tvec_amargs *a)
+{
+  struct automake_output *am;
+  unsigned f;
+
+  f = TVHF_TTY;
+  if (a->f&TVAF_COLOUR) f |= TVHF_COLOUR;
+
+  XNEW(am); am->a = arena_global; am->_o.ops = &automake_ops;
+  am->progress = tvec_humanoutput(stdout, f, TVHF_TTY | TVHF_COLOUR);
+  am->log = tvec_machineoutput(a->log); am->trs = a->trs;
+  return (&am->_o);
+}
+
 /*----- Default output ----------------------------------------------------*/
 
 /* --- @tvec_dfltoutput@ --- *
@@ -2277,12 +2662,12 @@ struct tvec_output *tvec_tapoutput(FILE *fp)
  *             otherwise the `machine' format is used.
  */
 
-struct tvec_output *tvec_dfltout(FILE *fp)
+struct tvec_output *tvec_dfltoutput(FILE *fp)
 {
   int ttyp = getenv_boolean("TVEC_TTY", -1);
 
   if (ttyp == -1) ttyp = isatty(fileno(fp));
-  if (ttyp) return (tvec_humanoutput(fp));
+  if (ttyp) return (tvec_humanoutput(fp, 0, 0));
   else return (tvec_machineoutput(fp));
 }