X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/mLib/blobdiff_plain/67b5031ec6d160b5cae425466a34d1be3b211dd4..d04c0e00da3a27693bbf9cc4f2d5c88e56d80f20:/test/tvec-output.c diff --git a/test/tvec-output.c b/test/tvec-output.c index 9ca4f29..014423e 100644 --- a/test/tvec-output.c +++ b/test/tvec-output.c @@ -36,6 +36,7 @@ #include #include +#include #include "alloc.h" #include "bench.h" @@ -43,7 +44,11 @@ #include "macros.h" #include "quis.h" #include "report.h" +#include "ttycolour.h" + #include "tvec.h" +#include "tvec-bench.h" +#include "tvec-output.h" /*----- Common machinery --------------------------------------------------*/ @@ -61,43 +66,57 @@ static const char *regdisp(unsigned disp) case TVRD_INPUT: return "input"; case TVRD_OUTPUT: return "output"; case TVRD_MATCH: return "matched"; - case TVRD_EXPECT: return "expected"; case TVRD_FOUND: return "found"; + case TVRD_EXPECT: return "expected"; default: abort(); } } -/* --- @getenv_boolean@ --- * +/* --- @interpret_boolean@, @getenv_boolean@ --- * * - * Arguments: @const char *var@ = environment variable name + * Arguments: @const char *val@ = a string + * @const char *var@ = environment variable name * @int dflt@ = default value + * @const char *what, ...@ = format string describing where the + * setting came from * - * Returns: @0@ if the variable is set to something falseish, @1@ if it's - * set to something truish, or @dflt@ otherwise. + * Returns: @0@ if the string, or variable, is set to something + * falseish, @1@ if it's set to something truish, or @dflt@ + * otherwise. */ -static int getenv_boolean(const char *var, int dflt) +static PRINTF_LIKE(3, 4) + int interpret_boolean(const char *val, int dflt, const char *what, ...) { - const char *p; - - p = getenv(var); - if (!p) - return (dflt); - else if (STRCMP(p, ==, "y") || STRCMP(p, ==, "yes") || - STRCMP(p, ==, "t") || STRCMP(p, ==, "true") || - STRCMP(p, ==, "on") || STRCMP(p, ==, "force") || - STRCMP(p, ==, "1")) - return (1); - else if (STRCMP(p, ==, "n") || STRCMP(p, ==, "no") || - STRCMP(p, ==, "f") || STRCMP(p, ==, "false") || - STRCMP(p, ==, "nil") || STRCMP(p, ==, "off") || - STRCMP(p, ==, "0")) - return (0); + dstr d = DSTR_INIT; + va_list ap; + int rc; + + if (!val) + rc = dflt; + else if (STRCMP(val, ==, "y") || STRCMP(val, ==, "yes") || + STRCMP(val, ==, "t") || STRCMP(val, ==, "true") || + STRCMP(val, ==, "on") || STRCMP(val, ==, "force") || + STRCMP(val, ==, "1")) + rc = 1; + else if (STRCMP(val, ==, "n") || STRCMP(val, ==, "no") || + STRCMP(val, ==, "nil") || STRCMP(val, ==, "f") || + STRCMP(val, ==, "false") || + STRCMP(val, ==, "off") || STRCMP(val, ==, "inhibit") || + STRCMP(val, ==, "0")) + rc = 0; else { - moan("ignoring unexpected value `%s' for environment variable `%s'", - var, p); - return (dflt); + va_start(ap, what); dstr_vputf(&d, what, &ap); va_end(ap); + moan("ignoring unexpected value `%s' for %s", val, d.buf); + rc = dflt; } + dstr_destroy(&d); return (rc); +} + +static int getenv_boolean(const char *var, int dflt) +{ + return (interpret_boolean(getenv(var), dflt, + "environment variable `%s'", var)); } /* --- @register_maxnamelen@ --- * @@ -110,31 +129,66 @@ static int getenv_boolean(const char *var, int dflt) static int register_maxnamelen(const struct tvec_state *tv) { const struct tvec_regdef *rd; - int maxlen = 6, n; + int maxlen = 10, n; for (rd = tv->test->regs; rd->name; rd++) { n = strlen(rd->name); if (n > maxlen) maxlen = n; } return (maxlen); } -/*----- Output formatting -------------------------------------------------*/ +/* --- @print_ident@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @unsigned style@ = style to use for register dumps + * @const struct gprintf_ops *gops@ = output operations + * @void *go@ = output state + * + * Returns: --- + * + * Use: Write a benchmark identification to the output. + */ + +static void print_ident(struct tvec_state *tv, unsigned style, + const struct gprintf_ops *gops, void *go) +{ + const struct tvec_regdef *rd; + unsigned f = 0; + +#define f_any 1u + + for (rd = tv->test->regs; rd->name; rd++) + if (rd->f&TVRF_ID) { + if (!(f&f_any)) f |= f_any; + else if (style&TVSF_RAW) gops->putch(go, ' '); + else gprintf(gops, go, ", "); + gprintf(gops, go, "%s", rd->name); + if (style&TVSF_RAW) gops->putch(go, '='); + else gprintf(gops, go, " = "); + rd->ty->dump(&TVEC_REG(tv, in, rd->i)->v, rd, style, gops, go); + } + +#undef f_any +} + +/*----- Output layout -----------------------------------------------------*/ -/* We have two main jobs in output formatting: trimming trailing blanks; and +/* 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. */ -struct format { +struct layout { FILE *fp; /* output file */ const char *prefix, *pfxtail, *pfxlim; /* prefix pointers */ dstr w; /* trailing whitespace */ + dstr ctrl; /* control sequence for next word */ unsigned f; /* flags */ -#define FMTF_NEWL 1u /* start of output line */ +#define LYTF_NEWL 1u /* start of output line */ }; -/* Support macros. These assume `fmt' is defined as a pointer to the `struct - * format' state. +/* Support macros. These assume `lyt' is defined as a pointer to the `struct + * layout' state. */ #define SPLIT_RANGE(tail, base, limit) do { \ @@ -151,27 +205,34 @@ struct format { * file. Return immediately on error. \ */ \ \ - size_t n = limit - base; \ - if (fwrite(base, 1, n, fmt->fp) < n) return (-1); \ + size_t _n = limit - base; \ + if (_n && fwrite(base, 1, _n, lyt->fp) < _n) return (-1); \ } while (0) #define PUT_CHAR(ch) do { \ /* Write CH to the output. Return immediately on error. */ \ \ - if (putc(ch, fmt->fp) == EOF) return (-1); \ + 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 (fmt->prefix) PUT_RANGE(fmt->prefix, fmt->pfxlim); \ + 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 = fmt->w.len; \ - if (n && fwrite(fmt->w.buf, 1, n, fmt->fp) < n) return (-1); \ + size_t _n = lyt->w.len; \ + if (_n && fwrite(lyt->w.buf, 1, _n, lyt->fp) < _n) return (-1); \ +} while (0) + +#define PUT_CTRL do { \ + /* Output the accumulated control string. */ \ + \ + size_t _n = lyt->ctrl.len; \ + if (_n && fwrite(lyt->ctrl.buf, 1, _n, lyt->fp) < _n) return (-1); \ } while (0) #define PUT_PFXINB do { \ @@ -179,69 +240,82 @@ struct format { * one. Return immediately on error. \ */ \ \ - if (fmt->prefix) PUT_RANGE(fmt->prefix, fmt->pfxtail); \ + if (lyt->prefix) PUT_RANGE(lyt->prefix, lyt->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); \ + if (lyt->prefix) \ + DPUTM(&lyt->w, lyt->pfxtail, lyt->pfxlim - lyt->pfxtail); \ } while (0) -/* --- @init_fmt@ --- * +/* --- @set_layout_prefix@ --- * * - * Arguments: @struct format *fmt@ = formatting state to initialize - * @FILE *fp@ = output file - * @const char *prefix@ = prefix string (or null if empty) + * Arguments: @struct layout *lyt@ = layout state + * @const char *prefix@ = new prefix string or null * * Returns: --- * - * Use: Initialize a formatting state. + * 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 init_fmt(struct format *fmt, FILE *fp, const char *prefix) +static void set_layout_prefix(struct layout *lyt, 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; + if (!prefix || !*prefix) + lyt->prefix = lyt->pfxtail = lyt->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); + lyt->prefix = prefix; + l = lyt->pfxlim = prefix + strlen(prefix); + SPLIT_RANGE(q, prefix, l); lyt->pfxtail = q; } } -/* --- @destroy_fmt@ --- * +/* --- @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) +{ + lyt->fp = fp; + lyt->f = LYTF_NEWL; + dstr_create(&lyt->w); dstr_create(&lyt->ctrl); + set_layout_prefix(lyt, prefix); +} + +/* --- @destroy_layout@ --- * * - * Arguments: @struct format *fmt@ = formatting state - * @unsigned f@ = flags (@DFF_...@) + * Arguments: @struct layout *lyt@ = layout state + * @unsigned f@ = flags (@DLF_...@) * * Returns: --- * - * Use: Releases a formatting state and the resources it holds. - * Close the file if @DFF_CLOSE@ is set in @f@; otherwise leave + * 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). */ -#define DFF_CLOSE 1u -static void destroy_fmt(struct format *fmt, unsigned f) +#define DLF_CLOSE 1u +static void destroy_layout(struct layout *lyt, unsigned f) { - if (f&DFF_CLOSE) fclose(fmt->fp); - dstr_destroy(&fmt->w); + if (f&DLF_CLOSE) fclose(lyt->fp); + dstr_destroy(&lyt->w); dstr_destroy(&lyt->ctrl); } -/* --- @format_char@ --- * +/* --- @layout_char@ --- * * - * Arguments: @struct format *fmt@ = formatting state + * Arguments: @struct layout *lyt@ = layout state * @int ch@ = character to write * * Returns: Zero on success, @-1@ on failure. @@ -249,23 +323,23 @@ static void destroy_fmt(struct format *fmt, unsigned f) * Use: Write a single character to the output. */ -static int format_char(struct format *fmt, int ch) +static int layout_char(struct layout *lyt, int ch) { if (ch == '\n') { - if (fmt->f&FMTF_NEWL) PUT_PFXINB; - PUT_CHAR('\n'); fmt->f |= FMTF_NEWL; DRESET(&fmt->w); + if (lyt->f&LYTF_NEWL) PUT_PFXINB; + PUT_CHAR('\n'); lyt->f |= LYTF_NEWL; DRESET(&lyt->w); } else if (isspace(ch)) - DPUTC(&fmt->w, ch); + DPUTC(&lyt->w, ch); else { - if (fmt->f&FMTF_NEWL) { PUT_PFXINB; fmt->f &= ~FMTF_NEWL; } - PUT_SAVED; PUT_CHAR(ch); DRESET(&fmt->w); + if (lyt->f&LYTF_NEWL) { PUT_PFXINB; lyt->f &= ~LYTF_NEWL; } + PUT_SAVED; PUT_CHAR(ch); DRESET(&lyt->w); } return (0); } -/* --- @format_string@ --- * +/* --- @layout_string@ --- * * - * Arguments: @struct format *fmt@ = formatting state + * Arguments: @struct layout *lyt@ = layout state * @const char *p@ = string to write * @size_t sz@ = length of string * @@ -274,7 +348,7 @@ static int format_char(struct format *fmt, int ch) * Use: Write a string to the output. */ -static int format_string(struct format *fmt, const char *p, size_t sz) +static int layout_string(struct layout *lyt, const char *p, size_t sz) { const char *q, *r, *l = p + sz; @@ -326,7 +400,7 @@ static int format_string(struct format *fmt, const char *p, size_t sz) * be omitted. \ */ \ \ - DPUTM(&fmt->w, r, l - r); \ + DPUTM(&lyt->w, r, l - r); \ } while (0) /* Determine the bounds of the first segment. Handling this is the most @@ -343,12 +417,15 @@ static int format_string(struct format *fmt, const char *p, size_t sz) * 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 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 (fmt->f&FMTF_NEWL) { PUT_PFXINB; fmt->f &= ~FMTF_NEWL; } - PUT_SAVED; PUT_NONBLANK; DRESET(&fmt->w); + if (lyt->f&LYTF_NEWL) { PUT_PREFIX; lyt->f &= ~LYTF_NEWL; } + PUT_SAVED; PUT_CTRL; PUT_NONBLANK; + DRESET(&lyt->w); DRESET(&lyt->ctrl); } SAVE_TAIL; return (0); @@ -357,9 +434,13 @@ static int format_string(struct format *fmt, const char *p, size_t sz) /* 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); + if (r > p) { + if (lyt->f&LYTF_NEWL) PUT_PREFIX; + PUT_SAVED; PUT_CTRL; PUT_NONBLANK; + DRESET(&lyt->ctrl); + } 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 @@ -367,7 +448,9 @@ static int format_string(struct format *fmt, const char *p, size_t sz) * newline, so we write the initial prefix and drop the trailing blanks. */ while (q) { - PUT_PREFIX; PUT_NONBLANK; PUT_NEWLINE; + if (r > p) { PUT_PREFIX; PUT_CTRL; PUT_NONBLANK; DRESET(&lyt->ctrl); } + else PUT_PFXINB; + PUT_NEWLINE; SPLIT_SEGMENT; } @@ -376,8 +459,8 @@ static int format_string(struct format *fmt, const char *p, size_t sz) * 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; } + if (r > p) { PUT_PREFIX; PUT_NONBLANK; lyt->f &= ~LYTF_NEWL; } + else { lyt->f |= LYTF_NEWL; SAVE_PFXTAIL; } SAVE_TAIL; #undef SPLIT_SEGMENT @@ -396,120 +479,98 @@ static int format_string(struct format *fmt, const char *p, size_t sz) #undef PUT_CHAR #undef SAVE_PFXTAIL -/*----- Skeleton ----------------------------------------------------------*/ -/* -static void ..._bsession(struct tvec_output *o, struct tvec_state *tv) -static int ..._esession(struct tvec_output *o) -static void ..._bgroup(struct tvec_output *o) -static void ..._skipgroup(struct tvec_output *o, - const char *excuse, va_list *ap) -static void ..._egroup(struct tvec_output *o) -static void ..._btest(struct tvec_output *o) -static void ..._skip(struct tvec_output *o, const char *excuse, va_list *ap) -static void ..._fail(struct tvec_output *o, const char *detail, va_list *ap) -static void ..._dumpreg(struct tvec_output *o, unsigned disp, - union tvec_regval *rv, const struct tvec_regdef *rd) -static void ..._etest(struct tvec_output *o, unsigned outcome) -static void ..._bbench(struct tvec_output *o, - const char *ident, unsigned unit) -static void ..._ebench(struct tvec_output *o, - const char *ident, unsigned unit, - const struct tvec_timing *t) -static void ..._error(struct tvec_output *o, const char *msg, va_list *ap) -static void ..._notice(struct tvec_output *o, const char *msg, va_list *ap) -static void ..._destroy(struct tvec_output *o) - -static const struct tvec_outops ..._ops = { - ..._bsession, ..._esession, - ..._bgroup, ..._egroup, ..._skip, - ..._btest, ..._skip, ..._fail, ..._dumpreg, ..._etest, - ..._bbench, ..._ebench, - ..._error, ..._notice, - ..._destroy -}; -*/ /*----- Human-readable output ---------------------------------------------*/ -#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 -#define HCOL_RED 1u -#define HCOL_GREEN 2u -#define HCOL_YELLOW 3u -#define HCOL_BLUE 4u -#define HCOL_MAGENTA 5u -#define HCOL_CYAN 6u -#define HCOL_WHITE 7u -#define HCF_BRIGHT 8u -#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 HA_ERR (HFG(MAGENTA) | HAF_BOLD) +/* Predefined attributes. */ +#define HIGHLIGHTS(_st, _) \ + _(_st, LOCFN, "lf", TC_FG(CYAN)) /* location filename */ \ + _(_st, LOCLN, "ln", TC_FG(CYAN)) /* location line number */ \ + _(_st, LOCSEP, "ls", TC_FG(BRBLUE)) /* location separator `:' */ \ + _(_st, INFO, "mi", 0) /* information */ \ + _(_st, NOTE, "mn", TC_FG(YELLOW)) /* notices */ \ + _(_st, ERR, "me", TC_FG(MAGENTA) | TCAF_BOLD) /* error messages */ \ + _(_st, UNKLEV, "mu", TC_FG(WHITE) | TC_BG(RED) | TCAF_BOLD) \ + _(_st, DSINPUT, "di", 0) /* disposition for input value */ \ + _(_st, DSOUTPUT, "do", 0) /* ... unsolicited output */ \ + _(_st, DSMATCH, "dm", 0) /* ... matching output */ \ + _(_st, DSFOUND, "df", 0) /* ... incorrect output */ \ + _(_st, DSEXPECT, "dx", 0) /* ... reference output */ \ + _(_st, RNINPUT, "ri", 0) /* register name for input value */ \ + _(_st, RNOUTPUT, "ro", 0) /* ... unsolicited output */ \ + _(_st, RNMATCH, "rm", 0) /* ... matching output */ \ + _(_st, RNFOUND, "rf", 0) /* ... incorrect output */ \ + _(_st, RNEXPECT, "rx", 0) /* ... reference output */ \ + _(_st, VINPUT, "vi", 0) /* input value */ \ + _(_st, VOUTPUT, "vo", 0) /* unsolicited output value */ \ + _(_st, VMATCH, "vm", 0) /* matching output value */ \ + _(_st, VFOUND, "vf", TC_FG(BRRED)) /* incorrect output value */ \ + _(_st, VEXPECT, "vx", TC_FG(GREEN)) /* reference output value */ \ + _(_st, VUNSET, "vu", TC_FG(YELLOW)) /* register not set */ \ + _(_st, LOSE, "ol", TC_FG(RED) | TCAF_BOLD) /* report failure */ \ + _(_st, SKIP, "os", TC_FG(YELLOW)) /* report a skipped test/group */ \ + _(_st, XFAIL, "ox", TC_FG(BLUE) | TCAF_BOLD) /* report expected fail */ \ + _(_st, WIN, "ow", TC_FG(GREEN)) /* report success */ \ + _(_st, SBLOSE, "sl", TC_FG(RED) | TCAF_BOLD) /* scoreboard failure */ \ + _(_st, SBSKIP, "ss", TC_FG(YELLOW)) /* scoreboard skipped test */ \ + _(_st, SBXFAIL, "sx", TC_FG(BLUE) | TCAF_BOLD) /* scoreboard xfail */ \ + _(_st, SBWIN, "sw", 0) /* scoreboard success */ + +TTYCOLOUR_DEFENUM(HIGHLIGHTS, HL_); +#define HL_PLAIN (-1) + +/* Scoreboard indicators. */ +static const char scoreboard[] = { 'x', '_', 'o', '.' }; struct human_output { - struct tvec_output _o; - struct tvec_state *tv; - struct format fmt; - char *outbuf; size_t outsz; - dstr scoreboard; - unsigned attr; - int maxlen; - 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 */ + arena *a; /* arena for memory allocation */ + struct layout lyt; /* output layout */ + char *outbuf; size_t outsz; /* buffer for formatted output */ + dstr scoreboard; /* history of test group results */ + unsigned short attr[HL__LIMIT]; /* highlight attribute map */ + struct ttycolour_state tc; /* terminal colour state */ + int maxlen; /* longest register name */ + unsigned f; /* flags */ + /* bits 0--7 from @TVHF_...@ */ +#define HOF_DUPERR 0x0100u /* duplicate errors to stderr */ +#define HOF_PROGRESS 0x0200u /* progress display is active */ }; -static void set_colour(FILE *fp, int *sep_inout, - const char *norm, const char *bright, - unsigned colour) -{ - if (*sep_inout) putc(*sep_inout, fp); - fprintf(fp, "%s%d", colour&HCF_BRIGHT ? bright : norm, colour&7); - *sep_inout = ';'; -} +/* --- @setattr@, @setattr_layout@ --- * + * + * Arguments: @struct human_output *h@ = output state + * @int hi@ = highlight code to set + * + * Returns: --- + * + * Use: Send a control sequence to the output stream so that + * subsequent text is printed with the given attributes. + */ -static void setattr(struct human_output *h, unsigned attr) +static void setattr_common(struct human_output *h, + const struct gprintf_ops *gops, void *go, int hl) { - unsigned diff = h->attr ^ attr; - int sep = 0; - - if (!diff || !(h->f&HOF_COLOUR)) return; - fputs("\x1b[", h->fmt.fp); + if (h->f&TVHF_COLOUR) + ttycolour_setattr(gops, go, &h->tc, hl < 0 ? 0 : h->attr[hl]); +} - if (diff&HAF_BOLD) { - 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->fmt.fp, &sep, "3", "9", - (attr&HAF_FGMASK) >> HAF_FGSHIFT); - else - { 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->fmt.fp, &sep, "4", "10", - (attr&HAF_BGMASK) >> HAF_BGSHIFT); - else - { if (sep) putc(sep, h->fmt.fp); fputs("49", h->fmt.fp); sep = ';'; } - } +static void setattr(struct human_output *h, int hl) + { setattr_common(h, &file_printops, h->lyt.fp, hl); } - putc('m', h->fmt.fp); h->attr = attr; +static void setattr_layout(struct human_output *h, int hl) + { setattr_common(h, &dstr_printops, &h->lyt.ctrl, hl); } -#undef f_any -} +/* --- @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) { @@ -517,68 +578,78 @@ 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->fmt.fp); + 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; - } - putc(ch, h->fmt.fp); setattr(h, 0); + assert(0 <= ch && ch < TVOUT_LIMIT); + setattr(h, HL_SBLOSE + ch); putc(scoreboard[ch], h->lyt.fp); } +/* --- @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 (tv->test && (h->f&HOF_TTY) && !(h->f&HOF_PROGRESS)) { - fprintf(h->fmt.fp, "%s: ", tv->test->name); - if (!(h->f&HOF_COLOUR)) - dstr_write(&h->scoreboard, h->fmt.fp); - else for (p = h->scoreboard.buf, l = p + h->scoreboard.len; p < l; p++) + if (tv->test && (h->f&TVHF_TTY) && !(h->f&HOF_PROGRESS)) { + fprintf(h->lyt.fp, "%s: ", tv->test->name); + for (p = h->scoreboard.buf, l = p + h->scoreboard.len; p < l; p++) write_scoreboard_char(h, *p); - fflush(h->fmt.fp); h->f |= HOF_PROGRESS; + setattr(h, HL_PLAIN); + 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->fmt.fp) f |= f_flush; - - if (file) { - 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 -} +/* --- @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. + */ static int human_writech(void *go, int ch) - { struct human_output *h = go; return (format_char(&h->fmt, ch)); } + { struct human_output *h = go; return (layout_char(&h->lyt, 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)); } + { struct human_output *h = go; return (layout_string(&h->lyt, p, sz)); } static int human_nwritef(void *go, size_t maxsz, const char *p, ...) { @@ -587,64 +658,136 @@ static int human_nwritef(void *go, size_t maxsz, const char *p, ...) va_list ap; va_start(ap, p); - n = gprintf_memputf(&h->outbuf, &h->outsz, maxsz, p, ap); + n = gprintf_memputf(h->a, &h->outbuf, &h->outsz, maxsz, p, ap); va_end(ap); - return (format_string(&h->fmt, h->outbuf, n)); + if (layout_string(&h->lyt, h->outbuf, n)) return (-1); + return (n); } static const struct gprintf_ops human_printops = { human_writech, human_writem, human_nwritef }; +/* --- @human_bsession@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct human_output@ + * @struct tvec_state *tv@ = the test state producing output + * + * Returns: --- + * + * Use: Begin a test session. + * + * The human driver just records the test state for later + * reference. + */ + static void human_bsession(struct tvec_output *o, struct tvec_state *tv) { struct human_output *h = (struct human_output *)o; h->tv = tv; } -static void report_skipped(struct human_output *h, unsigned n) +/* --- @human_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_report_unusual(struct human_output *h, + unsigned nxfail, unsigned nskip) { - if (n) { - fprintf(h->fmt.fp, " (%u ", n); - setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0); - fputc(')', h->fmt.fp); + unsigned f = 0; +#define f_any 1u + + if (nxfail) { + fprintf(h->lyt.fp, "%s%u ", f&f_any ? ", " : " (", nxfail); + setattr(h, HL_XFAIL); + fprintf(h->lyt.fp, "expected %s", nxfail == 1 ? "failure" : "failures"); + setattr(h, HL_PLAIN); + f |= f_any; + } + + if (nskip) { + fprintf(h->lyt.fp, "%s%u ", f&f_any ? ", " : " (", nskip); + setattr(h, HL_SKIP); fputs("skipped", h->lyt.fp); setattr(h, HL_PLAIN); + f |= f_any; } + + if (f&f_any) fputc(')', h->lyt.fp); + +#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->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->fmt.fp); setattr(h, 0); - fprintf(h->fmt.fp, " %s%u %s", + setattr(h, HL_WIN); fputs("PASSED", h->lyt.fp); setattr(h, HL_PLAIN); + fprintf(h->lyt.fp, " %s%u %s", !(all_skip || grps_skip) ? "all " : "", - all_win, all_win == 1 ? "test" : "tests"); - report_skipped(h, all_skip); - fprintf(h->fmt.fp, " in %u %s", + all_pass, all_pass == 1 ? "test" : "tests"); + human_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); + human_report_unusual(h, 0, grps_skip); } else { - setattr(h, HA_LOSE); fputs("FAILED", h->fmt.fp); setattr(h, 0); - fprintf(h->fmt.fp, " %u out of %u %s", + setattr(h, HL_LOSE); fputs("FAILED", h->lyt.fp); setattr(h, HL_PLAIN); + fprintf(h->lyt.fp, " %u out of %u %s", all_lose, all_run, all_run == 1 ? "test" : "tests"); - report_skipped(h, all_skip); - fprintf(h->fmt.fp, " in %u out of %u %s", + human_report_unusual(h, all_xfail, all_skip); + fprintf(h->lyt.fp, " in %u out of %u %s", grps_lose, grps_run, grps_run == 1 ? "group" : "groups"); - report_skipped(h, grps_skip); + human_report_unusual(h, 0, grps_skip); } - fputc('\n', h->fmt.fp); + fputc('\n', h->lyt.fp); if (tv->f&TVSF_ERROR) { - 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); + setattr(h, HL_ERR); fputs("ERRORS", h->lyt.fp); setattr(h, HL_PLAIN); + fputs(" found in input; tests may not have run correctly\n", h->lyt.fp); } - h->tv = 0; return (tv->f&TVSF_ERROR ? 2 : 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; @@ -653,74 +796,173 @@ static void human_bgroup(struct tvec_output *o) dstr_reset(&h->scoreboard); show_progress(h); } +/* --- @human_skipgroup@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct human_output@ + * @const char *excuse@, @va_list *ap@ = reason for skipping the + * group, or null + * + * Returns: --- + * + * Use: Report that a test group is being skipped. + * + * The human driver just reports the situation to its output + * stream. + */ + static void human_skipgroup(struct tvec_output *o, const char *excuse, va_list *ap) { 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->fmt.fp); setattr(h, 0); - } else { - fprintf(h->fmt.fp, "%s: ", h->tv->test->name); - setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0); + if (!(h->f&TVHF_TTY)) + fprintf(h->lyt.fp, "%s ", h->tv->test->name); + else { + show_progress(h); h->f &= ~HOF_PROGRESS; + if (h->scoreboard.len) putc(' ', h->lyt.fp); } - if (excuse) { fputs(": ", h->fmt.fp); vfprintf(h->fmt.fp, excuse, *ap); } - fputc('\n', h->fmt.fp); + setattr(h, HL_SKIP); fputs("skipped", h->lyt.fp); setattr(h, HL_PLAIN); + if (excuse) { fputs(": ", h->lyt.fp); vfprintf(h->lyt.fp, excuse, *ap); } + fputc('\n', h->lyt.fp); } +/* --- @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], lose = tv->curr[TVOUT_LOSE], - skip = tv->curr[TVOUT_SKIP], run = win + lose; + 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->fmt.fp, "%s:", h->tv->test->name); + if (h->f&TVHF_TTY) h->f &= ~HOF_PROGRESS; + else fprintf(h->lyt.fp, "%s:", h->tv->test->name); if (lose) { - fprintf(h->fmt.fp, " %u/%u ", lose, run); - setattr(h, HA_LOSE); fputs("FAILED", h->fmt.fp); setattr(h, 0); - report_skipped(h, skip); + fprintf(h->lyt.fp, " %u/%u ", lose, run); + setattr(h, HL_LOSE); fputs("FAILED", h->lyt.fp); setattr(h, HL_PLAIN); + human_report_unusual(h, xfail, skip); } else { - fputc(' ', h->fmt.fp); setattr(h, HA_WIN); - fputs("ok", h->fmt.fp); setattr(h, 0); - report_skipped(h, skip); + fputc(' ', h->lyt.fp); setattr(h, HL_WIN); + fputs("ok", h->lyt.fp); setattr(h, HL_PLAIN); + human_report_unusual(h, xfail, skip); } - fputc('\n', h->fmt.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) +/* --- @human_report_location@ --- * + * + * Arguments: @struct human_output *h@ = output state + * @FILE *fp@ = stream to write the location on + * @const char *file@ = filename + * @unsigned lno@ = line number + * + * Returns: --- + * + * Use: Print the filename and line number to the output stream @fp@. + * Also, if appropriate, print interleaved highlighting control + * codes to our usual output stream. If @file@ is null then do + * nothing. + */ + +static void human_report_location(struct human_output *h, FILE *fp, + const char *file, unsigned lno) +{ + if (!file) + /* nothing to do */; + else if (fp != h->lyt.fp || !(h->f&TVHF_COLOUR)) + fprintf(fp, "%s:%u: ", file, lno); + else { + setattr(h, HL_LOCFN); fputs(file, fp); + setattr(h, HL_LOCSEP); fputc(':', fp); + setattr(h, HL_LOCLN); fprintf(fp, "%u", lno); + setattr(h, HL_LOCSEP); fputc(':', fp); + setattr(h, HL_PLAIN); fputc(' ', fp); + } +} + +/* --- @human_outcome@, @human_skip@, @human_fail@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct human_output@ + * @unsigned attr@ = attribute to apply to the outcome + * @const char *outcome@ = outcome string to report + * @const char *detail@, @va_list *ap@ = a detail message + * @const char *excuse@, @va_list *ap@ = reason for skipping the + * test + * + * Returns: --- + * + * Use: Report that a test has been skipped or failed. + * + * The human driver reports the situation on its output stream. + */ + +static void human_outcome(struct tvec_output *o, + unsigned attr, const char *outcome, + const char *detail, va_list *ap) { struct human_output *h = (struct human_output *)o; struct tvec_state *tv = h->tv; clear_progress(h); - report_location(h, h->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); + human_report_location(h, h->lyt.fp, tv->infile, tv->test_lno); + fprintf(h->lyt.fp, "`%s' ", tv->test->name); + setattr(h, attr); fputs(outcome, h->lyt.fp); setattr(h, HL_PLAIN); + if (detail) { fputs(": ", h->lyt.fp); vfprintf(h->lyt.fp, detail, *ap); } + fputc('\n', h->lyt.fp); } +static void human_skip(struct tvec_output *o, + const char *excuse, va_list *ap) + { human_outcome(o, HL_SKIP, "skipped", excuse, ap); } 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->tv; + { human_outcome(o, HL_LOSE, "FAILED", detail, ap); } - clear_progress(h); - 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); -} +/* --- @human_dumpreg@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct human_output@ + * @unsigned disp@ = register disposition + * @const union tvec_regval *rv@ = register value + * @const struct tvec_regdef *rd@ = register definition + * + * Returns: --- + * + * Use: Dump a register. + * + * The human driver applies highlighting to mismatching output + * registers, but otherwise delegates to the register type + * handler and the layout machinery. + */ static void human_dumpreg(struct tvec_output *o, unsigned disp, const union tvec_regval *rv, @@ -730,345 +972,1703 @@ static void human_dumpreg(struct tvec_output *o, const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name); clear_progress(h); - gprintf(&human_printops, h, "%*s%s %s = ", - 10 + h->maxlen - n, "", ds, rd->name); - if (h->f&HOF_COLOUR) { - if (!rv) setattr(h, HFG(YELLOW)); - else if (disp == TVRD_FOUND) setattr(h, HFG(RED)); - else if (disp == TVRD_EXPECT) setattr(h, HFG(GREEN)); + gprintf(&human_printops, h, "%*s", 10 + h->maxlen - n, ""); + setattr_layout(h, HL_DSINPUT + disp); + gprintf(&human_printops, h, "%s", ds); + setattr(h, HL_PLAIN); layout_char(&h->lyt, ' '); + setattr_layout(h, HL_RNINPUT + disp); + gprintf(&human_printops, h, "%s", rd->name); + setattr(h, HL_PLAIN); gprintf(&human_printops, h, " = "); + if (!rv) { + setattr_layout(h, HL_VUNSET); + gprintf(&human_printops, h, "#unset"); + } else { + setattr_layout(h, HL_VINPUT + disp); + rd->ty->dump(rv, rd, 0, &human_printops, h); } - if (!rv) gprintf(&human_printops, h, "#unset"); - else rd->ty->dump(rv, rd, 0, &human_printops, h); - setattr(h, 0); format_char(&h->fmt, '\n'); + setattr(h, HL_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; - int ch; - if (h->f&HOF_TTY) { + if (h->f&TVHF_TTY) { show_progress(h); - switch (outcome) { - case TVOUT_WIN: ch = '.'; break; - case TVOUT_LOSE: ch = 'x'; break; - case TVOUT_SKIP: ch = '_'; break; - default: abort(); - } - dstr_putc(&h->scoreboard, ch); - write_scoreboard_char(h, ch); fflush(h->fmt.fp); + dstr_putc(&h->scoreboard, outcome); write_scoreboard_char(h, outcome); + setattr(h, HL_PLAIN); fflush(h->lyt.fp); } } -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->tv; - - clear_progress(h); - fprintf(h->fmt.fp, "%s: %s: ", tv->test->name, ident); fflush(h->fmt.fp); -} - -static void human_ebench(struct tvec_output *o, - const char *ident, unsigned unit, - const struct bench_timing *tm) -{ - struct human_output *h = (struct human_output *)o; - - tvec_benchreport(&human_printops, h->fmt.fp, unit, tm); - fputc('\n', h->fmt.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, const char *msg, va_list *ap) +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 levhl; dstr d = DSTR_INIT; + unsigned f = 0; +#define f_progress 1u dstr_vputf(&d, msg, ap); dstr_putc(&d, '\n'); - 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); + switch (level) { +#define CASE(tag, name, val) \ + case TVLEV_##tag: levstr = name; levhl = HL_##tag; break; + TVEC_LEVELS(CASE) + default: levstr = "??"; levhl = HL_UNKLEV; break; + } + + if (h->f&HOF_PROGRESS) { clear_progress(h); f |= f_progress; } if (h->f&HOF_DUPERR) { - report_location(h, h->fmt.fp, tv->infile, tv->lno); - fwrite(d.buf, 1, d.len, h->fmt.fp); + fprintf(stderr, "%s: ", QUIS); + human_report_location(h, stderr, tv->infile, tv->lno); + fprintf(stderr, "%s: ", levstr); + fwrite(d.buf, 1, d.len, stderr); } - show_progress(h); + + human_report_location(h, h->lyt.fp, tv->infile, tv->lno); + setattr(h, levhl); fputs(levstr, h->lyt.fp); + setattr(h, HL_PLAIN); fputs(": ", h->lyt.fp); + fwrite(d.buf, 1, d.len, h->lyt.fp); + + if (f&f_progress) show_progress(h); + dstr_destroy(&d); + +#undef f_progress } -static void human_destroy(struct tvec_output *o) +/* --- @human_bbench@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct human_output@ + * @const char *desc@ = adhoc test description + * @unsigned unit@ = measurement unit (@BTU_...@) + * + * 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 *desc, unsigned unit) { struct human_output *h = (struct human_output *)o; + struct tvec_state *tv = h->tv; - destroy_fmt(&h->fmt, h->f&HOF_DUPERR ? DFF_CLOSE : 0); - dstr_destroy(&h->scoreboard); - xfree(h->outbuf); xfree(h); + clear_progress(h); + gprintf(&human_printops, h, "%s ", tv->test->name); + if (desc) gprintf(&human_printops, h, "%s", desc); + else print_ident(tv, TVSF_COMPACT, &human_printops, h); + gprintf(&human_printops, h, ": "); + if (h->f&TVHF_TTY) fflush(h->lyt.fp); } -static const struct tvec_outops human_ops = { - human_bsession, human_esession, - human_bgroup, human_skipgroup, human_egroup, - human_btest, human_skip, human_fail, human_dumpreg, human_etest, - human_bbench, human_ebench, - human_report, human_report, - human_destroy -}; +/* --- @human_ebench@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct human_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 human driver just delegates to the default benchmark + * reporting, via the layout machinery. + */ -struct tvec_output *tvec_humanoutput(FILE *fp) +static void human_ebench(struct tvec_output *o, + const char *desc, unsigned unit, + const struct bench_timing *t) { - struct human_output *h; - const char *p; + struct human_output *h = (struct human_output *)o; - h = xmalloc(sizeof(*h)); h->_o.ops = &human_ops; - h->f = 0; h->attr = 0; + tvec_benchreport(&human_printops, h, unit, 0, t); + layout_char(&h->lyt, '\n'); +} - init_fmt(&h->fmt, fp, 0); - h->outbuf = 0; h->outsz = 0; +static const struct tvec_benchoutops human_benchops = + { human_bbench, human_ebench }; - switch (getenv_boolean("TVEC_TTY", -1)) { - case 1: h->f |= HOF_TTY; break; - case 0: break; - default: - if (isatty(fileno(fp))) h->f |= HOF_TTY; +/* --- @human_extend@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct human_output@ + * @const char *name@ = extension name + * + * Returns: A pointer to the extension implementation, or null. + */ + +static const void *human_extend(struct tvec_output *o, const char *name) +{ + if (STRCMP(name, ==, TVEC_BENCHOUTEXT)) return (&human_benchops); + else return (0); +} + +/* --- @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; + + destroy_layout(&h->lyt, + h->lyt.fp == stdout || h->lyt.fp == stderr ? 0 : DLF_CLOSE); + dstr_destroy(&h->scoreboard); + x_free(h->a, h->outbuf); x_free(h->a, h); +} + +static const struct tvec_outops human_ops = { + human_bsession, human_esession, + human_bgroup, human_skipgroup, human_egroup, + human_btest, human_skip, human_fail, human_dumpreg, human_etest, + human_report, human_extend, human_destroy +}; + +/* --- @tvec_humanoutput@ --- * + * + * Arguments: @FILE *fp@ = output file to write on + * @unsigned f, m@ = flags and mask + * + * Returns: An output formatter. + * + * Use: Return an output formatter which writes on @fp@ with the + * expectation that a human will interpret the output. + * + * The flags @f@ and mask @m@ operate together. Flag bits not + * covered by the mask must be left clear, i.e., @f&~m$ must be + * zero; the semantics are that a set mask bit indicates that + * the corresponding bit of @f@ should control the indicated + * behaviour; a clear mask bit indicates that a suitable default + * should be chosen based on environmental conditions. + * + * If @TVHF_TTY@ is set, then the output shows a `scoreboard' + * indicating the outcome of each test case attempted, providing + * a visual indication of progress. If @TVHF_COLOUR@ is set, + * then the output uses control codes for colour and other + * highlighting. It is unusual to set @TVHF_COLOUR@ without + * @TVHF_TTY@, this is permitted anyway. + * + * The environment variables %|TVEC_TTY|% and %|TVEC_COLOUR|% + * provide default values for these settings. If they are not + * set, then @TVHF_TTY@ is set if @fp@ refers to a terminal, and + * @TVHF_COLOUR@ is set if @TVHF_TTY@ is set and, additionally, + * the %|TERM|% environment variable is set to a value other + * than %|dumb|%. + */ + +struct tvec_output *tvec_humanoutput(FILE *fp, unsigned f, unsigned m) +{ + struct human_output *h; + const char *p; + struct stat st_out, st_err; + int rc_out, rc_err; + + static const struct ttycolour_style hltab[] = + TTYCOLOUR_INITTAB(HIGHLIGHTS); + + assert(!(f&~m)); + + if (!(m&TVHF_TTY)) + switch (getenv_boolean("TVEC_TTY", -1)) { + case 1: f |= TVHF_TTY; break; + case 0: break; + default: + if (isatty(fileno(fp))) f |= TVHF_TTY; + break; + } + if (!(m&TVHF_COLOUR)) + switch (getenv_boolean("TVEC_COLOUR", -1)) { + case 1: f |= TVHF_COLOUR; break; + case 0: break; + default: + if (f&TVHF_TTY) { + p = getenv("TERM"); + if (p && STRCMP(p, !=, "dumb")) f |= TVHF_COLOUR; + } + break; + } + + /* Decide whether to write copies of reports to stderr. + * + * There's not much point if the output is already going to stderr. + * Otherwise, check to see whether stdout is the same underlying file. + */ + if (fp != stderr) { + rc_out = fstat(fileno(fp), &st_out); + rc_err = fstat(STDERR_FILENO, &st_err); + if (!rc_err && (rc_out || + st_out.st_dev != st_err.st_dev || + st_out.st_ino != st_err.st_ino)) + f |= HOF_DUPERR; + } + + XNEW(h); h->a = arena_global; h->_o.ops = &human_ops; + h->f = f; + + /* Initialize the colour tables. */ + if (h->f&TVHF_COLOUR) { + ttycolour_config(h->attr, "TVEC_COLOURS", TCIF_GETENV | TCIF_REPORT, + hltab); + ttycolour_init(&h->tc); + } + + init_layout(&h->lyt, fp, 0); + h->outbuf = 0; h->outsz = 0; + + dstr_create(&h->scoreboard); + return (&h->_o); +} + +/*----- Machine-readable output -------------------------------------------*/ + +struct machine_output { + struct tvec_output _o; /* output base class */ + struct tvec_state *tv; /* stashed testing state */ + arena *a; /* arena for memory allocation */ + FILE *fp; /* output stream */ + char *outbuf; size_t outsz; /* buffer for formatted output */ + unsigned grpix, testix; /* group and test indices */ + unsigned f; /* flags */ +#define MF_BENCH 1u /* current test is a benchmark */ +}; + +/* --- @machine_writech@, @machine_write@, @machine_writef@ --- * + * + * Arguments: @void *go@ = output sink, secretly a @struct machine_output@ + * @int ch@ = character to write + * @const char *@p@, @size_t sz@ = string (with explicit length) + * to write + * @const char *p, ...@ = format control string and arguments to + * write + * + * Returns: --- + * + * Use: Write characters, strings, or formatted strings to the + * output, applying appropriate quoting. + */ + +static void machine_escape(struct machine_output *m, int ch) +{ + switch (ch) { + case 0: fputs("\\0", m->fp); break; + case '\a': fputs("\\a", m->fp); break; + case '\b': fputs("\\b", m->fp); break; + case '\x1b': fputs("\\e", m->fp); break; + case '\f': fputs("\\f", m->fp); break; + case '\n': fputs("\\n", m->fp); break; + case '\r': fputs("\\r", m->fp); break; + case '\t': fputs("\\t", m->fp); break; + case '\v': fputs("\\v", m->fp); break; + case '"': fputs("\\\"", m->fp); break; + case '\\': fputs("\\\\", m->fp); break; + default: fprintf(m->fp, "\\x{%02x}", ch); + } +} + +static int machine_writech(void *go, int ch) +{ + struct machine_output *m = go; + + if (ISPRINT(ch)) putc(ch, m->fp); + else machine_escape(m, ch); + return (1); +} + +static int machine_writem(void *go, const char *p, size_t sz) +{ + struct machine_output *m = go; + const char *q, *l = p + sz; + + for (;;) { + q = p; + for (;;) { + if (q >= l) goto final; + if (!ISPRINT(*q) || *q == '\\' || *q == '"') break; + q++; + } + if (p < q) fwrite(p, 1, q - p, m->fp); + p = q; machine_escape(m, (unsigned char)*p++); + } +final: + if (p < l) fwrite(p, 1, l - p, m->fp); + return (sz); +} + +static int machine_nwritef(void *go, size_t maxsz, const char *p, ...) +{ + struct machine_output *m = go; + int n; + va_list ap; + + va_start(ap, p); + n = gprintf_memputf(m->a, &m->outbuf, &m->outsz, maxsz, p, ap); + va_end(ap); + return (machine_writem(m, m->outbuf, n)); +} + +static const struct gprintf_ops machine_printops = + { machine_writech, machine_writem, machine_nwritef }; + +/* --- @machine_maybe_quote@ --- * + * + * Arguments: @struct machine_output *m@ = output sink + * @const char *p@ = pointer to string + * + * Returns: --- + * + * Use: Print the string @p@, quoting it if necessary. + */ + +static void machine_maybe_quote(struct machine_output *m, const char *p) +{ + const char *q; + + for (q = p; *q; q++) + if (!ISPRINT(*q) || ISSPACE(*q)) goto quote; + fputs(p, m->fp); return; +quote: + putc('"', m->fp); machine_writem(m, p, strlen(p)); putc('"', m->fp); +} + +/* --- @machine_bsession@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct machine_output@ + * @struct tvec_state *tv@ = the test state producing output + * + * Returns: --- + * + * Use: Begin a test session. + * + * The TAP driver records the test state for later reference, + * initializes the group index counter, and prints the version + * number. + */ + +static void machine_bsession(struct tvec_output *o, struct tvec_state *tv) +{ + struct machine_output *m = (struct machine_output *)o; + + m->tv = tv; m->grpix = 0; +} + +/* --- @machine_show_stats@ --- * + * + * Arguments: @struct machine_output *m@ = output sink + * @unsigned *out[TVOUT_LIMIT]@ = outcome counter table + * + * Returns: --- + * + * Use: Print a machine readable outcome statistics table + */ + +static void machine_show_stats(struct machine_output *m, + unsigned out[TVOUT_LIMIT]) +{ + static const char *outtab[] = { "lose", "skip", "xfail", "win" }; + unsigned i; + + for (i = 0; i < TVOUT_LIMIT; i++) { + if (i) putc(' ', m->fp); + fprintf(m->fp, "%s=%u", outtab[i], out[i]); + } +} + +/* --- @machine_esession@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct machine_output@ + * + * Returns: Suggested exit code. + * + * Use: End a test session. + * + * The TAP driver prints a final summary of the rest results + * and returns a suitable exit code. If errors occurred, it + * instead prints a `Bail out!' line forcing the reader to + * report a failure. + */ + +static int machine_esession(struct tvec_output *o) +{ + struct machine_output *m = (struct machine_output *)o; + struct tvec_state *tv = m->tv; + + fputs("END groups: ", m->fp); + machine_show_stats(m, tv->grps); + fputs("; tests: ", m->fp); + machine_show_stats(m, tv->all); + putc('\n', m->fp); + m->tv = 0; return (tv->f&TVSF_ERROR ? 2 : tv->all[TVOUT_LOSE] ? 1 : 0); +} + +/* --- @machine_bgroup@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct machine_output@ + * + * Returns: --- + * + * Use: Begin a test group. + * + * The TAP driver determines the length of the longest + * register name, resets the group progress scoreboard, and + * activates the progress display. + */ + +static void machine_bgroup(struct tvec_output *o) +{ + struct machine_output *m = (struct machine_output *)o; + struct tvec_state *tv = m->tv; + + fputs("BGROUP ", m->fp); + machine_maybe_quote(m, tv->test->name); + putc('\n', m->fp); + m->grpix++; m->testix = 0; +} + +/* --- @machine_skipgroup@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct machine_output@ + * @const char *excuse@, @va_list *ap@ = reason for skipping the + * group, or null + * + * Returns: --- + * + * Use: Report that a test group is being skipped. + * + * The TAP driver just reports the situation to its output + * stream. + */ + +static void machine_skipgroup(struct tvec_output *o, + const char *excuse, va_list *ap) +{ + struct machine_output *m = (struct machine_output *)o; + struct tvec_state *tv = m->tv; + + fputs("SKIPGRP ", m->fp); + machine_maybe_quote(m, tv->test->name); + if (excuse) { + fputs(" \"", m->fp); + vgprintf(&machine_printops, m, excuse, ap); + putc('\"', m->fp); + } + putc('\n', m->fp); +} + +/* --- @machine_egroup@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct machine_output@ + * + * Returns: --- + * + * Use: Report that a test group has finished. + * + * The TAP driver reports a summary of the group's tests. + */ + +static void machine_egroup(struct tvec_output *o) +{ + struct machine_output *m = (struct machine_output *)o; + struct tvec_state *tv = m->tv; + + if (!(tv->f&TVSF_SKIP)) { + fputs("EGROUP ", m->fp); machine_maybe_quote(m, tv->test->name); + putc(' ', m->fp); machine_show_stats(m, tv->curr); + putc('\n', m->fp); + } +} + +/* --- @machine_btest@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct machine_output@ + * + * Returns: --- + * + * Use: Report that a test is starting. + * + * The TAP driver advances its test counter. (We could do this + * by adding up up the counters in @tv->curr@, and add on the + * current test, but it's easier this way.) + */ + +static void machine_btest(struct tvec_output *o) { ; } + +/* --- @machine_report_location@ --- * + * + * Arguments: @struct human_output *h@ = output state + * @const char *file@ = filename + * @unsigned lno@ = line number + * + * Returns: --- + * + * Use: Print the filename and line number to the output stream. + */ + +static void machine_report_location(struct machine_output *m, + const char *file, unsigned lno) +{ + if (file) { + putc(' ', m->fp); + machine_maybe_quote(m, file); + fprintf(m->fp, ":%u", lno); + } +} + +/* --- @machine_outcome@, @machine_skip@, @machine_fail@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct machine_output@ + * @const char *outcome@ = outcome string to report + * @const char *detail@, @va_list *ap@ = a detail message + * @const char *excuse@, @va_list *ap@ = reason for skipping the + * test + * + * Returns: --- + * + * Use: Report that a test has been skipped or failed. + */ + +static void machine_outcome(struct tvec_output *o, const char *outcome, + const char *detail, va_list *ap) +{ + struct machine_output *m = (struct machine_output *)o; + struct tvec_state *tv = m->tv; + + fprintf(m->fp, "%s %u", outcome, m->testix); + machine_report_location(m, tv->infile, tv->test_lno); + if (detail) + { putc(' ', m->fp); vgprintf(&machine_printops, m, detail, ap); } + putc('\n', m->fp); +} + +static void machine_skip(struct tvec_output *o, + const char *excuse, va_list *ap) + { machine_outcome(o, "SKIP", excuse, ap); } +static void machine_fail(struct tvec_output *o, + const char *detail, va_list *ap) + { machine_outcome(o, "FAIL", detail, ap); } + +/* --- @machine_dumpreg@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct machine_output@ + * @unsigned disp@ = register disposition + * @const union tvec_regval *rv@ = register value + * @const struct tvec_regdef *rd@ = register definition + * + * Returns: --- + * + * Use: Dump a register. + * + * The machine driver applies highlighting to mismatching output + * registers, but otherwise delegates to the register type + * handler. + */ + +static void machine_dumpreg(struct tvec_output *o, + unsigned disp, const union tvec_regval *rv, + const struct tvec_regdef *rd) +{ + struct machine_output *m = (struct machine_output *)o; + unsigned f = 0; +#define f_reg 1u +#define f_nl 2u + + switch (disp) { + case TVRD_INPUT: fputs("\tINPUT ", m->fp); f |= f_reg | f_nl; break; + case TVRD_OUTPUT: fputs("\tOUTPUT ", m->fp); f |= f_reg | f_nl; break; + case TVRD_MATCH: fputs("\tMATCH ", m->fp); f |= f_reg | f_nl; break; + case TVRD_FOUND: fputs("\tMISMATCH ", m->fp); f |= f_reg; break; + case TVRD_EXPECT: fputs(" /= ", m->fp); f |= f_nl; break; + default: abort(); + } + + if (f&f_reg) fprintf(m->fp, "%s = ", rd->name); + if (!rv) fputs("#unset", m->fp); + else rd->ty->dump(rv, rd, TVSF_RAW, &file_printops, m->fp); + if (f&f_nl) putc('\n', m->fp); + +#undef f_reg +#undef f_newline +} + +/* --- @machine_etest@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct machine_output@ + * @unsigned outcome@ = the test outcome + * + * Returns: --- + * + * Use: Report that a test has finished. + * + * The machine driver reports the outcome of the test, if that's + * not already decided. + */ + +static void machine_etest(struct tvec_output *o, unsigned outcome) +{ + struct machine_output *m = (struct machine_output *)o; + + if (!(m->f&MF_BENCH)) switch (outcome) { + case TVOUT_WIN: machine_outcome(o, "WIN", 0, 0); break; + case TVOUT_XFAIL: machine_outcome(o, "XFAIL", 0, 0); break; + } + m->testix++; m->f &= ~MF_BENCH; +} + +/* --- @machine_report@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct machine_output@ + * @unsigned level@ = message level (@TVLEV_...@) + * @const char *msg@, @va_list *ap@ = format string and + * arguments + * + * Returns: --- + * + * Use: Report a message to the user. + * + * Each report level has its own output tag + */ + +static void machine_report(struct tvec_output *o, unsigned level, + const char *msg, va_list *ap) +{ + struct machine_output *m = (struct machine_output *)o; + struct tvec_state *tv = m->tv; + const char *p; + + for (p = tvec_strlevel(level); *p; p++) putc(TOUPPER(*p), m->fp); + machine_report_location(m, tv->infile, tv->lno); + fputs(" \"", m->fp); vgprintf(&machine_printops, m, msg, ap); + putc('"', m->fp); + putc('\n', m->fp); +} + +/* --- @machine_bbench@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct machine_output@ + * @const char *desc@ = adhoc test description + * @unsigned unit@ = measurement unit (@BTU_...@) + * + * Returns: --- + * + * Use: Report that a benchmark has started. + * + * The machine driver does nothing here. All of the reporting + * happens in @machine_ebench@. + */ + +static void machine_bbench(struct tvec_output *o, + const char *desc, unsigned unit) + { ; } + +/* --- @machine_ebench@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct machine_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 machine driver prints the result as a `BENCH' output + * line, in raw format. + */ + +static void machine_ebench(struct tvec_output *o, + const char *desc, unsigned unit, + const struct bench_timing *t) +{ + struct machine_output *m = (struct machine_output *)o; + struct tvec_state *tv = m->tv; + + fprintf(m->fp, "BENCH %u", m->testix); + machine_report_location(m, tv->infile, tv->test_lno); + putc(' ', m->fp); + if (desc) machine_maybe_quote(m, desc); + else print_ident(tv, TVSF_RAW, &file_printops, m->fp); + fputs(": ", m->fp); + tvec_benchreport(&file_printops, m->fp, unit, TVSF_RAW, t); + putc('\n', m->fp); m->f |= MF_BENCH; +} + +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, "#"); + 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; - } - switch (getenv_boolean("TVEC_COLOUR", -1)) { - case 1: h->f |= HOF_COLOUR; break; - case 0: break; - default: - if (h->f&HOF_TTY) { - p = getenv("TERM"); - if (p && STRCMP(p, !=, "dumb")) h->f |= HOF_COLOUR; - } + case TVOUT_XFAIL: + tap_outcome(o, "not ok", " # TODO expected failure", 0, 0); break; } +} - if (fp != stderr && (fp != stdout || !(h->f&HOF_TTY))) h->f |= HOF_DUPERR; - dstr_create(&h->scoreboard); - return (&h->_o); +/* --- @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'); } -/*----- Perl's `Test Anything Protocol' -----------------------------------*/ +/* --- @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. + */ -struct tap_output { - struct tvec_output _o; - struct tvec_state *tv; - struct format fmt; - char *outbuf; size_t outsz; - int maxlen; -}; +static const void *tap_extend(struct tvec_output *o, const char *name) + { return (0); } -static int tap_writech(void *go, int ch) - { struct tap_output *t = go; return (format_char(&t->fmt, ch)); } +/* --- @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 int tap_writem(void *go, const char *p, size_t sz) - { struct human_output *t = go; return (format_string(&t->fmt, p, sz)); } +static void tap_destroy(struct tvec_output *o) +{ + struct tap_output *t = (struct tap_output *)o; -static int tap_nwritef(void *go, size_t maxsz, const char *p, ...) + 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 human_output *t = go; - size_t n; - va_list ap; + struct tap_output *t; - va_start(ap, p); - n = gprintf_memputf(&t->outbuf, &t->outsz, maxsz, p, ap); - va_end(ap); - return (format_string(&t->fmt, t->outbuf, n)); + 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); } -static const struct gprintf_ops tap_printops = - { tap_writech, tap_writem, tap_nwritef }; +/*----- Automake support --------------------------------------------------*/ -static void tap_bsession(struct tvec_output *o, struct tvec_state *tv) +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 tap_output *t = (struct tap_output *)o; + struct automake_output *am = (struct automake_output *)o; - t->tv = tv; - fputs("TAP version 13\n", t->fmt.fp); + am->tv = tv; + human_bsession(am->progress, tv); + machine_bsession(am->log, tv); } -static unsigned tap_grpix(struct tap_output *t) +/* --- @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) { - struct tvec_state *tv = t->tv; + unsigned f = 0; +#define f_any 1u - return (tv->grps[TVOUT_WIN] + - tv->grps[TVOUT_LOSE] + - tv->grps[TVOUT_SKIP]); + 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 tap_esession(struct tvec_output *o) +static int am_esession(struct tvec_output *o) { - struct tap_output *t = (struct tap_output *)o; - struct tvec_state *tv = t->tv; + 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; - if (tv->f&TVSF_ERROR) { - fputs("Bail out! " - "Errors found in input; tests may not have run correctly\n", - t->fmt.fp); - return (2); + 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(t->fmt.fp, "1..%u\n", tap_grpix(t)); - t->tv = 0; return (tv->all[TVOUT_LOSE] ? 1 : 0); + 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); } -static void tap_bgroup(struct tvec_output *o) +/* --- @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 tap_output *t = (struct tap_output *)o; - t->maxlen = register_maxnamelen(t->tv); + struct automake_output *am = (struct automake_output *)o; + + human_bgroup(am->progress); + machine_bgroup(am->log); } -static void tap_skipgroup(struct tvec_output *o, - const char *excuse, va_list *ap) +/* --- @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 tap_output *t = (struct tap_output *)o; + struct automake_output *am = (struct automake_output *)o; + struct tvec_state *tv = am->tv; - 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); + fprintf(am->trs, ":test-result: SKIP %s\n", tv->test->name); + human_skipgroup(am->progress, excuse, ap); + machine_skipgroup(am->log, excuse, ap); } -static void tap_egroup(struct tvec_output *o) +/* --- @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 tap_output *t = (struct tap_output *)o; - struct tvec_state *tv = t->tv; - unsigned - grpix = tap_grpix(t), - win = tv->curr[TVOUT_WIN], - lose = tv->curr[TVOUT_LOSE], - skip = tv->curr[TVOUT_SKIP]; + struct automake_output *am = (struct automake_output *)o; + struct tvec_state *tv = am->tv; - if (lose) { - fprintf(t->fmt.fp, "not ok %u - %s: FAILED %u/%u", - grpix, tv->test->name, lose, win + lose); - if (skip) fprintf(t->fmt.fp, " (skipped %u)", skip); - } else { - 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->fmt.fp); + 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); } -static void tap_btest(struct tvec_output *o) { ; } +/* --- @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 tap_outcome(struct tvec_output *o, const char *outcome, - const char *detail, va_list *ap) +static void am_btest(struct tvec_output *o) { - struct tap_output *t = (struct tap_output *)o; - struct tvec_state *tv = t->tv; + struct automake_output *am = (struct automake_output *)o; - 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'); + human_btest(am->progress); + machine_btest(am->log); } -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) - { tap_outcome(o, "FAILED", detail, ap); } +/* --- @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 tap_dumpreg(struct tvec_output *o, - unsigned disp, const union tvec_regval *rv, - const struct tvec_regdef *rd) +static void am_skip(struct tvec_output *o, const char *excuse, va_list *ap) { - struct tap_output *t = (struct tap_output *)o; - const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name); + struct automake_output *am = (struct automake_output *)o; - 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); - format_char(&t->fmt, '\n'); + human_skip(am->progress, excuse, ap); + machine_skip(am->log, excuse, ap); } -static void tap_etest(struct tvec_output *o, unsigned outcome) { ; } +static void am_fail(struct tvec_output *o, const char *detail, va_list *ap) +{ + struct automake_output *am = (struct automake_output *)o; -static void tap_bbench(struct tvec_output *o, - const char *ident, unsigned unit) - { ; } + 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 tap_ebench(struct tvec_output *o, - const char *ident, unsigned unit, - const struct bench_timing *tm) +static void am_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->tv; + struct automake_output *am = (struct automake_output *)o; - gprintf(&tap_printops, t, "%s: %s: ", tv->test->name, ident); - tvec_benchreport(&tap_printops, t, unit, tm); - format_char(&t->fmt, '\n'); + human_dumpreg(am->progress, disp, rv, rd); + machine_dumpreg(am->log, disp, rv, rd); } -static void tap_report(struct tap_output *t, - const struct gprintf_ops *gops, void *go, - const char *msg, va_list *ap) +/* --- @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 tvec_state *tv = t->tv; + struct automake_output *am = (struct automake_output *)o; - if (tv->infile) gprintf(gops, go, "%s:%u: ", tv->infile, tv->lno); - gprintf(gops, go, msg, ap); gops->putch(go, '\n'); + human_etest(am->progress, outcome); + machine_etest(am->log, outcome); } -static void tap_error(struct tvec_output *o, const char *msg, va_list *ap) +/* --- @am_report@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a + * @struct automake_output@ + * @unsigned level@ = message level (@TVLEV_...@) + * @const char *msg@, @va_list *ap@ = format string and + * arguments + * + * Returns: --- + * + * Use: Report a message to the user. + * + * The Automake driver passes the event on to its subordinates. + */ + +static void am_report(struct tvec_output *o, unsigned level, + const char *msg, va_list *ap) { - struct tap_output *t = (struct tap_output *)o; + struct automake_output *am = (struct automake_output *)o; - fputs("Bail out! ", t->fmt.fp); - tap_report(t, &file_printops, t->fmt.fp, msg, ap); + human_report(am->progress, level, msg, ap); + machine_report(am->log, level, msg, ap); } -static void tap_notice(struct tvec_output *o, const char *msg, va_list *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 tap_output *t = (struct tap_output *)o; + struct automake_output *am = (struct automake_output *)o; - tap_report(t, &tap_printops, t, msg, ap); + human_bbench(am->progress, desc, unit); + machine_bbench(am->progress, desc, unit); } -static void tap_destroy(struct tvec_output *o) +/* --- @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 tap_output *t = (struct tap_output *)o; + struct automake_output *am = (struct automake_output *)o; - destroy_fmt(&t->fmt, - t->fmt.fp == stdout || t->fmt.fp == stderr ? 0 : DFF_CLOSE); - xfree(t->outbuf); xfree(t); + human_ebench(am->progress, desc, unit, t); + machine_ebench(am->progress, desc, unit, 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_bbench, tap_ebench, - tap_error, tap_notice, - tap_destroy +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 }; -struct tvec_output *tvec_tapoutput(FILE *fp) +/* --- @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 tap_output *t; + struct automake_output *am; + unsigned f; - t = xmalloc(sizeof(*t)); t->_o.ops = &tap_ops; - init_fmt(&t->fmt, fp, "## "); - t->outbuf = 0; t->outsz = 0; - return (&t->_o); + 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 ----------------------------------------------------*/ -struct tvec_output *tvec_dfltout(FILE *fp) +/* --- @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. + */ + +struct tvec_output *tvec_dfltoutput(FILE *fp) { int ttyp = getenv_boolean("TVEC_TTY", -1); if (ttyp == -1) ttyp = isatty(fileno(fp)); - if (ttyp) return (tvec_humanoutput(fp)); - else return (tvec_tapoutput(fp)); + if (ttyp) return (tvec_humanoutput(fp, 0, 0)); + else return (tvec_machineoutput(fp)); } /*----- That's all, folks -------------------------------------------------*/