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