X-Git-Url: https://git.distorted.org.uk/~mdw/mLib/blobdiff_plain/ce896b0b69481d8ec941e1601fddd2df5143b259..5c0f2e080603967952db43eb7b12d44dd64f7169:/test/tvec-output.c?ds=sidebyside diff --git a/test/tvec-output.c b/test/tvec-output.c index a4a4415..46c3442 100644 --- a/test/tvec-output.c +++ b/test/tvec-output.c @@ -61,8 +61,8 @@ 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(); } } @@ -151,8 +151,8 @@ struct layout { * file. Return immediately on error. \ */ \ \ - size_t n = limit - base; \ - if (fwrite(base, 1, n, lyt->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 { \ @@ -170,8 +170,8 @@ struct layout { #define PUT_SAVED do { \ /* Output the saved trailing blank material in the buffer. */ \ \ - size_t n = lyt->w.len; \ - if (n && fwrite(lyt->w.buf, 1, n, lyt->fp) < n) return (-1); \ + size_t _n = lyt->w.len; \ + if (_n && fwrite(lyt->w.buf, 1, _n, lyt->fp) < _n) return (-1); \ } while (0) #define PUT_PFXINB do { \ @@ -189,6 +189,31 @@ struct layout { DPUTM(&lyt->w, lyt->pfxtail, lyt->pfxlim - lyt->pfxtail); \ } while (0) +/* --- @set_layout_prefix@ --- * + * + * Arguments: @struct layout *lyt@ = layout state + * @const char *prefix@ = new prefix string or null + * + * Returns: --- + * + * Use: Change the configured prefix string. The change takes effect + * at the start of the next line (or the current line if it's + * empty or only whitespace so far). + */ + +static void set_layout_prefix(struct layout *lyt, const char *prefix) +{ + const char *q, *l; + + if (!prefix || !*prefix) + lyt->prefix = lyt->pfxtail = lyt->pfxlim = 0; + else { + lyt->prefix = prefix; + l = lyt->pfxlim = prefix + strlen(prefix); + SPLIT_RANGE(q, prefix, l); lyt->pfxtail = q; + } +} + /* --- @init_layout@ --- * * * Arguments: @struct layout *lyt@ = layout state to initialize @@ -202,21 +227,10 @@ struct layout { static void init_layout(struct layout *lyt, FILE *fp, const char *prefix) { - const char *q, *l; - - /* Basics. */ lyt->fp = fp; lyt->f = LYTF_NEWL; dstr_create(&lyt->w); - - /* Prefix portions. */ - if (!prefix || !*prefix) - lyt->prefix = lyt->pfxtail = lyt->pfxlim = 0; - else { - lyt->prefix = prefix; - l = lyt->pfxlim = prefix + strlen(prefix); - SPLIT_RANGE(q, prefix, l); lyt->pfxtail = q; - } + set_layout_prefix(lyt, prefix); } /* --- @destroy_layout@ --- * @@ -402,38 +416,6 @@ static int layout_string(struct layout *lyt, 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 ---------------------------------------------*/ /* Attributes for colour output. This should be done better, but @terminfo@ @@ -467,6 +449,9 @@ static const struct tvec_outops ..._ops = { #define HA_PLAIN 0 /* nothing special: terminal defaults */ #define HA_LOC (HFG(CYAN)) /* filename or line number */ #define HA_LOCSEP (HFG(BLUE)) /* location separator `:' */ +#define HA_ERR (HFG(MAGENTA) | HAF_BOLD) /* error messages */ +#define HA_NOTE (HFG(YELLOW)) /* notices */ +#define HA_UNKLEV (HFG(WHITE) | HBG(RED) | HAF_BOLD) /* unknown level */ #define HA_UNSET (HFG(YELLOW)) /* register not set */ #define HA_FOUND (HFG(RED)) /* incorrect output value */ #define HA_EXPECT (HFG(GREEN)) /* what the value should have been */ @@ -474,7 +459,6 @@ static const struct tvec_outops ..._ops = { #define HA_LOSE (HFG(RED) | HAF_BOLD) /* reporting failure */ #define HA_XFAIL (HFG(BLUE) | HAF_BOLD) /* reporting expected failure */ #define HA_SKIP (HFG(YELLOW)) /* reporting a skipped test/group */ -#define HA_ERR (HFG(MAGENTA) | HAF_BOLD) /* reporting an error */ /* Scoreboard indicators. */ #define HSB_WIN '.' /* test passed */ @@ -656,62 +640,24 @@ static void show_progress(struct human_output *h) } } -/* --- @report_location@ --- * +/* --- @human_writech@, @human_write@, @human_writef@ --- * * - * Arguments: @struct human_output *h@ = output state - * @FILE *fp@ = stream to write the location on - * @const char *file@ = filename - * @unsigned lno@ = line number + * 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: 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. + * 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 void report_location(struct human_output *h, FILE *fp, - const char *file, unsigned lno) -{ - unsigned f = 0; -#define f_flush 1u - - /* We emit highlighting if @fp@ is our usual output stream, or the - * duplicate-errors flag is clear indicating that (we assume) they're - * secretly going to the same place anyway. If they're different streams, - * though, we have to be careful to keep the highlighting and the actual - * text synchronized. - */ - - if (!file) - /* nothing to do */; - else if (fp != h->lyt.fp && (h->f&HOF_DUPERR)) - fprintf(fp, "%s:%u: ", file, lno); - else { - if (fp != h->lyt.fp) f |= f_flush; - -#define FLUSH(fp) do if (f&f_flush) fflush(fp); while (0) - - setattr(h, HA_LOC); FLUSH(h->lyt.fp); - fputs(file, fp); FLUSH(fp); - setattr(h, HA_LOCSEP); FLUSH(h->lyt.fp); - fputc(':', fp); FLUSH(fp); - setattr(h, HA_LOC); FLUSH(h->lyt.fp); - fprintf(fp, "%u", lno); FLUSH(fp); - setattr(h, HA_LOCSEP); FLUSH(h->lyt.fp); - fputc(':', fp); FLUSH(fp); - setattr(h, HA_PLAIN); FLUSH(h->lyt.fp); - fputc(' ', fp); - -#undef FLUSH - } - -#undef f_flush -} - -/* Output layout. Pass everything along to the layout machinery above. */ - static int human_writech(void *go, int ch) { struct human_output *h = go; return (layout_char(&h->lyt, ch)); } @@ -727,19 +673,44 @@ static int human_nwritef(void *go, size_t maxsz, const char *p, ...) va_start(ap, p); n = gprintf_memputf(&h->outbuf, &h->outsz, maxsz, p, ap); va_end(ap); - return (layout_string(&h->lyt, h->outbuf, n)); + 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 }; -/* Output methods. */ +/* --- @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 human_report_unusual(struct human_output *h, - unsigned nxfail, unsigned nskip) +/* --- @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 report_unusual(struct human_output *h, + unsigned nxfail, unsigned nskip) { const char *sep = " ("; unsigned f = 0; @@ -764,6 +735,19 @@ static void human_report_unusual(struct human_output *h, #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; @@ -781,18 +765,18 @@ static int human_esession(struct tvec_output *o) fprintf(h->lyt.fp, " %s%u %s", !(all_skip || grps_skip) ? "all " : "", all_pass, all_pass == 1 ? "test" : "tests"); - human_report_unusual(h, all_xfail, all_skip); + report_unusual(h, all_xfail, all_skip); fprintf(h->lyt.fp, " in %u %s", grps_win, grps_win == 1 ? "group" : "groups"); - human_report_unusual(h, 0, grps_skip); + report_unusual(h, 0, grps_skip); } else { setattr(h, HA_LOSE); fputs("FAILED", h->lyt.fp); setattr(h, HA_PLAIN); fprintf(h->lyt.fp, " %u out of %u %s", all_lose, all_run, all_run == 1 ? "test" : "tests"); - human_report_unusual(h, all_xfail, all_skip); + 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"); - human_report_unusual(h, 0, grps_skip); + report_unusual(h, 0, grps_skip); } fputc('\n', h->lyt.fp); @@ -804,6 +788,20 @@ static int human_esession(struct tvec_output *o) 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; @@ -812,23 +810,49 @@ 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; - putc(' ', h->lyt.fp); - setattr(h, HA_SKIP); fputs("skipped", h->lyt.fp); setattr(h, HA_PLAIN); - } else { - fprintf(h->lyt.fp, "%s: ", h->tv->test->name); - setattr(h, HA_SKIP); fputs("skipped", h->lyt.fp); setattr(h, HA_PLAIN); + if (!(h->f&HOF_TTY)) + fprintf(h->lyt.fp, "%s ", h->tv->test->name); + else { + show_progress(h); h->f &= ~HOF_PROGRESS; + if (h->scoreboard.len) putc(' ', h->lyt.fp); } + setattr(h, HA_SKIP); fputs("skipped", h->lyt.fp); setattr(h, HA_PLAIN); if (excuse) { fputs(": ", h->lyt.fp); vfprintf(h->lyt.fp, excuse, *ap); } fputc('\n', h->lyt.fp); } +/* --- @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; @@ -843,46 +867,140 @@ static void human_egroup(struct tvec_output *o) if (lose) { fprintf(h->lyt.fp, " %u/%u ", lose, run); setattr(h, HA_LOSE); fputs("FAILED", h->lyt.fp); setattr(h, HA_PLAIN); - human_report_unusual(h, xfail, skip); + report_unusual(h, xfail, skip); } else { fputc(' ', h->lyt.fp); setattr(h, HA_WIN); fputs("ok", h->lyt.fp); setattr(h, HA_PLAIN); - human_report_unusual(h, xfail, skip); + report_unusual(h, xfail, skip); } 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) { - struct human_output *h = (struct human_output *)o; - struct tvec_state *tv = h->tv; + unsigned f = 0; +#define f_flush 1u - clear_progress(h); - report_location(h, h->lyt.fp, tv->infile, tv->test_lno); - fprintf(h->lyt.fp, "`%s' ", tv->test->name); - setattr(h, HA_SKIP); fputs("skipped", h->lyt.fp); setattr(h, HA_PLAIN); - if (excuse) { fputs(": ", h->lyt.fp); vfprintf(h->lyt.fp, excuse, *ap); } - fputc('\n', h->lyt.fp); + /* We emit highlighting if @fp@ is our usual output stream, or the + * duplicate-errors flag is clear indicating that (we assume) they're + * secretly going to the same place anyway. If they're different streams, + * though, we have to be careful to keep the highlighting and the actual + * text synchronized. + */ + + if (!file) + /* nothing to do */; + else if (fp != h->lyt.fp && (h->f&HOF_DUPERR)) + fprintf(fp, "%s:%u: ", file, lno); + else { + if (fp != h->lyt.fp) f |= f_flush; + +#define FLUSH(fp) do if (f&f_flush) fflush(fp); while (0) + + setattr(h, HA_LOC); FLUSH(h->lyt.fp); + fputs(file, fp); FLUSH(fp); + setattr(h, HA_LOCSEP); FLUSH(h->lyt.fp); + fputc(':', fp); FLUSH(fp); + setattr(h, HA_LOC); FLUSH(h->lyt.fp); + fprintf(fp, "%u", lno); FLUSH(fp); + setattr(h, HA_LOCSEP); FLUSH(h->lyt.fp); + fputc(':', fp); FLUSH(fp); + setattr(h, HA_PLAIN); FLUSH(h->lyt.fp); + fputc(' ', fp); + +#undef FLUSH + } + +#undef f_flush } -static void human_fail(struct tvec_output *o, - const char *detail, va_list *ap) +/* --- @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->lyt.fp, tv->infile, tv->test_lno); + human_report_location(h, h->lyt.fp, tv->infile, tv->test_lno); fprintf(h->lyt.fp, "`%s' ", tv->test->name); - setattr(h, HA_LOSE); fputs("FAILED", h->lyt.fp); setattr(h, HA_PLAIN); + setattr(h, attr); fputs(outcome, h->lyt.fp); setattr(h, HA_PLAIN); if (detail) { fputs(": ", h->lyt.fp); vfprintf(h->lyt.fp, detail, *ap); } fputc('\n', h->lyt.fp); } +static void human_skip(struct tvec_output *o, + const char *excuse, va_list *ap) + { human_outcome(o, HA_SKIP, "skipped", excuse, ap); } +static void human_fail(struct tvec_output *o, + const char *detail, va_list *ap) + { human_outcome(o, HA_LOSE, "FAILED", detail, ap); } + +/* --- @human_dumpreg@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct + * human_output@ + * @unsigned disp@ = register disposition + * @const union tvec_regval *rv@ = register value + * @const struct tvec_regdef *rd@ = register definition + * + * Returns: --- + * + * Use: Dump a register. + * + * The human driver applies highlighting to mismatching output + * registers, but otherwise delegates to the register type + * handler and the layout machinery. + */ + static void human_dumpreg(struct tvec_output *o, unsigned disp, const union tvec_regval *rv, const struct tvec_regdef *rd) @@ -903,6 +1021,20 @@ static void human_dumpreg(struct tvec_output *o, setattr(h, HA_PLAIN); layout_char(&h->lyt, '\n'); } +/* --- @human_etest@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct + * human_output@ + * @unsigned outcome@ = the test outcome + * + * Returns: --- + * + * Use: Report that a test has finished. + * + * The human driver reactivates the progress display, if + * necessary, and adds a new character for the completed test. + */ + static void human_etest(struct tvec_output *o, unsigned outcome) { struct human_output *h = (struct human_output *)o; @@ -922,6 +1054,21 @@ static void human_etest(struct tvec_output *o, unsigned outcome) } } +/* --- @human_bbench@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct + * human_output@ + * @const char *ident@ = identifying register values + * @unsigned unit@ = measurement unit (@TVBU_...@) + * + * Returns: --- + * + * Use: Report that a benchmark has started. + * + * The human driver just prints the start of the benchmark + * report. + */ + static void human_bbench(struct tvec_output *o, const char *ident, unsigned unit) { @@ -929,40 +1076,107 @@ static void human_bbench(struct tvec_output *o, struct tvec_state *tv = h->tv; clear_progress(h); - fprintf(h->lyt.fp, "%s: %s: ", tv->test->name, ident); fflush(h->lyt.fp); + gprintf(&human_printops, h, "%s %s: ", tv->test->name, ident); + if (h->f&HOF_TTY) fflush(h->lyt.fp); } +/* --- @human_ebench@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct + * human_output@ + * @const char *ident@ = identifying register values + * @unsigned unit@ = measurement unit (@TVBU_...@) + * @const struct bench_timing *tm@ = measurement + * + * Returns: --- + * + * Use: Report a benchmark's results + * + * The human driver just delegates to the default benchmark + * reporting, via the layout machinery. + */ + static void human_ebench(struct tvec_output *o, const char *ident, unsigned unit, const struct bench_timing *tm) { struct human_output *h = (struct human_output *)o; - tvec_benchreport(&human_printops, h->lyt.fp, unit, tm); - fputc('\n', h->lyt.fp); + tvec_benchreport(&human_printops, h, unit, 0, tm); + layout_char(&h->lyt, '\n'); } +/* --- @human_report@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct + * human_output@ + * @unsigned level@ = message level (@TVLEV_...@) + * @const char *msg@, @va_list *ap@ = format string and + * arguments + * + * Returns: --- + * + * Use: Report a message to the user. + * + * The human driver arranges to show the message on @stderr@ as + * well as the usual output, with a certain amount of + * intelligence in case they're both actually the same device. + */ + static void human_report(struct tvec_output *o, unsigned level, const char *msg, va_list *ap) { struct human_output *h = (struct human_output *)o; struct tvec_state *tv = h->tv; + const char *levstr; unsigned levattr; dstr d = DSTR_INIT; + unsigned f = 0; +#define f_flush 1u +#define f_progress 2u dstr_vputf(&d, msg, ap); dstr_putc(&d, '\n'); - clear_progress(h); fflush(h->lyt.fp); + switch (level) { +#define CASE(tag, name, val) \ + case TVLEV_##tag: levstr = name; levattr = HA_##tag; break; + TVEC_LEVELS(CASE) + default: levstr = "??"; levattr = HA_UNKLEV; break; + } + + if (h->lyt.fp != stderr && !(h->f&HOF_DUPERR)) f |= f_flush; + +#define FLUSH do if (f&f_flush) fflush(h->lyt.fp); while (0) + + if (h->f^HOF_PROGRESS) + { clear_progress(h); fflush(h->lyt.fp); f |= f_progress; } fprintf(stderr, "%s: ", QUIS); - report_location(h, stderr, tv->infile, tv->lno); - fwrite(d.buf, 1, d.len, stderr); + human_report_location(h, stderr, tv->infile, tv->lno); + setattr(h, levattr); FLUSH; fputs(levstr, stderr); setattr(h, 0); FLUSH; + fputs(": ", stderr); fwrite(d.buf, 1, d.len, stderr); + +#undef FLUSH if (h->f&HOF_DUPERR) { - report_location(h, h->lyt.fp, tv->infile, tv->lno); + human_report_location(h, h->lyt.fp, tv->infile, tv->lno); + fprintf(h->lyt.fp, "%s: ", levstr); fwrite(d.buf, 1, d.len, h->lyt.fp); } - show_progress(h); + if (f&f_progress) show_progress(h); + +#undef f_flush +#undef f_progress } +/* --- @human_destroy@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct + * human_output@ + * + * Returns: --- + * + * Use: Release the resources held by the output driver. + */ + static void human_destroy(struct tvec_output *o) { struct human_output *h = (struct human_output *)o; @@ -982,6 +1196,20 @@ static const struct tvec_outops human_ops = { human_destroy }; +/* --- @tvec_humanoutput@ --- * + * + * Arguments: @FILE *fp@ = output file to write on + * + * Returns: An output formatter. + * + * Use: Return an output formatter which writes on @fp@ with the + * expectation that a human will be watching and interpreting + * the output. If @fp@ denotes a terminal, the display shows a + * `scoreboard' indicating the outcome of each test case + * attempted, and may in addition use colour and other + * highlighting. + */ + struct tvec_output *tvec_humanoutput(FILE *fp) { struct human_output *h; @@ -1016,22 +1244,574 @@ struct tvec_output *tvec_humanoutput(FILE *fp) return (&h->_o); } +/*----- Machine-readable output -------------------------------------------*/ + +struct machine_output { + struct tvec_output _o; /* output base class */ + struct tvec_state *tv; /* stashed testing state */ + 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 +}; + +/* --- @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->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)) 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("BGROUP ", 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; + + 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) + { 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 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 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); + 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 TAP 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_bbench@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct + * machine_output@ + * @const char *ident@ = identifying register values + * @unsigned unit@ = measurement unit (@TVBU_...@) + * + * Returns: --- + * + * Use: Report that a benchmark has started. + * + * The TAP driver does nothing here. All of the reporting + * happens in @machine_ebench@. + */ + +static void machine_bbench(struct tvec_output *o, + const char *ident, unsigned unit) + { ; } + +/* --- @machine_ebench@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct + * machine_output@ + * @const char *ident@ = identifying register values + * @unsigned unit@ = measurement unit (@TVBU_...@) + * @const struct bench_timing *tm@ = measurement + * + * Returns: --- + * + * Use: Report a benchmark's results + * + * The TAP driver just delegates to the default benchmark + * reporting, via the layout machinery so that the result is + * printed as a comment. + */ + +static void machine_ebench(struct tvec_output *o, + const char *ident, unsigned unit, + const struct bench_timing *tm) +{ + 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); + fprintf(m->fp, " %s: ", ident); + tvec_benchreport(&file_printops, m->fp, unit, TVSF_RAW, tm); + putc('\n', m->fp); 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. + * + * 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 machine_report(struct tvec_output *o, unsigned level, + const char *msg, va_list *ap) +{ + struct machine_output *m = (struct machine_output *)o; + const char *p; + + for (p = tvec_strlevel(level); *p; p++) putc(TOUPPER(*p), m->fp); + putc(' ', m->fp); + putc('"', m->fp); + vgprintf(&machine_printops, m, msg, ap); + putc('"', m->fp); + putc('\n', m->fp); +} + +/* --- @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); + xfree(m->outbuf); xfree(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_bbench, machine_ebench, + machine_report, + 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; + + m = xmalloc(sizeof(*m)); m->_o.ops = &machine_ops; + 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; - struct tvec_state *tv; - struct layout lyt; - unsigned last; - char *outbuf; size_t outsz; - int maxlen; + struct tvec_output _o; /* output base class */ + struct tvec_state *tv; /* stashed testing state */ + struct layout lyt; /* output layout */ + char *outbuf; size_t outsz; /* buffer for formatted output */ + unsigned grpix, testix; /* group and test indices */ + unsigned previx; /* previously reported test index */ + int maxlen; /* longest register name */ }; +/* --- @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; return (layout_char(&t->lyt, 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; return (layout_string(&t->lyt, p, 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, ...) { @@ -1042,38 +1822,50 @@ static int tap_nwritef(void *go, size_t maxsz, const char *p, ...) va_start(ap, p); n = gprintf_memputf(&t->outbuf, &t->outsz, maxsz, p, ap); va_end(ap); - return (layout_string(&t->lyt, t->outbuf, n)); + 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->last = 0; + t->tv = tv; t->grpix = 0; fputs("TAP version 13\n", t->lyt.fp); /* but secretly 14 really */ } -static unsigned tap_grpix(struct tap_output *t) -{ - struct tvec_state *tv = t->tv; - unsigned i, n; - - for (n = 0, i = 0; i < TVOUT_LIMIT; i++) n += tv->grps[i]; - return (n); -} - -static unsigned tap_testix(struct tap_output *t) -{ - struct tvec_state *tv = t->tv; - unsigned i, n; - - for (n = 0, i = 0; i < TVOUT_LIMIT; i++) n += tv->curr[i]; - if (tv->f&TVSF_OPEN) n++; - return (n); -} +/* --- @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) { @@ -1087,42 +1879,120 @@ static int tap_esession(struct tvec_output *o) return (2); } - fprintf(t->lyt.fp, "1..%u\n", tap_grpix(t)); + 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", tap_testix(t)); - fprintf(t->lyt.fp, "ok %u %s # SKIP", tap_grpix(t), t->tv->test->name); + 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", tap_testix(t)); + fprintf(t->lyt.fp, " 1..%u\n", t->testix); fprintf(t->lyt.fp, "%s %u - %s\n", tv->curr[TVOUT_LOSE] ? "not ok" : "ok", - tap_grpix(t), tv->test->name); + t->grpix, tv->test->name); } -static void tap_btest(struct tvec_output *o) { ; } +/* --- @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, @@ -1130,14 +2000,14 @@ static void tap_outcome(struct tvec_output *o, { struct tap_output *t = (struct tap_output *)o; struct tvec_state *tv = t->tv; - unsigned ix = tap_testix(t); fprintf(t->lyt.fp, " %s %u - %s:%u%s", - ix == t->last ? "##" : head, ix, tv->infile, tv->test_lno, tail); + 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->last = ix; + t->previx = t->testix; } static void tap_skip(struct tvec_output *o, const char *excuse, va_list *ap) @@ -1145,6 +2015,24 @@ static void tap_skip(struct tvec_output *o, const char *excuse, va_list *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) @@ -1152,6 +2040,7 @@ static void tap_dumpreg(struct tvec_output *o, struct tap_output *t = (struct tap_output *)o; const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name); + 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, "#"); @@ -1159,6 +2048,20 @@ static void tap_dumpreg(struct tvec_output *o, 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) { @@ -1171,10 +2074,42 @@ static void tap_etest(struct tvec_output *o, unsigned outcome) } } +/* --- @tap_bbench@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct + * tap_output@ + * @const char *ident@ = identifying register values + * @unsigned unit@ = measurement unit (@TVBU_...@) + * + * Returns: --- + * + * Use: Report that a benchmark has started. + * + * The TAP driver does nothing here. All of the reporting + * happens in @tap_ebench@. + */ + static void tap_bbench(struct tvec_output *o, const char *ident, unsigned unit) { ; } +/* --- @tap_ebench@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct + * tap_output@ + * @const char *ident@ = identifying register values + * @unsigned unit@ = measurement unit (@TVBU_...@) + * @const struct bench_timing *tm@ = measurement + * + * Returns: --- + * + * Use: Report a benchmark's results + * + * The TAP driver just delegates to the default benchmark + * reporting, via the layout machinery so that the result is + * printed as a comment. + */ + static void tap_ebench(struct tvec_output *o, const char *ident, unsigned unit, const struct bench_timing *tm) @@ -1182,28 +2117,56 @@ static void tap_ebench(struct tvec_output *o, struct tap_output *t = (struct tap_output *)o; struct tvec_state *tv = t->tv; - gprintf(&tap_printops, t, "%s: %s: ", tv->test->name, ident); - tvec_benchreport(&tap_printops, t, unit, tm); + set_layout_prefix(&t->lyt, " ## "); + gprintf(&tap_printops, t, "%s %s: ", tv->test->name, ident); + tvec_benchreport(&tap_printops, t, unit, 0, tm); layout_char(&t->lyt, '\n'); } +/* --- @tap_report@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct + * tap_output@ + * @unsigned level@ = message level (@TVLEV_...@) + * @const char *msg@, @va_list *ap@ = format string and + * arguments + * + * Returns: --- + * + * Use: Report a message to the user. + * + * Messages are reported as comments, so that they can be + * accumulated by the reader. An error will cause a later + * bailout or, if we crash before then, a missing plan line, + * either of which will cause the reader to report a serious + * problem. + */ + static void tap_report(struct tvec_output *o, unsigned level, const char *msg, va_list *ap) { struct tap_output *t = (struct tap_output *)o; struct tvec_state *tv = t->tv; - const struct gprintf_ops *gops; void *go; - if (level >= TVLEV_ERR) { - fputs("Bail out! ", t->lyt.fp); - gops = &file_printops; go = t->lyt.fp; - } else { - gops = &tap_printops; go = t; - } - if (tv->infile) gprintf(gops, go, "%s:%u: ", tv->infile, tv->lno); - gprintf(gops, go, msg, ap); gops->putch(go, '\n'); + if (tv->test) set_layout_prefix(&t->lyt, " ## "); + else set_layout_prefix(&t->lyt, "## "); + + if (tv->infile) gprintf(&tap_printops, t, "%s:%u: ", tv->infile, tv->lno); + gprintf(&tap_printops, t, "%s: ", tvec_strlevel(level)); + vgprintf(&tap_printops, t, msg, ap); + layout_char(&t->lyt, '\n'); } +/* --- @tap_destroy@ --- * + * + * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct + * tap_output@ + * + * Returns: --- + * + * Use: Release the resources held by the output driver. + */ + static void tap_destroy(struct tvec_output *o) { struct tap_output *t = (struct tap_output *)o; @@ -1222,18 +2185,53 @@ static const struct tvec_outops tap_ops = { tap_destroy }; +/* --- @tvec_tapoutput@ --- * + * + * Arguments: @FILE *fp@ = output file to write on + * + * Returns: An output formatter. + * + * Use: Return an output formatter which writes on @fp@ in `TAP' + * (`Test Anything Protocol') format. + * + * TAP comes from the Perl community, but has spread rather + * further. This driver produces TAP version 14, but pretends + * to be version 13. The driver produces a TAP `test point' -- + * i.e., a result reported as `ok' or `not ok' -- for each input + * test group. Failure reports and register dumps are produced + * as diagnostic messages before the final group result. (TAP + * permits structuerd YAML data after the test-point result, + * which could be used to report details, but (a) postponing the + * details until after the report is inconvenient, and (b) there + * is no standardization for the YAML anyway, so in practice + * it's no more useful than the unstructured diagnostics. + */ + struct tvec_output *tvec_tapoutput(FILE *fp) { struct tap_output *t; t = xmalloc(sizeof(*t)); t->_o.ops = &tap_ops; - init_layout(&t->lyt, fp, " ## "); + init_layout(&t->lyt, fp, 0); t->outbuf = 0; t->outsz = 0; return (&t->_o); } /*----- Default output ----------------------------------------------------*/ +/* --- @tvec_dfltoutput@ --- * + * + * Arguments: @FILE *fp@ = output file to write on + * + * Returns: An output formatter. + * + * Use: Selects and instantiates an output formatter suitable for + * writing on @fp@. The policy is subject to change, but + * currently the `human' output format is selected if @fp@ is + * interactive (i.e., if @isatty(fileno(fp))@ is true), and + * otherwise the `tap' format is used. + */ + struct tvec_output *tvec_dfltout(FILE *fp) { int ttyp = getenv_boolean("TVEC_TTY", -1);