return (maxlen);
}
-/*----- Output formatting -------------------------------------------------*/
+/*----- 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 */
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 { \
* 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_PFXINB do { \
* 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;
+ lyt->prefix = prefix;
+ l = lyt->pfxlim = prefix + strlen(prefix);
+ SPLIT_RANGE(q, prefix, l); lyt->pfxtail = q;
}
}
-/* --- @destroy_fmt@ --- *
+/* --- @init_layout@ --- *
*
- * Arguments: @struct format *fmt@ = formatting state
- * @unsigned f@ = flags (@DFF_...@)
+ * Arguments: @struct layout *lyt@ = layout state to initialize
+ * @FILE *fp@ = output file
+ * @const char *prefix@ = prefix string (or null if empty)
*
* Returns: ---
*
- * Use: Releases a formatting state and the resources it holds.
- * Close the file if @DFF_CLOSE@ is set in @f@; otherwise leave
+ * 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);
+ set_layout_prefix(lyt, prefix);
+}
+
+/* --- @destroy_layout@ --- *
+ *
+ * Arguments: @struct layout *lyt@ = layout state
+ * @unsigned f@ = flags (@DLF_...@)
+ *
+ * Returns: ---
+ *
+ * Use: Releases a layout state and the resources it holds.
+ * Close the file if @DLF_CLOSE@ is set in @f@; otherwise leave
* it open (in case it's @stderr@ or something).
*/
-#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);
}
-/* --- @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.
* 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
*
* 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;
* 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
* 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_PREFIX; 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_NONBLANK; DRESET(&lyt->w);
}
SAVE_TAIL;
return (0);
* to output.
*/
if (r > p) {
- if (fmt->f&FMTF_NEWL) PUT_PREFIX;
+ if (lyt->f&LYTF_NEWL) PUT_PREFIX;
PUT_SAVED; PUT_NONBLANK;
- } else if (fmt->f&FMTF_NEWL)
+ } else if (lyt->f&LYTF_NEWL)
PUT_PFXINB;
- PUT_NEWLINE; DRESET(&fmt->w);
+ PUT_NEWLINE; DRESET(&lyt->w);
SPLIT_SEGMENT;
/* Main loop over whole segments with trailing newlines. For each one, we
* 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
*/
/*----- Human-readable output ---------------------------------------------*/
-#define HAF_FGMASK 0x0f
-#define HAF_FGSHIFT 0
-#define HAF_BGMASK 0xf0
-#define HAF_BGSHIFT 4
-#define HAF_FG 256u
-#define HAF_BG 512u
-#define HAF_BOLD 1024u
-#define HCOL_BLACK 0u
+/* Attributes for colour output. This should be done better, but @terminfo@
+ * is a disaster.
+ *
+ * An attribute byte holds a foreground colour in the low nibble, a
+ * background colour in the next nibble, and some flags in the next few
+ * bits. A colour is expressed in classic 1-bit-per-channel style, with red,
+ * green, and blue in bits 0, 1, and 2, and a `bright' flag in bit 3.
+ */
+#define HAF_FGMASK 0x0f /* foreground colour mask */
+#define HAF_FGSHIFT 0 /* foreground colour shift */
+#define HAF_BGMASK 0xf0 /* background colour mask */
+#define HAF_BGSHIFT 4 /* background colour shift */
+#define HAF_FG 256u /* set foreground? */
+#define HAF_BG 512u /* set background? */
+#define HAF_BOLD 1024u /* set bold? */
+#define HCOL_BLACK 0u /* colour codes... */
#define HCOL_RED 1u
#define HCOL_GREEN 2u
#define HCOL_YELLOW 3u
#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)
+#define HCF_BRIGHT 8u /* bright colour flag */
+#define HFG(col) (HAF_FG | (HCOL_##col) << HAF_FGSHIFT) /* set foreground */
+#define HBG(col) (HAF_BG | (HCOL_##col) << HAF_BGSHIFT) /* set background */
+
+/* Predefined attributes. */
+#define HA_PLAIN 0 /* nothing special: terminal defaults */
+#define HA_LOC (HFG(CYAN)) /* filename or line number */
+#define HA_LOCSEP (HFG(BLUE)) /* location separator `:' */
+#define HA_ERR (HFG(MAGENTA) | HAF_BOLD) /* error messages */
+#define HA_NOTE (HFG(YELLOW)) /* notices */
+#define HA_UNKLEV (HFG(WHITE) | HBG(RED) | HAF_BOLD) /* unknown level */
+#define HA_UNSET (HFG(YELLOW)) /* register not set */
+#define HA_FOUND (HFG(RED)) /* incorrect output value */
+#define HA_EXPECT (HFG(GREEN)) /* what the value should have been */
+#define HA_WIN (HFG(GREEN)) /* reporting success */
+#define HA_LOSE (HFG(RED) | HAF_BOLD) /* reporting failure */
+#define HA_XFAIL (HFG(BLUE) | HAF_BOLD) /* reporting expected failure */
+#define HA_SKIP (HFG(YELLOW)) /* reporting a skipped test/group */
+
+/* Scoreboard indicators. */
+#define HSB_WIN '.' /* test passed */
+#define HSB_LOSE 'x' /* test failed */
+#define HSB_XFAIL 'o' /* test failed expectedly */
+#define HSB_SKIP '_' /* test wasn't run */
struct human_output {
- struct tvec_output _o;
- 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 */
+ struct layout lyt; /* output layout */
+ char *outbuf; size_t outsz; /* buffer for formatted output */
+ dstr scoreboard; /* history of test group results */
+ unsigned attr; /* current terminal attributes */
+ int maxlen; /* longest register name */
+ unsigned f; /* flags */
+#define HOF_TTY 1u /* writing to terminal */
+#define HOF_DUPERR 2u /* duplicate errors to stderr */
+#define HOF_COLOUR 4u /* print in angry fruit salad */
+#define HOF_PROGRESS 8u /* progress display is active */
};
+/* --- @set_colour@ --- *
+ *
+ * Arguments: @FILE *fp@ = output stream to write on
+ * @int *sep_inout@ = where to maintain separator
+ * @const char *norm@ = prefix for normal colour
+ * @const char *bright@ = prefix for bright colour
+ * @unsigned colour@ = four bit colour code
+ *
+ * Returns: ---
+ *
+ * Use: Write to the output stream @fp@, the current character at
+ * @*sep_inout@, if that's not zero, followed by either @norm@
+ * or @bright@, according to whether the @HCF_BRIGHT@ flag is
+ * set in @colour@, followed by the plain colour code from
+ * @colour@; finally, update @*sep_inout@ to be a `%|;|%'.
+ *
+ * This is an internal subroutine for @setattr@ below.
+ */
+
static void set_colour(FILE *fp, int *sep_inout,
const char *norm, const char *bright,
unsigned colour)
*sep_inout = ';';
}
+/* --- @setattr@ --- *
+ *
+ * Arguments: @struct human_output *h@ = output state
+ * @unsigned attr@ = attribute code to set
+ *
+ * Returns: ---
+ *
+ * Use: Send a control sequence to the output stream so that
+ * subsequent text is printed with the given attributes.
+ *
+ * Some effort is taken to avoid unnecessary control sequences.
+ * In particular, if @attr@ matches the current terminal
+ * settings already, then nothing is written.
+ */
+
static void setattr(struct human_output *h, unsigned attr)
{
unsigned diff = h->attr ^ attr;
int sep = 0;
+ /* If there's nothing to do, we might as well stop now. */
if (!diff || !(h->f&HOF_COLOUR)) return;
- fputs("\x1b[", h->fmt.fp);
+ /* Start on the control command. */
+ fputs("\x1b[", h->lyt.fp);
+
+ /* Change the boldness if necessary. */
if (diff&HAF_BOLD) {
- if (attr&HAF_BOLD) putc('1', h->fmt.fp);
- else { putc('0', h->fmt.fp); diff = h->attr; }
+ if (attr&HAF_BOLD) putc('1', h->lyt.fp);
+ else { putc('0', h->lyt.fp); diff = h->attr; }
sep = ';';
}
+
+ /* Change the foreground colour if necessary. */
if (diff&(HAF_FG | HAF_FGMASK)) {
if (attr&HAF_FG)
- set_colour(h->fmt.fp, &sep, "3", "9",
+ set_colour(h->lyt.fp, &sep, "3", "9",
(attr&HAF_FGMASK) >> HAF_FGSHIFT);
- else
- { if (sep) putc(sep, h->fmt.fp); fputs("39", h->fmt.fp); sep = ';'; }
+ else {
+ if (sep) putc(sep, h->lyt.fp);
+ fputs("39", h->lyt.fp); sep = ';';
+ }
}
+
+ /* Change the background colour if necessary. */
if (diff&(HAF_BG | HAF_BGMASK)) {
if (attr&HAF_BG)
- set_colour(h->fmt.fp, &sep, "4", "10",
+ set_colour(h->lyt.fp, &sep, "4", "10",
(attr&HAF_BGMASK) >> HAF_BGSHIFT);
- else
- { if (sep) putc(sep, h->fmt.fp); fputs("49", h->fmt.fp); sep = ';'; }
+ else {
+ if (sep) putc(sep, h->lyt.fp);
+ fputs("49", h->lyt.fp); sep = ';';
+ }
}
- putc('m', h->fmt.fp); h->attr = attr;
-
-#undef f_any
+ /* Terminate the control command and save the new attributes. */
+ putc('m', h->lyt.fp); h->attr = attr;
}
+/* --- @clear_progress@ --- *
+ *
+ * Arguments: @struct human_output *h@ = output state
+ *
+ * Returns: ---
+ *
+ * Use: Remove the progress display from the terminal.
+ *
+ * If the progress display isn't active then do nothing.
+ */
+
static void clear_progress(struct human_output *h)
{
size_t i, n;
if (h->f&HOF_PROGRESS) {
n = strlen(h->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;
+ case HSB_LOSE: setattr(h, HA_LOSE); break;
+ case HSB_SKIP: setattr(h, HA_SKIP); break;
+ case HSB_XFAIL: setattr(h, HA_XFAIL); break;
+ default: setattr(h, HA_PLAIN); break;
}
- putc(ch, h->fmt.fp); setattr(h, 0);
+ putc(ch, h->lyt.fp); setattr(h, HA_PLAIN);
}
+/* --- @show_progress@ --- *
+ *
+ * Arguments: @struct human_output *h@ = output state
+ *
+ * Returns: ---
+ *
+ * Use: Show the progress display, with the record of outcomes for
+ * the current test group.
+ *
+ * If the progress display is already active, or the output
+ * stream is not interactive, then nothing happens.
+ */
+
static void show_progress(struct human_output *h)
{
struct tvec_state *tv = h->tv;
const char *p, *l;
if (tv->test && (h->f&HOF_TTY) && !(h->f&HOF_PROGRESS)) {
- fprintf(h->fmt.fp, "%s: ", tv->test->name);
+ fprintf(h->lyt.fp, "%s: ", tv->test->name);
if (!(h->f&HOF_COLOUR))
- dstr_write(&h->scoreboard, h->fmt.fp);
+ dstr_write(&h->scoreboard, h->lyt.fp);
else for (p = h->scoreboard.buf, l = p + h->scoreboard.len; p < l; p++)
write_scoreboard_char(h, *p);
- fflush(h->fmt.fp); h->f |= HOF_PROGRESS;
+ fflush(h->lyt.fp); h->f |= HOF_PROGRESS;
}
}
-static void report_location(struct human_output *h, FILE *fp,
- const char *file, unsigned lno)
-{
- unsigned f = 0;
-#define f_flush 1u
-
-#define FLUSH(fp) do if (f&f_flush) fflush(fp); while (0)
-
- if (fp != h->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, ...)
{
va_start(ap, p);
n = gprintf_memputf(&h->outbuf, &h->outsz, maxsz, p, ap);
va_end(ap);
- return (format_string(&h->fmt, h->outbuf, n));
+ return (layout_string(&h->lyt, h->outbuf, 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)
+/* --- @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)
{
- if (n) {
- fprintf(h->fmt.fp, " (%u ", n);
- setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0);
- fputc(')', h->fmt.fp);
+ const char *sep = " (";
+ unsigned f = 0;
+#define f_any 1u
+
+ if (nxfail) {
+ fprintf(h->lyt.fp, "%s%u ", sep, nxfail);
+ setattr(h, HA_XFAIL);
+ fprintf(h->lyt.fp, "expected %s", nxfail == 1 ? "failure" : "failures");
+ setattr(h, HA_PLAIN);
+ sep = ", "; f |= f_any;
+ }
+
+ if (nskip) {
+ fprintf(h->lyt.fp, "%s%u ", sep, nskip);
+ setattr(h, HA_SKIP); fputs("skipped", h->lyt.fp); setattr(h, HA_PLAIN);
+ sep = ", "; f |= f_any;
}
+
+ if (f&f_any) fputc(')', h->lyt.fp);
+
+#undef f_any
}
+/* --- @human_esession@ --- *
+ *
+ * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct
+ * human_output@
+ *
+ * Returns: Suggested exit code.
+ *
+ * Use: End a test session.
+ *
+ * The human driver prints a final summary of the rest results
+ * and returns a suitable exit code.
+ */
+
static int human_esession(struct tvec_output *o)
{
struct human_output *h = (struct human_output *)o;
struct tvec_state *tv = h->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, HA_WIN); fputs("PASSED", h->lyt.fp); setattr(h, HA_PLAIN);
+ fprintf(h->lyt.fp, " %s%u %s",
!(all_skip || grps_skip) ? "all " : "",
- all_win, all_win == 1 ? "test" : "tests");
- report_skipped(h, all_skip);
- fprintf(h->fmt.fp, " in %u %s",
+ all_pass, all_pass == 1 ? "test" : "tests");
+ report_unusual(h, all_xfail, all_skip);
+ fprintf(h->lyt.fp, " in %u %s",
grps_win, grps_win == 1 ? "group" : "groups");
- report_skipped(h, grps_skip);
+ report_unusual(h, 0, grps_skip);
} else {
- setattr(h, HA_LOSE); fputs("FAILED", h->fmt.fp); setattr(h, 0);
- fprintf(h->fmt.fp, " %u out of %u %s",
+ setattr(h, HA_LOSE); fputs("FAILED", h->lyt.fp); setattr(h, HA_PLAIN);
+ fprintf(h->lyt.fp, " %u out of %u %s",
all_lose, all_run, all_run == 1 ? "test" : "tests");
- report_skipped(h, all_skip);
- fprintf(h->fmt.fp, " in %u out of %u %s",
+ report_unusual(h, all_xfail, all_skip);
+ fprintf(h->lyt.fp, " in %u out of %u %s",
grps_lose, grps_run, grps_run == 1 ? "group" : "groups");
- report_skipped(h, grps_skip);
+ report_unusual(h, 0, grps_skip);
}
- fputc('\n', h->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, HA_ERR); fputs("ERRORS", h->lyt.fp); setattr(h, HA_PLAIN);
+ fputs(" found in input; tests may not have run correctly\n", h->lyt.fp);
}
- 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;
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->fmt.fp);
- 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&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);
}
- if (excuse) { fputs(": ", h->fmt.fp); vfprintf(h->fmt.fp, excuse, *ap); }
- fputc('\n', h->fmt.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;
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);
+ 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, HA_LOSE); fputs("FAILED", h->lyt.fp); setattr(h, HA_PLAIN);
+ 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, HA_WIN);
+ fputs("ok", h->lyt.fp); setattr(h, HA_PLAIN);
+ 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)
+/* --- @report_location@ --- *
+ *
+ * Arguments: @struct human_output *h@ = output state
+ * @FILE *fp@ = stream to write the location on
+ * @const char *file@ = filename
+ * @unsigned lno@ = line number
+ *
+ * Returns: ---
+ *
+ * Use: Print the filename and line number to the output stream @fp@.
+ * Also, if appropriate, print interleaved highlighting control
+ * codes to our usual output stream. If @file@ is null then do
+ * nothing.
+ */
+
+static void report_location(struct human_output *h, FILE *fp,
+ const char *file, unsigned lno)
{
- struct human_output *h = (struct human_output *)o;
- struct tvec_state *tv = h->tv;
+ unsigned f = 0;
+#define f_flush 1u
- 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);
+ /* 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->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);
+ report_location(h, h->lyt.fp, tv->infile, tv->test_lno);
+ fprintf(h->lyt.fp, "`%s' ", tv->test->name);
+ setattr(h, attr); fputs(outcome, h->lyt.fp); setattr(h, HA_PLAIN);
+ if (detail) { fputs(": ", h->lyt.fp); vfprintf(h->lyt.fp, detail, *ap); }
+ fputc('\n', h->lyt.fp);
}
+static 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)
gprintf(&human_printops, h, "%*s%s %s = ",
10 + h->maxlen - n, "", ds, rd->name);
if (h->f&HOF_COLOUR) {
- if (!rv) setattr(h, HFG(YELLOW));
- else if (disp == TVRD_FOUND) setattr(h, HFG(RED));
- else if (disp == TVRD_EXPECT) setattr(h, HFG(GREEN));
+ if (!rv) setattr(h, HA_UNSET);
+ else if (disp == TVRD_FOUND) setattr(h, HA_FOUND);
+ else if (disp == TVRD_EXPECT) setattr(h, HA_EXPECT);
}
if (!rv) gprintf(&human_printops, h, "#unset");
else rd->ty->dump(rv, rd, 0, &human_printops, h);
- setattr(h, 0); format_char(&h->fmt, '\n');
+ 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;
if (h->f&HOF_TTY) {
show_progress(h);
switch (outcome) {
- case TVOUT_WIN: ch = '.'; break;
- case TVOUT_LOSE: ch = 'x'; break;
- case TVOUT_SKIP: ch = '_'; break;
+ case TVOUT_WIN: ch = HSB_WIN; break;
+ case TVOUT_LOSE: ch = HSB_LOSE; break;
+ case TVOUT_XFAIL: ch = HSB_XFAIL; break;
+ case TVOUT_SKIP: ch = HSB_SKIP; break;
default: abort();
}
dstr_putc(&h->scoreboard, ch);
- write_scoreboard_char(h, ch); fflush(h->fmt.fp);
+ write_scoreboard_char(h, ch); fflush(h->lyt.fp);
}
}
+/* --- @human_bbench@ --- *
+ *
+ * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct
+ * human_output@
+ * @const char *ident@ = identifying register values
+ * @unsigned unit@ = measurement unit (@TVBU_...@)
+ *
+ * Returns: ---
+ *
+ * Use: Report that a benchmark has started.
+ *
+ * The human driver just prints the start of the benchmark
+ * report.
+ */
+
static void human_bbench(struct tvec_output *o,
const char *ident, unsigned unit)
{
struct tvec_state *tv = h->tv;
clear_progress(h);
- fprintf(h->fmt.fp, "%s: %s: ", tv->test->name, ident); fflush(h->fmt.fp);
+ fprintf(h->lyt.fp, "%s: %s: ", tv->test->name, ident); fflush(h->lyt.fp);
}
+/* --- @human_ebench@ --- *
+ *
+ * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct
+ * human_output@
+ * @const char *ident@ = identifying register values
+ * @unsigned unit@ = measurement unit (@TVBU_...@)
+ * @const struct bench_timing *tm@ = measurement
+ *
+ * Returns: ---
+ *
+ * Use: Report a benchmark's results
+ *
+ * The human driver just delegates to the default benchmark
+ * reporting, via the layout machinery.
+ */
+
static void human_ebench(struct tvec_output *o,
const char *ident, unsigned unit,
const struct bench_timing *tm)
{
struct human_output *h = (struct human_output *)o;
- tvec_benchreport(&human_printops, h->fmt.fp, unit, tm);
- fputc('\n', h->fmt.fp);
+ tvec_benchreport(&human_printops, h, unit, tm);
+ fputc('\n', h->lyt.fp);
}
+/* --- @human_report@ --- *
+ *
+ * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct
+ * human_output@
+ * @unsigned level@ = message level (@TVLEV_...@)
+ * @const char *msg@, @va_list *ap@ = format string and
+ * arguments
+ *
+ * Returns: ---
+ *
+ * Use: Report a message to the user.
+ *
+ * The human driver arranges to show the message on @stderr@ as
+ * well as the usual output, with a certain amount of
+ * intelligence in case they're both actually the same device.
+ */
+
static void human_report(struct tvec_output *o, unsigned level,
const char *msg, va_list *ap)
{
struct human_output *h = (struct human_output *)o;
struct tvec_state *tv = h->tv;
+ const char *levstr; unsigned levattr;
dstr d = DSTR_INIT;
+ unsigned f = 0;
+#define f_flush 1u
+#define f_progress 2u
dstr_vputf(&d, msg, ap); dstr_putc(&d, '\n');
- clear_progress(h); fflush(h->fmt.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);
+ 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->fmt.fp, tv->infile, tv->lno);
- fwrite(d.buf, 1, d.len, h->fmt.fp);
+ 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;
- destroy_fmt(&h->fmt, h->f&HOF_DUPERR ? DFF_CLOSE : 0);
+ destroy_layout(&h->lyt,
+ h->lyt.fp == stdout || h->lyt.fp == stderr ? 0 : DLF_CLOSE);
dstr_destroy(&h->scoreboard);
xfree(h->outbuf); xfree(h);
}
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;
h = xmalloc(sizeof(*h)); h->_o.ops = &human_ops;
h->f = 0; h->attr = 0;
- init_fmt(&h->fmt, fp, 0);
+ init_layout(&h->lyt, fp, 0);
h->outbuf = 0; h->outsz = 0;
switch (getenv_boolean("TVEC_TTY", -1)) {
/*----- Perl's `Test Anything Protocol' -----------------------------------*/
struct tap_output {
- struct tvec_output _o;
- struct tvec_state *tv;
- struct format fmt;
- 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 (format_char(&t->fmt, ch)); }
+ { struct tap_output *t = go; return (layout_char(&t->lyt, ch)); }
static int tap_writem(void *go, const char *p, size_t sz)
- { struct human_output *t = go; return (format_string(&t->fmt, p, sz)); }
+ { struct tap_output *t = go; return (layout_string(&t->lyt, p, sz)); }
static int tap_nwritef(void *go, size_t maxsz, const char *p, ...)
{
- struct human_output *t = go;
+ struct tap_output *t = go;
size_t n;
va_list ap;
va_start(ap, p);
n = gprintf_memputf(&t->outbuf, &t->outsz, maxsz, p, ap);
va_end(ap);
- return (format_string(&t->fmt, t->outbuf, n));
+ return (layout_string(&t->lyt, t->outbuf, 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;
- fputs("TAP version 13\n", t->fmt.fp);
+ 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;
-
- return (tv->grps[TVOUT_WIN] +
- tv->grps[TVOUT_LOSE] +
- tv->grps[TVOUT_SKIP]);
-}
+/* --- @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)
{
if (tv->f&TVSF_ERROR) {
fputs("Bail out! "
"Errors found in input; tests may not have run correctly\n",
- t->fmt.fp);
+ t->lyt.fp);
return (2);
}
- fprintf(t->fmt.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->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(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;
- unsigned
- grpix = tap_grpix(t),
- win = tv->curr[TVOUT_WIN],
- lose = tv->curr[TVOUT_LOSE],
- skip = tv->curr[TVOUT_SKIP];
- 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(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);
}
-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@
+ * @unsigned attr@ = attribute to apply to the outcome
+ * @const char *outcome@ = outcome string to report
+ * @const char *detail@, @va_list *ap@ = a detail message
+ * @const char *excuse@, @va_list *ap@ = reason for skipping the
+ * test
+ *
+ * Returns: ---
+ *
+ * Use: Report that a test has been skipped or failed.
+ *
+ * The TAP driver reports the situation on its output stream.
+ * TAP only allows us to report a single status for each
+ * subtest, so we notice when we've already reported a status
+ * for the current test and convert the second report as a
+ * comment. This should only happen in the case of multiple
+ * failures.
+ */
-static void tap_outcome(struct tvec_output *o, const char *outcome,
+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;
- 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');
+ 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, "skipped", excuse, 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, "FAILED", detail, 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,
struct tap_output *t = (struct tap_output *)o;
const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name);
+ set_layout_prefix(&t->lyt, " ## ");
gprintf(&tap_printops, t, "%*s%s %s = ",
10 + t->maxlen - n, "", ds, rd->name);
if (!rv) gprintf(&tap_printops, t, "#<unset>");
else rd->ty->dump(rv, rd, 0, &tap_printops, t);
- format_char(&t->fmt, '\n');
+ layout_char(&t->lyt, '\n');
+}
+
+/* --- @tap_etest@ --- *
+ *
+ * Arguments: @struct tvec_output *o@ = output sink, secretly a @struct
+ * tap_output@
+ * @unsigned outcome@ = the test outcome
+ *
+ * Returns: ---
+ *
+ * Use: Report that a test has finished.
+ *
+ * The TAP driver reports the outcome of the test, if that's not
+ * already decided.
+ */
+
+static void tap_etest(struct tvec_output *o, unsigned outcome)
+{
+ switch (outcome) {
+ case TVOUT_WIN:
+ tap_outcome(o, "ok", "", 0, 0);
+ break;
+ case TVOUT_XFAIL:
+ tap_outcome(o, "not ok", " # TODO expected failure", 0, 0);
+ break;
+ }
}
-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)
struct tap_output *t = (struct tap_output *)o;
struct tvec_state *tv = t->tv;
+ set_layout_prefix(&t->lyt, " ## ");
gprintf(&tap_printops, t, "%s: %s: ", tv->test->name, ident);
tvec_benchreport(&tap_printops, t, unit, tm);
- format_char(&t->fmt, '\n');
+ 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->fmt.fp);
- gops = &file_printops; go = t->fmt.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;
- destroy_fmt(&t->fmt,
- t->fmt.fp == stdout || t->fmt.fp == stderr ? 0 : DFF_CLOSE);
+ destroy_layout(&t->lyt,
+ t->lyt.fp == stdout || t->lyt.fp == stderr ? 0 : DLF_CLOSE);
xfree(t->outbuf); xfree(t);
}
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_fmt(&t->fmt, 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);