chiark / gitweb /
@@@ wip
[mLib] / test / tvec-output.c
index c4809bb389ce337a2ab76edd6116abe5fd797756..9ca4f29ef7da1fd8010a9caca486b1bb8516319e 100644 (file)
@@ -30,6 +30,7 @@
 #include "config.h"
 
 #include <assert.h>
+#include <ctype.h>
 #include <stdarg.h>
 #include <stdio.h>
 #include <string.h>
 #include "alloc.h"
 #include "bench.h"
 #include "dstr.h"
+#include "macros.h"
 #include "quis.h"
 #include "report.h"
 #include "tvec.h"
 
 /*----- Common machinery --------------------------------------------------*/
 
+/* --- @regdisp@ --- *
+ *
+ * Arguments:  @unsigned disp@ = a @TVRD_...@ disposition code
+ *
+ * Returns:    A human-readable adjective describing the register
+ *             disposition.
+ */
+
 static const char *regdisp(unsigned disp)
 {
   switch (disp) {
@@ -57,6 +67,15 @@ static const char *regdisp(unsigned disp)
   }
 }
 
+/* --- @getenv_boolean@ --- *
+ *
+ * Arguments:  @const char *var@ = environment variable name
+ *             @int dflt@ = default value
+ *
+ * Returns:    @0@ if the variable is set to something falseish, @1@ if it's
+ *             set to something truish, or @dflt@ otherwise.
+ */
+
 static int getenv_boolean(const char *var, int dflt)
 {
   const char *p;
@@ -75,12 +94,19 @@ static int getenv_boolean(const char *var, int dflt)
           STRCMP(p, ==, "0"))
     return (0);
   else {
-    moan("unexpected value `%s' for boolean environment variable `%s'",
+    moan("ignoring unexpected value `%s' for environment variable `%s'",
         var, p);
     return (dflt);
   }
 }
 
+/* --- @register_maxnamelen@ --- *
+ *
+ * Arguments:  @const struct tvec_state *tv@ = test vector state
+ *
+ * Returns:    The maximum length of a register name in the current test.
+ */
+
 static int register_maxnamelen(const struct tvec_state *tv)
 {
   const struct tvec_regdef *rd;
@@ -91,6 +117,285 @@ static int register_maxnamelen(const struct tvec_state *tv)
   return (maxlen);
 }
 
+/*----- Output formatting -------------------------------------------------*/
+
+/* We have two main jobs in output formatting: trimming trailing blanks; and
+ * adding a prefix to each line.
+ *
+ * This is somehow much more complicated than it ought to be.
+ */
+
+struct format {
+  FILE *fp;                             /* output file */
+  const char *prefix, *pfxtail, *pfxlim; /* prefix pointers */
+  dstr w;                              /* trailing whitespace */
+  unsigned f;                          /* flags */
+#define FMTF_NEWL 1u                   /*   start of output line */
+};
+
+/* Support macros.  These assume `fmt' is defined as a pointer to the `struct
+ * format' state.
+ */
+
+#define SPLIT_RANGE(tail, base, limit) do {                            \
+  /* Set TAIL to point just after the last nonspace character between  \
+   * BASE and LIMIT.  If there are no nonspace characters, then set    \
+   * TAIL to equal BASE.                                               \
+   */                                                                  \
+                                                                       \
+  for (tail = limit; tail > base && ISSPACE(tail[-1]); tail--);                \
+} while (0)
+
+#define PUT_RANGE(base, limit) do {                                    \
+  /* Write the range of characters between BASE and LIMIT to the output \
+   * file.  Return immediately on error.                               \
+   */                                                                  \
+                                                                       \
+  size_t n = limit - base;                                             \
+  if (fwrite(base, 1, n, fmt->fp) < n) return (-1);                    \
+} while (0)
+
+#define PUT_CHAR(ch) do {                                              \
+  /* Write CH to the output. Return immediately on error. */           \
+                                                                       \
+  if (putc(ch, fmt->fp) == EOF) return (-1);                           \
+} while (0)
+
+#define PUT_PREFIX do {                                                        \
+  /* Output the prefix, if there is one.  Return immediately on error. */ \
+                                                                       \
+  if (fmt->prefix) PUT_RANGE(fmt->prefix, fmt->pfxlim);                        \
+} while (0)
+
+#define PUT_SAVED do {                                                 \
+  /* Output the saved trailing blank material in the buffer. */                \
+                                                                       \
+  size_t n = fmt->w.len;                                               \
+  if (n && fwrite(fmt->w.buf, 1, n, fmt->fp) < n) return (-1);         \
+} while (0)
+
+#define PUT_PFXINB do {                                                        \
+  /* Output the initial nonblank portion of the prefix, if there is    \
+   * one.  Return immediately on error.                                        \
+   */                                                                  \
+                                                                       \
+  if (fmt->prefix) PUT_RANGE(fmt->prefix, fmt->pfxtail);               \
+} while (0)
+
+#define SAVE_PFXTAIL do {                                              \
+  /* Save the trailing blank portion of the prefix. */                 \
+                                                                       \
+  if (fmt->prefix)                                                     \
+    DPUTM(&fmt->w, fmt->pfxtail, fmt->pfxlim - fmt->pfxtail);          \
+} while (0)
+
+/* --- @init_fmt@ --- *
+ *
+ * Arguments:  @struct format *fmt@ = formatting state to initialize
+ *             @FILE *fp@ = output file
+ *             @const char *prefix@ = prefix string (or null if empty)
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize a formatting state.
+ */
+
+static void init_fmt(struct format *fmt, FILE *fp, const char *prefix)
+{
+  const char *q, *l;
+
+  /* Basics. */
+  fmt->fp = fp;
+  fmt->f = FMTF_NEWL;
+  dstr_create(&fmt->w);
+
+  /* Prefix portions. */
+  if (!prefix || !*prefix)
+    fmt->prefix = fmt->pfxtail = fmt->pfxlim = 0;
+  else {
+    fmt->prefix = prefix;
+    l = fmt->pfxlim = prefix + strlen(prefix);
+    SPLIT_RANGE(q, prefix, l); fmt->pfxtail = q;
+    DPUTM(&fmt->w, q, l - q);
+  }
+}
+
+/* --- @destroy_fmt@ --- *
+ *
+ * Arguments:  @struct format *fmt@ = formatting state
+ *             @unsigned f@ = flags (@DFF_...@)
+ *
+ * Returns:    ---
+ *
+ * Use:                Releases a formatting state and the resources it holds.
+ *             Close the file if @DFF_CLOSE@ is set in @f@; otherwise leave
+ *             it open (in case it's @stderr@ or something).
+ */
+
+#define DFF_CLOSE 1u
+static void destroy_fmt(struct format *fmt, unsigned f)
+{
+  if (f&DFF_CLOSE) fclose(fmt->fp);
+  dstr_destroy(&fmt->w);
+}
+
+/* --- @format_char@ --- *
+ *
+ * Arguments:  @struct format *fmt@ = formatting state
+ *             @int ch@ = character to write
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Write a single character to the output.
+ */
+
+static int format_char(struct format *fmt, int ch)
+{
+  if (ch == '\n') {
+    if (fmt->f&FMTF_NEWL) PUT_PFXINB;
+    PUT_CHAR('\n'); fmt->f |= FMTF_NEWL; DRESET(&fmt->w);
+  } else if (isspace(ch))
+    DPUTC(&fmt->w, ch);
+  else {
+    if (fmt->f&FMTF_NEWL) { PUT_PFXINB; fmt->f &= ~FMTF_NEWL; }
+    PUT_SAVED; PUT_CHAR(ch); DRESET(&fmt->w);
+  }
+  return (0);
+}
+
+/* --- @format_string@ --- *
+ *
+ * Arguments:  @struct format *fmt@ = formatting state
+ *             @const char *p@ = string to write
+ *             @size_t sz@ = length of string
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Write a string to the output.
+ */
+
+static int format_string(struct format *fmt, const char *p, size_t sz)
+{
+  const char *q, *r, *l = p + sz;
+
+  /* This is rather vexing.  There are a small number of jobs to do, but the
+   * logic for deciding which to do when gets rather hairy if, as I've tried
+   * here, one aims to minimize the number of decisions being checked, so
+   * it's worth canning them into macros.
+   *
+   * Here, a `blank' is a whitespace character other than newline.  The input
+   * buffer consists of one or more `segments', each of which consists of:
+   *
+   *   * an initial portion, which is either empty or ends with a nonblank
+   *    character;
+   *
+   *   * a suffix which consists only of blanks; and
+   *
+   *   * an optional newline.
+   *
+   * All segments except the last end with a newline.
+   */
+
+#define SPLIT_SEGMENT do {                                             \
+  /* Determine the bounds of the current segment.  If there is a final \
+   * newline, then q is non-null and points to this newline; otherwise,        \
+   * q is null.  The initial portion of the segment lies between p .. r        \
+   * and the blank suffix lies between r .. q (or r .. l if q is null).        \
+   * This sounds awkward, but the suffix is only relevant if there is  \
+   * no newline.                                                       \
+   */                                                                  \
+                                                                       \
+  q = memchr(p, '\n', l - p); SPLIT_RANGE(r, p, q ? q : l);            \
+} while (0)
+
+#define PUT_NONBLANK do {                                              \
+  /* Output the initial portion of the segment. */                     \
+                                                                       \
+  PUT_RANGE(p, r);                                                     \
+} while (0)
+
+#define PUT_NEWLINE do {                                               \
+  /* Write a newline, and advance to the next segment. */              \
+                                                                       \
+  PUT_CHAR('\n'); p = q + 1;                                           \
+} while (0)
+
+#define SAVE_TAIL do {                                                 \
+  /* Save the trailing blank portion of the segment in the buffer.     \
+   * Assumes that there is no newline, since otherwise the suffix would        \
+   * be omitted.                                                       \
+   */                                                                  \
+                                                                       \
+  DPUTM(&fmt->w, r, l - r);                                            \
+} while (0)
+
+  /* Determine the bounds of the first segment.  Handling this is the most
+   * complicated part of this function.
+   */
+  SPLIT_SEGMENT;
+
+  if (!q) {
+    /* This is the only segment.  We'll handle the whole thing here.
+     *
+     * If there's an initial nonblank portion, then we need to write that
+     * out.  Furthermore, if we're at the start of the line then we'll need
+     * to write the prefix, and if there's saved blank material then we'll
+     * need to write that.  Otherwise, there's only blank stuff, which we
+     * accumulate in the buffer.
+     *
+     * If we're at the start of a line here, then
+     */
+
+    if (r > p) {
+      if (fmt->f&FMTF_NEWL) { PUT_PFXINB; fmt->f &= ~FMTF_NEWL; }
+      PUT_SAVED; PUT_NONBLANK; DRESET(&fmt->w);
+    }
+    SAVE_TAIL;
+    return (0);
+  }
+
+  /* There is at least one more segment, so we know that there'll be a line
+   * to output.
+   */
+  if (fmt->f&FMTF_NEWL) PUT_PFXINB;
+  if (r > p) { PUT_SAVED; PUT_NONBLANK; }
+  PUT_NEWLINE; DRESET(&fmt->w);
+  SPLIT_SEGMENT;
+
+  /* Main loop over whole segments with trailing newlines.  For each one, we
+   * know that we're starting at the beginning of a line and there's a final
+   * newline, so we write the initial prefix and drop the trailing blanks.
+   */
+  while (q) {
+    PUT_PREFIX; PUT_NONBLANK; PUT_NEWLINE;
+    SPLIT_SEGMENT;
+  }
+
+  /* At the end, there's no final newline.  If there's nonblank material,
+   * then we can write the prefix and the nonblank stuff.  Otherwise, stash
+   * the blank stuff (including the trailing blanks of the prefix) and leave
+   * the newline flag set.
+   */
+  if (r > p) { PUT_PREFIX; PUT_NONBLANK; fmt->f &= ~FMTF_NEWL; }
+  else { fmt->f |= FMTF_NEWL; SAVE_PFXTAIL; }
+  SAVE_TAIL;
+
+#undef SPLIT_SEGMENT
+#undef PUT_NONBLANK
+#undef PUT_NEWLINE
+#undef SAVE_TAIL
+
+  return (0);
+}
+
+#undef SPLIT_RANGE
+#undef PUT_RANGE
+#undef PUT_PREFIX
+#undef PUT_PFXINB
+#undef PUT_SAVED
+#undef PUT_CHAR
+#undef SAVE_PFXTAIL
+
 /*----- Skeleton ----------------------------------------------------------*/
 /*
 static void ..._bsession(struct tvec_output *o, struct tvec_state *tv)
@@ -152,7 +457,8 @@ static const struct tvec_outops ..._ops = {
 struct human_output {
   struct tvec_output _o;
   struct tvec_state *tv;
-  FILE *fp;
+  struct format fmt;
+  char *outbuf; size_t outsz;
   dstr scoreboard;
   unsigned attr;
   int maxlen;
@@ -178,27 +484,29 @@ static void setattr(struct human_output *h, unsigned attr)
   int sep = 0;
 
   if (!diff || !(h->f&HOF_COLOUR)) return;
-  fputs("\x1b[", h->fp);
+  fputs("\x1b[", h->fmt.fp);
 
   if (diff&HAF_BOLD) {
-    if (attr&HAF_BOLD) putc('1', h->fp);
-    else { putc('0', h->fp); diff = h->attr; }
+    if (attr&HAF_BOLD) putc('1', h->fmt.fp);
+    else { putc('0', h->fmt.fp); diff = h->attr; }
     sep = ';';
   }
   if (diff&(HAF_FG | HAF_FGMASK)) {
     if (attr&HAF_FG)
-      set_colour(h->fp, &sep, "3", "9", (attr&HAF_FGMASK) >> HAF_FGSHIFT);
+      set_colour(h->fmt.fp, &sep, "3", "9",
+                (attr&HAF_FGMASK) >> HAF_FGSHIFT);
     else
-      { if (sep) putc(sep, h->fp); fputs("39", h->fp); sep = ';'; }
+      { if (sep) putc(sep, h->fmt.fp); fputs("39", h->fmt.fp); sep = ';'; }
   }
   if (diff&(HAF_BG | HAF_BGMASK)) {
     if (attr&HAF_BG)
-      set_colour(h->fp, &sep, "4", "10", (attr&HAF_BGMASK) >> HAF_BGSHIFT);
+      set_colour(h->fmt.fp, &sep, "4", "10",
+                (attr&HAF_BGMASK) >> HAF_BGSHIFT);
     else
-      { if (sep) putc(sep, h->fp); fputs("49", h->fp); sep = ';'; }
+      { if (sep) putc(sep, h->fmt.fp); fputs("49", h->fmt.fp); sep = ';'; }
   }
 
-  putc('m', h->fp); h->attr = attr;
+  putc('m', h->fmt.fp); h->attr = attr;
 
 #undef f_any
 }
@@ -209,7 +517,7 @@ static void clear_progress(struct human_output *h)
 
   if (h->f&HOF_PROGRESS) {
     n = strlen(h->tv->test->name) + 2 + h->scoreboard.len;
-    for (i = 0; i < n; i++) fputs("\b \b", h->fp);
+    for (i = 0; i < n; i++) fputs("\b \b", h->fmt.fp);
     h->f &= ~HOF_PROGRESS;
   }
 }
@@ -221,7 +529,7 @@ static void write_scoreboard_char(struct human_output *h, int ch)
     case '_': setattr(h, HA_SKIP); break;
     default: setattr(h, 0); break;
   }
-  putc(ch, h->fp); setattr(h, 0);
+  putc(ch, h->fmt.fp); setattr(h, 0);
 }
 
 static void show_progress(struct human_output *h)
@@ -230,12 +538,12 @@ static void show_progress(struct human_output *h)
   const char *p, *l;
 
   if (tv->test && (h->f&HOF_TTY) && !(h->f&HOF_PROGRESS)) {
-    fprintf(h->fp, "%s: ", tv->test->name);
+    fprintf(h->fmt.fp, "%s: ", tv->test->name);
     if (!(h->f&HOF_COLOUR))
-      dstr_write(&h->scoreboard, h->fp);
+      dstr_write(&h->scoreboard, h->fmt.fp);
     else for (p = h->scoreboard.buf, l = p + h->scoreboard.len; p < l; p++)
       write_scoreboard_char(h, *p);
-    fflush(h->fp); h->f |= HOF_PROGRESS;
+    fflush(h->fmt.fp); h->f |= HOF_PROGRESS;
   }
 }
 
@@ -247,29 +555,55 @@ static void report_location(struct human_output *h, FILE *fp,
 
 #define FLUSH(fp) do if (f&f_flush) fflush(fp); while (0)
 
-  if (fp != h->fp) f |= f_flush;
+  if (fp != h->fmt.fp) f |= f_flush;
 
   if (file) {
-    setattr(h, HFG(CYAN)); FLUSH(h->fp); fputs(file, fp); FLUSH(fp);
-    setattr(h, HFG(BLUE)); FLUSH(h->fp); fputc(':', fp); FLUSH(fp);
-    setattr(h, HFG(CYAN)); FLUSH(h->fp); fprintf(fp, "%u", lno); FLUSH(fp);
-    setattr(h, HFG(BLUE)); FLUSH(h->fp); fputc(':', fp); FLUSH(fp);
-    setattr(h, 0); FLUSH(h->fp); fputc(' ', fp);
+    setattr(h, HFG(CYAN));     FLUSH(h->fmt.fp);
+    fputs(file, fp);           FLUSH(fp);
+    setattr(h, HFG(BLUE));     FLUSH(h->fmt.fp);
+    fputc(':', fp);            FLUSH(fp);
+    setattr(h, HFG(CYAN));     FLUSH(h->fmt.fp);
+    fprintf(fp, "%u", lno);    FLUSH(fp);
+    setattr(h, HFG(BLUE));     FLUSH(h->fmt.fp);
+    fputc(':', fp);            FLUSH(fp);
+    setattr(h, 0);             FLUSH(h->fmt.fp);
+    fputc(' ', fp);
   }
 
 #undef f_flush
 #undef FLUSH
 }
 
+static int human_writech(void *go, int ch)
+  { struct human_output *h = go; return (format_char(&h->fmt, ch)); }
+
+static int human_writem(void *go, const char *p, size_t sz)
+  { struct human_output *h = go; return (format_string(&h->fmt, p, sz)); }
+
+static int human_nwritef(void *go, size_t maxsz, const char *p, ...)
+{
+  struct human_output *h = go;
+  size_t n;
+  va_list ap;
+
+  va_start(ap, p);
+  n = gprintf_memputf(&h->outbuf, &h->outsz, maxsz, p, ap);
+  va_end(ap);
+  return (format_string(&h->fmt, h->outbuf, n));
+}
+
+static const struct gprintf_ops human_printops =
+  { human_writech, human_writem, human_nwritef };
+
 static void human_bsession(struct tvec_output *o, struct tvec_state *tv)
   { struct human_output *h = (struct human_output *)o; h->tv = tv; }
 
 static void report_skipped(struct human_output *h, unsigned n)
 {
   if (n) {
-    fprintf(h->fp, " (%u ", n);
-    setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0);
-    fputc(')', h->fp);
+    fprintf(h->fmt.fp, " (%u ", n);
+    setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0);
+    fputc(')', h->fmt.fp);
   }
 }
 
@@ -284,28 +618,28 @@ static int human_esession(struct tvec_output *o)
     all_run = all_win + all_lose, grps_run = grps_win + grps_lose;
 
   if (!all_lose) {
-    setattr(h, HA_WIN); fputs("PASSED", h->fp); setattr(h, 0);
-    fprintf(h->fp, " %s%u %s",
+    setattr(h, HA_WIN); fputs("PASSED", h->fmt.fp); setattr(h, 0);
+    fprintf(h->fmt.fp, " %s%u %s",
            !(all_skip || grps_skip) ? "all " : "",
            all_win, all_win == 1 ? "test" : "tests");
     report_skipped(h, all_skip);
-    fprintf(h->fp, " in %u %s",
+    fprintf(h->fmt.fp, " in %u %s",
            grps_win, grps_win == 1 ? "group" : "groups");
     report_skipped(h, grps_skip);
   } else {
-    setattr(h, HA_LOSE); fputs("FAILED", h->fp); setattr(h, 0);
-    fprintf(h->fp, " %u out of %u %s",
+    setattr(h, HA_LOSE); fputs("FAILED", h->fmt.fp); setattr(h, 0);
+    fprintf(h->fmt.fp, " %u out of %u %s",
            all_lose, all_run, all_run == 1 ? "test" : "tests");
     report_skipped(h, all_skip);
-    fprintf(h->fp, " in %u out of %u %s",
+    fprintf(h->fmt.fp, " in %u out of %u %s",
            grps_lose, grps_run, grps_run == 1 ? "group" : "groups");
     report_skipped(h, grps_skip);
   }
-  fputc('\n', h->fp);
+  fputc('\n', h->fmt.fp);
 
   if (tv->f&TVSF_ERROR) {
-    setattr(h, HA_ERR); fputs("ERRORS", h->fp); setattr(h, 0);
-    fputs(" found in input; tests may not have run correctly\n", h->fp);
+    setattr(h, HA_ERR); fputs("ERRORS", h->fmt.fp); setattr(h, 0);
+    fputs(" found in input; tests may not have run correctly\n", h->fmt.fp);
   }
 
   h->tv = 0; return (tv->f&TVSF_ERROR ? 2 : tv->all[TVOUT_LOSE] ? 1 : 0);
@@ -326,13 +660,13 @@ static void human_skipgroup(struct tvec_output *o,
 
   if (!(~h->f&(HOF_TTY | HOF_PROGRESS))) {
     h->f &= ~HOF_PROGRESS;
-    setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0);
+    setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0);
   } else {
-    fprintf(h->fp, "%s: ", h->tv->test->name);
-    setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0);
+    fprintf(h->fmt.fp, "%s: ", h->tv->test->name);
+    setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0);
   }
-  if (excuse) { fputs(": ", h->fp); vfprintf(h->fp, excuse, *ap); }
-  fputc('\n', h->fp);
+  if (excuse) { fputs(": ", h->fmt.fp); vfprintf(h->fmt.fp, excuse, *ap); }
+  fputc('\n', h->fmt.fp);
 }
 
 static void human_egroup(struct tvec_output *o)
@@ -343,17 +677,18 @@ static void human_egroup(struct tvec_output *o)
     skip = tv->curr[TVOUT_SKIP], run = win + lose;
 
   if (h->f&HOF_TTY) h->f &= ~HOF_PROGRESS;
-  else fprintf(h->fp, "%s:", h->tv->test->name);
+  else fprintf(h->fmt.fp, "%s:", h->tv->test->name);
 
   if (lose) {
-    fprintf(h->fp, " %u/%u ", lose, run);
-    setattr(h, HA_LOSE); fputs("FAILED", h->fp); setattr(h, 0);
+    fprintf(h->fmt.fp, " %u/%u ", lose, run);
+    setattr(h, HA_LOSE); fputs("FAILED", h->fmt.fp); setattr(h, 0);
     report_skipped(h, skip);
   } else {
-    fputc(' ', h->fp); setattr(h, HA_WIN); fputs("ok", h->fp); setattr(h, 0);
+    fputc(' ', h->fmt.fp); setattr(h, HA_WIN);
+    fputs("ok", h->fmt.fp); setattr(h, 0);
     report_skipped(h, skip);
   }
-  fputc('\n', h->fp);
+  fputc('\n', h->fmt.fp);
 }
 
 static void human_btest(struct tvec_output *o)
@@ -366,11 +701,11 @@ static void human_skip(struct tvec_output *o,
   struct tvec_state *tv = h->tv;
 
   clear_progress(h);
-  report_location(h, h->fp, tv->infile, tv->test_lno);
-  fprintf(h->fp, "`%s' ", tv->test->name);
-  setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0);
-  if (excuse) { fputs(": ", h->fp); vfprintf(h->fp, excuse, *ap); }
-  fputc('\n', h->fp);
+  report_location(h, h->fmt.fp, tv->infile, tv->test_lno);
+  fprintf(h->fmt.fp, "`%s' ", tv->test->name);
+  setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0);
+  if (excuse) { fputs(": ", h->fmt.fp); vfprintf(h->fmt.fp, excuse, *ap); }
+  fputc('\n', h->fmt.fp);
 }
 
 static void human_fail(struct tvec_output *o,
@@ -380,11 +715,11 @@ static void human_fail(struct tvec_output *o,
   struct tvec_state *tv = h->tv;
 
   clear_progress(h);
-  report_location(h, h->fp, tv->infile, tv->test_lno);
-  fprintf(h->fp, "`%s' ", tv->test->name);
-  setattr(h, HA_LOSE); fputs("FAILED", h->fp); setattr(h, 0);
-  if (detail) { fputs(": ", h->fp); vfprintf(h->fp, detail, *ap); }
-  fputc('\n', h->fp);
+  report_location(h, h->fmt.fp, tv->infile, tv->test_lno);
+  fprintf(h->fmt.fp, "`%s' ", tv->test->name);
+  setattr(h, HA_LOSE); fputs("FAILED", h->fmt.fp); setattr(h, 0);
+  if (detail) { fputs(": ", h->fmt.fp); vfprintf(h->fmt.fp, detail, *ap); }
+  fputc('\n', h->fmt.fp);
 }
 
 static void human_dumpreg(struct tvec_output *o,
@@ -394,15 +729,17 @@ static void human_dumpreg(struct tvec_output *o,
   struct human_output *h = (struct human_output *)o;
   const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name);
 
-  fprintf(h->fp, "%*s%s %s = ", 10 + h->maxlen - n, "", ds, 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, HFG(YELLOW));
     else if (disp == TVRD_FOUND) setattr(h, HFG(RED));
     else if (disp == TVRD_EXPECT) setattr(h, HFG(GREEN));
   }
-  if (!rv) fprintf(h->fp, "#<unset>");
-  else rd->ty->dump(rv, rd, 0, &file_printops, h->fp);
-  setattr(h, 0); fputc('\n', h->fp);
+  if (!rv) gprintf(&human_printops, h, "#unset");
+  else rd->ty->dump(rv, rd, 0, &human_printops, h);
+  setattr(h, 0); format_char(&h->fmt, '\n');
 }
 
 static void human_etest(struct tvec_output *o, unsigned outcome)
@@ -419,7 +756,7 @@ static void human_etest(struct tvec_output *o, unsigned outcome)
       default: abort();
     }
     dstr_putc(&h->scoreboard, ch);
-    write_scoreboard_char(h, ch); fflush(h->fp);
+    write_scoreboard_char(h, ch); fflush(h->fmt.fp);
   }
 }
 
@@ -430,7 +767,7 @@ static void human_bbench(struct tvec_output *o,
   struct tvec_state *tv = h->tv;
 
   clear_progress(h);
-  fprintf(h->fp, "%s: %s: ", tv->test->name, ident); fflush(h->fp);
+  fprintf(h->fmt.fp, "%s: %s: ", tv->test->name, ident); fflush(h->fmt.fp);
 }
 
 static void human_ebench(struct tvec_output *o,
@@ -438,7 +775,9 @@ static void human_ebench(struct tvec_output *o,
                         const struct bench_timing *tm)
 {
   struct human_output *h = (struct human_output *)o;
-  tvec_benchreport(&file_printops, h->fp, unit, tm); fputc('\n', h->fp);
+
+  tvec_benchreport(&human_printops, h->fmt.fp, unit, tm);
+  fputc('\n', h->fmt.fp);
 }
 
 static void human_report(struct tvec_output *o, const char *msg, va_list *ap)
@@ -449,14 +788,14 @@ static void human_report(struct tvec_output *o, const char *msg, va_list *ap)
 
   dstr_vputf(&d, msg, ap); dstr_putc(&d, '\n');
 
-  clear_progress(h); fflush(h->fp);
+  clear_progress(h); fflush(h->fmt.fp);
   fprintf(stderr, "%s: ", QUIS);
   report_location(h, stderr, tv->infile, tv->lno);
   fwrite(d.buf, 1, d.len, stderr);
 
   if (h->f&HOF_DUPERR) {
-    report_location(h, h->fp, tv->infile, tv->lno);
-    fwrite(d.buf, 1, d.len, h->fp);
+    report_location(h, h->fmt.fp, tv->infile, tv->lno);
+    fwrite(d.buf, 1, d.len, h->fmt.fp);
   }
   show_progress(h);
 }
@@ -465,9 +804,9 @@ static void human_destroy(struct tvec_output *o)
 {
   struct human_output *h = (struct human_output *)o;
 
-  if (h->f&HOF_DUPERR) fclose(h->fp);
+  destroy_fmt(&h->fmt, h->f&HOF_DUPERR ? DFF_CLOSE : 0);
   dstr_destroy(&h->scoreboard);
-  xfree(h);
+  xfree(h->outbuf); xfree(h);
 }
 
 static const struct tvec_outops human_ops = {
@@ -487,7 +826,8 @@ struct tvec_output *tvec_humanoutput(FILE *fp)
   h = xmalloc(sizeof(*h)); h->_o.ops = &human_ops;
   h->f = 0; h->attr = 0;
 
-  h->fp = fp;
+  init_fmt(&h->fmt, fp, 0);
+  h->outbuf = 0; h->outsz = 0;
 
   switch (getenv_boolean("TVEC_TTY", -1)) {
     case 1: h->f |= HOF_TTY; break;
@@ -517,61 +857,27 @@ struct tvec_output *tvec_humanoutput(FILE *fp)
 struct tap_output {
   struct tvec_output _o;
   struct tvec_state *tv;
-  FILE *fp;
-  dstr d;
+  struct format fmt;
+  char *outbuf; size_t outsz;
   int maxlen;
-  unsigned f;
-#define TOF_FRESHLINE 1u
 };
 
 static int tap_writech(void *go, int ch)
-{
-  struct tap_output *t = go;
-
-  if (t->f&TOF_FRESHLINE) {
-    if (fputs("## ", t->fp) < 0) return (-1);
-    t->f &= ~TOF_FRESHLINE;
-  }
-  if (putc(ch, t->fp) < 0) return (-1);
-  if (ch == '\n') t->f |= TOF_FRESHLINE;
-  return (1);
-}
+  { struct tap_output *t = go; return (format_char(&t->fmt, ch)); }
 
 static int tap_writem(void *go, const char *p, size_t sz)
-{
-  struct tap_output *t = go;
-  const char *q, *l = p + sz;
-  size_t n;
-
-  if (p == l) return (0);
-  if (t->f&TOF_FRESHLINE)
-    if (fputs("## ", t->fp) < 0) return (-1);
-  for (;;) {
-    q = memchr(p, '\n', l - p); if (!q) break;
-    n = q + 1 - p; if (fwrite(p, 1, n, t->fp) < n) return (-1);
-    p = q + 1;
-    if (p == l) { t->f |= TOF_FRESHLINE; return (sz); }
-    if (fputs("## ", t->fp) < 0) return (-1);
-  }
-  n = l - p; if (fwrite(p, 1, n, t->fp) < n) return (-1);
-  t->f &= ~TOF_FRESHLINE; return (0);
-}
+  { struct human_output *t = go; return (format_string(&t->fmt, p, sz)); }
 
 static int tap_nwritef(void *go, size_t maxsz, const char *p, ...)
 {
-  struct tap_output *t = go;
+  struct human_output *t = go;
+  size_t n;
   va_list ap;
-  int n;
-
-  va_start(ap, p); DRESET(&t->d); DENSURE(&t->d, maxsz + 1);
-#ifdef HAVE_SNPRINTF
-  n = vsnprintf(t->d.buf, maxsz + 1, p, ap);
-#else
-  n = vsprintf(t->d.buf, p, ap);
-#endif
-  assert(0 <= n && n <= maxsz);
+
+  va_start(ap, p);
+  n = gprintf_memputf(&t->outbuf, &t->outsz, maxsz, p, ap);
   va_end(ap);
-  return (tap_writem(t, t->d.buf, n));
+  return (format_string(&t->fmt, t->outbuf, n));
 }
 
 static const struct gprintf_ops tap_printops =
@@ -582,7 +888,7 @@ static void tap_bsession(struct tvec_output *o, struct tvec_state *tv)
   struct tap_output *t = (struct tap_output *)o;
 
   t->tv = tv;
-  fputs("TAP version 13\n", t->fp);
+  fputs("TAP version 13\n", t->fmt.fp);
 }
 
 static unsigned tap_grpix(struct tap_output *t)
@@ -602,11 +908,11 @@ static int tap_esession(struct tvec_output *o)
   if (tv->f&TVSF_ERROR) {
     fputs("Bail out!  "
          "Errors found in input; tests may not have run correctly\n",
-         t->fp);
+         t->fmt.fp);
     return (2);
   }
 
-  fprintf(t->fp, "1..%u\n", tap_grpix(t));
+  fprintf(t->fmt.fp, "1..%u\n", tap_grpix(t));
   t->tv = 0; return (tv->all[TVOUT_LOSE] ? 1 : 0);
 }
 
@@ -621,9 +927,9 @@ static void tap_skipgroup(struct tvec_output *o,
 {
   struct tap_output *t = (struct tap_output *)o;
 
-  fprintf(t->fp, "ok %u %s # SKIP", tap_grpix(t), t->tv->test->name);
-  if (excuse) { fputc(' ', t->fp); vfprintf(t->fp, excuse, *ap); }
-  fputc('\n', t->fp);
+  fprintf(t->fmt.fp, "ok %u %s # SKIP", tap_grpix(t), t->tv->test->name);
+  if (excuse) { fputc(' ', t->fmt.fp); vfprintf(t->fmt.fp, excuse, *ap); }
+  fputc('\n', t->fmt.fp);
 }
 
 static void tap_egroup(struct tvec_output *o)
@@ -637,39 +943,37 @@ static void tap_egroup(struct tvec_output *o)
     skip = tv->curr[TVOUT_SKIP];
 
   if (lose) {
-    fprintf(t->fp, "not ok %u - %s: FAILED %u/%u",
+    fprintf(t->fmt.fp, "not ok %u - %s: FAILED %u/%u",
            grpix, tv->test->name, lose, win + lose);
-    if (skip) fprintf(t->fp, " (skipped %u)", skip);
+    if (skip) fprintf(t->fmt.fp, " (skipped %u)", skip);
   } else {
-    fprintf(t->fp, "ok %u - %s: passed %u", grpix, tv->test->name, win);
-    if (skip) fprintf(t->fp, " (skipped %u)", skip);
+    fprintf(t->fmt.fp, "ok %u - %s: passed %u", grpix, tv->test->name, win);
+    if (skip) fprintf(t->fmt.fp, " (skipped %u)", skip);
   }
-  fputc('\n', t->fp);
+  fputc('\n', t->fmt.fp);
 }
 
 static void tap_btest(struct tvec_output *o) { ; }
 
-static void tap_skip(struct tvec_output *o, const char *excuse, va_list *ap)
+static void tap_outcome(struct tvec_output *o, const char *outcome,
+                       const char *detail, va_list *ap)
 {
   struct tap_output *t = (struct tap_output *)o;
   struct tvec_state *tv = t->tv;
 
-  fprintf(t->fp, "## %s:%u: `%s' skipped",
-         tv->infile, tv->test_lno, tv->test->name);
-  if (excuse) { fputs(": ", t->fp); vfprintf(t->fp, excuse, *ap); }
-  fputc('\n', t->fp);
+  gprintf(&tap_printops, t, "%s:%u: `%s' %s",
+         tv->infile, tv->test_lno, tv->test->name, outcome);
+  if (detail) {
+    format_string(&t->fmt, ": ", 2);
+    vgprintf(&tap_printops, t, detail, ap);
+  }
+  format_char(&t->fmt, '\n');
 }
 
+static void tap_skip(struct tvec_output *o, const char *excuse, va_list *ap)
+  { tap_outcome(o, "skipped", excuse, ap); }
 static void tap_fail(struct tvec_output *o, const char *detail, va_list *ap)
-{
-  struct tap_output *t = (struct tap_output *)o;
-  struct tvec_state *tv = t->tv;
-
-  fprintf(t->fp, "## %s:%u: `%s' FAILED",
-         tv->infile, tv->test_lno, tv->test->name);
-  if (detail) { fputs(": ", t->fp); vfprintf(t->fp, detail, *ap); }
-  fputc('\n', t->fp);
-}
+  { tap_outcome(o, "FAILED", detail, ap); }
 
 static void tap_dumpreg(struct tvec_output *o,
                        unsigned disp, const union tvec_regval *rv,
@@ -678,14 +982,11 @@ 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);
 
-  fprintf(t->fp, "## %*s%s %s = ", 10 + t->maxlen - n, "", ds, rd->name);
-  if (!rv)
-    fprintf(t->fp, "#<unset>\n");
-  else {
-    t->f &= ~TOF_FRESHLINE;
-    rd->ty->dump(rv, rd, 0, &tap_printops, t);
-    fputc('\n', t->fp);
-  }
+  gprintf(&tap_printops, t, "%*s%s %s = ",
+         10 + t->maxlen - n, "", ds, rd->name);
+  if (!rv) gprintf(&tap_printops, t, "#<unset>");
+  else rd->ty->dump(rv, rd, 0, &tap_printops, t);
+  format_char(&t->fmt, '\n');
 }
 
 static void tap_etest(struct tvec_output *o, unsigned outcome) { ; }
@@ -701,38 +1002,43 @@ static void tap_ebench(struct tvec_output *o,
   struct tap_output *t = (struct tap_output *)o;
   struct tvec_state *tv = t->tv;
 
-  fprintf(t->fp, "## %s: %s: ", tv->test->name, ident);
-  t->f &= ~TOF_FRESHLINE; tvec_benchreport(&tap_printops, t, unit, tm);
-  fputc('\n', t->fp);
+  gprintf(&tap_printops, t, "%s: %s: ", tv->test->name, ident);
+  tvec_benchreport(&tap_printops, t, unit, tm);
+  format_char(&t->fmt, '\n');
 }
 
-static void tap_report(struct tap_output *t, const char *msg, va_list *ap)
+static void tap_report(struct tap_output *t,
+                      const struct gprintf_ops *gops, void *go,
+                      const char *msg, va_list *ap)
 {
   struct tvec_state *tv = t->tv;
 
-  if (tv->infile) fprintf(t->fp, "%s:%u: ", tv->infile, tv->lno);
-  vfprintf(t->fp, msg, *ap); fputc('\n', t->fp);
+  if (tv->infile) gprintf(gops, go, "%s:%u: ", tv->infile, tv->lno);
+  gprintf(gops, go, msg, ap); gops->putch(go, '\n');
 }
 
 static void tap_error(struct tvec_output *o, const char *msg, va_list *ap)
 {
   struct tap_output *t = (struct tap_output *)o;
-  fputs("Bail out!  ", t->fp); tap_report(t, msg, ap);
+
+  fputs("Bail out!  ", t->fmt.fp);
+  tap_report(t, &file_printops, t->fmt.fp, msg, ap);
 }
 
 static void tap_notice(struct tvec_output *o, const char *msg, va_list *ap)
 {
   struct tap_output *t = (struct tap_output *)o;
-  fputs("## ", t->fp); tap_report(t, msg, ap);
+
+  tap_report(t, &tap_printops, t, msg, ap);
 }
 
 static void tap_destroy(struct tvec_output *o)
 {
   struct tap_output *t = (struct tap_output *)o;
 
-  if (t->fp != stdout && t->fp != stderr) fclose(t->fp);
-  dstr_destroy(&t->d);
-  xfree(t);
+  destroy_fmt(&t->fmt,
+             t->fmt.fp == stdout || t->fmt.fp == stderr ? 0 : DFF_CLOSE);
+  xfree(t->outbuf); xfree(t);
 }
 
 static const struct tvec_outops tap_ops = {
@@ -749,8 +1055,8 @@ struct tvec_output *tvec_tapoutput(FILE *fp)
   struct tap_output *t;
 
   t = xmalloc(sizeof(*t)); t->_o.ops = &tap_ops;
-  dstr_create(&t->d);
-  t->f = 0; t->fp = fp;
+  init_fmt(&t->fmt, fp, "## ");
+  t->outbuf = 0; t->outsz = 0;
   return (&t->_o);
 }