+static const struct tvec_benchoutops machine_benchops =
+ { machine_bbench, machine_ebench };
+
+/* --- @machine_extend@ --- *
+ *
+ * Arguments: @struct tvec_output *o@ = output sink, secretly a
+ * @struct machine_output@
+ * @const char *name@ = extension name
+ *
+ * Returns: A pointer to the extension implementation, or null.
+ */
+
+static const void *machine_extend(struct tvec_output *o, const char *name)
+{
+ if (STRCMP(name, ==, TVEC_BENCHOUTEXT)) return (&machine_benchops);
+ else return (0);
+}
+
+/* --- @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);
+ x_free(m->a, m->outbuf); x_free(m->a, 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_report, machine_extend, 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;
+
+ XNEW(m); m->a = arena_global; m->_o.ops = &machine_ops;
+ m->f = 0; 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 */
+ arena *a; /* arena for memory allocation */
+ 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, ...)
+{
+ struct tap_output *t = go;
+ size_t n;
+ va_list ap;
+
+ va_start(ap, p);
+ n = gprintf_memputf(t->a, &t->outbuf, &t->outsz, maxsz, p, ap);
+ va_end(ap);
+ 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;
+
+ t->tv = tv; t->grpix = 0;
+ fputs("TAP version 13\n", t->lyt.fp); /* but secretly 14 really */
+}
+
+/* --- @tap_esession@ --- *
+ *
+ * Arguments: @struct tvec_output *o@ = output sink, secretly a
+ * @struct tap_output@
+ *
+ * Returns: Suggested exit code.
+ *
+ * Use: End a test session.
+ *
+ * The TAP driver prints a final summary of the rest results
+ * and returns a suitable exit code. If errors occurred, it
+ * instead prints a `Bail out!' line forcing the reader to
+ * report a failure.
+ */
+
+static int tap_esession(struct tvec_output *o)
+{
+ struct tap_output *t = (struct tap_output *)o;
+ struct tvec_state *tv = t->tv;
+
+ if (tv->f&TVSF_ERROR) {
+ fputs("Bail out! "
+ "Errors found in input; tests may not have run correctly\n",
+ t->lyt.fp);
+ return (2);
+ }
+
+ fprintf(t->lyt.fp, "1..%u\n", t->grpix);
+ t->tv = 0; return (tv->all[TVOUT_LOSE] ? 1 : 0);
+}
+
+/* --- @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;
+ struct tvec_state *tv = t->tv;
+
+ t->grpix++; t->testix = t->previx = 0;
+ t->maxlen = register_maxnamelen(t->tv);
+ fprintf(t->lyt.fp, "# Subtest: %s\n", tv->test->name);
+}
+
+/* --- @tap_skipgroup@ --- *
+ *
+ * Arguments: @struct tvec_output *o@ = output sink, secretly a
+ * @struct tap_output@
+ * @const char *excuse@, @va_list *ap@ = reason for skipping the
+ * group, or null
+ *
+ * Returns: ---
+ *
+ * Use: Report that a test group is being skipped.
+ *
+ * The TAP driver just reports the situation to its output
+ * stream.
+ */
+
+static void tap_skipgroup(struct tvec_output *o,
+ const char *excuse, va_list *ap)
+{
+ struct tap_output *t = (struct tap_output *)o;
+
+ fprintf(t->lyt.fp, " 1..%u\n", t->testix);
+ fprintf(t->lyt.fp, "ok %u %s # SKIP", t->grpix, t->tv->test->name);
+ if (excuse) { fputc(' ', t->lyt.fp); vfprintf(t->lyt.fp, excuse, *ap); }
+ fputc('\n', t->lyt.fp);
+}
+
+/* --- @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;
+ struct tvec_state *tv = t->tv;
+
+ fprintf(t->lyt.fp, " 1..%u\n", t->testix);
+ fprintf(t->lyt.fp, "%s %u - %s\n",
+ tv->curr[TVOUT_LOSE] ? "not ok" : "ok",
+ t->grpix, tv->test->name);
+}
+
+/* --- @tap_btest@ --- *
+ *
+ * Arguments: @struct tvec_output *o@ = output sink, secretly a
+ * @struct tap_output@
+ *
+ * Returns: ---
+ *
+ * Use: Report that a test is starting.
+ *
+ * The TAP driver advances its test counter. (We could do this
+ * by adding up up the counters in @tv->curr@, and add on the
+ * current test, but it's easier this way.)
+ */
+
+static void tap_btest(struct tvec_output *o)
+ { struct tap_output *t = (struct tap_output *)o; t->testix++; }
+
+/* --- @tap_outcome@, @tap_skip@, @tap_fail@ --- *
+ *
+ * Arguments: @struct tvec_output *o@ = output sink, secretly a
+ * @struct tap_output@
+ * @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)
+{
+ struct tap_output *t = (struct tap_output *)o;
+ struct tvec_state *tv = t->tv;
+
+ fprintf(t->lyt.fp, " %s %u - %s:%u%s",
+ t->testix == t->previx ? "##" : head,
+ t->testix, tv->infile, tv->test_lno, tail);
+ if (detail)
+ { fputc(' ', t->lyt.fp); vfprintf(t->lyt.fp, detail, *ap); }
+ fputc('\n', t->lyt.fp);
+ t->previx = t->testix;
+}
+
+static void tap_skip(struct tvec_output *o, const char *excuse, va_list *ap)
+ { tap_outcome(o, "ok", " # SKIP", excuse, ap); }
+static void tap_fail(struct tvec_output *o, const char *detail, va_list *ap)
+ { tap_outcome(o, "not ok", "", detail, ap); }
+
+/* --- @tap_dumpreg@ --- *
+ *
+ * Arguments: @struct tvec_output *o@ = output sink, secretly a
+ * @struct tap_output@
+ * @unsigned disp@ = register disposition
+ * @const union tvec_regval *rv@ = register value
+ * @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns: ---
+ *
+ * Use: Dump a register.
+ *
+ * The TAP driver applies highlighting to mismatching output
+ * registers, but otherwise delegates to the register type
+ * handler and the layout machinery. The result is that the
+ * register dump is marked as a comment and indented.
+ */
+
+static void tap_dumpreg(struct tvec_output *o,
+ unsigned disp, const union tvec_regval *rv,
+ const struct tvec_regdef *rd)
+{
+ struct tap_output *t = (struct tap_output *)o;
+ const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name);
+
+ set_layout_prefix(&t->lyt, " ## ");
+ gprintf(&tap_printops, t, "%*s%s %s = ",
+ 10 + t->maxlen - n, "", ds, rd->name);
+ if (!rv) gprintf(&tap_printops, t, "#<unset>");
+ else rd->ty->dump(rv, rd, 0, &tap_printops, t);
+ layout_char(&t->lyt, '\n');
+}
+
+/* --- @tap_etest@ --- *
+ *
+ * Arguments: @struct tvec_output *o@ = output sink, secretly a
+ * @struct tap_output@
+ * @unsigned outcome@ = the test outcome
+ *
+ * Returns: ---
+ *
+ * Use: Report that a test has finished.
+ *
+ * The TAP driver reports the outcome of the test, if that's not
+ * already decided.
+ */
+
+static void tap_etest(struct tvec_output *o, unsigned outcome)
+{
+ switch (outcome) {
+ case TVOUT_WIN:
+ tap_outcome(o, "ok", "", 0, 0);
+ break;
+ case TVOUT_XFAIL:
+ tap_outcome(o, "not ok", " # TODO expected failure", 0, 0);
+ break;
+ }
+}
+
+/* --- @tap_report@ --- *
+ *
+ * Arguments: @struct tvec_output *o@ = output sink, secretly a
+ * @struct tap_output@
+ * @unsigned level@ = message level (@TVLV_...@)
+ * @const char *msg@, @va_list *ap@ = format string and
+ * arguments
+ *
+ * Returns: ---
+ *
+ * Use: Report a message to the user.
+ *
+ * Messages are reported as comments, so that they can be
+ * accumulated by the reader. An error will cause a later
+ * bailout or, if we crash before then, a missing plan line,
+ * either of which will cause the reader to report a serious
+ * problem.
+ */
+
+static void tap_report(struct tvec_output *o, unsigned level,
+ const char *msg, va_list *ap)
+{
+ struct tap_output *t = (struct tap_output *)o;
+ struct tvec_state *tv = t->tv;
+
+ if (tv->test) set_layout_prefix(&t->lyt, " ## ");
+ else set_layout_prefix(&t->lyt, "## ");
+
+ if (tv->infile) gprintf(&tap_printops, t, "%s:%u: ", tv->infile, tv->lno);
+ gprintf(&tap_printops, t, "%s: ", tvec_strlevel(level));
+ vgprintf(&tap_printops, t, msg, ap);
+ layout_char(&t->lyt, '\n');
+}
+
+/* --- @tap_extend@ --- *
+ *
+ * Arguments: @struct tvec_output *o@ = output sink, secretly a
+ * @struct tap_output@
+ * @const char *name@ = extension name
+ *
+ * Returns: A pointer to the extension implementation, or null.
+ */
+
+static const void *tap_extend(struct tvec_output *o, const char *name)
+ { return (0); }
+
+/* --- @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;
+
+ destroy_layout(&t->lyt,
+ t->lyt.fp == stdout || t->lyt.fp == stderr ? 0 : DLF_CLOSE);
+ x_free(t->a, t->outbuf); x_free(t->a, t);
+}
+
+static const struct tvec_outops tap_ops = {
+ tap_bsession, tap_esession,
+ tap_bgroup, tap_skipgroup, tap_egroup,
+ tap_btest, tap_skip, tap_fail, tap_dumpreg, tap_etest,
+ tap_report, tap_extend, 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;
+
+ XNEW(t); t->a = arena_global; t->_o.ops = &tap_ops;
+ init_layout(&t->lyt, fp, 0);
+ t->outbuf = 0; t->outsz = 0;
+ 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 (@TVLV_...@)
+ * @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@ --- *
+ *
+ * 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 `machine' format is used.
+ */