X-Git-Url: https://git.distorted.org.uk/~mdw/mLib/blobdiff_plain/20ba6b0b276f584060415023d68b59917de1e7e6..5c0f2e080603967952db43eb7b12d44dd64f7169:/test/tvec-core.c diff --git a/test/tvec-core.c b/test/tvec-core.c index 36a54ed..d2a08c3 100644 --- a/test/tvec-core.c +++ b/test/tvec-core.c @@ -33,13 +33,36 @@ #include #include "alloc.h" +#include "growbuf.h" #include "tvec.h" /*----- Output ------------------------------------------------------------*/ +/* --- @tvec_strlevel@ --- * + * + * Arguments: @unsigned level@ = level code + * + * Returns: A human-readable description. + * + * Use: Converts a level code into something that you can print in a + * message. + */ + +const char *tvec_strlevel(unsigned level) +{ + switch (level) { +#define CASE(tag, name, val) \ + case TVLEV_##tag: return (name); + TVEC_LEVELS(CASE) +#undef CASE + default: return ("??"); + } +} + /* --- @tvec_report@, @tvec_report_v@ --- * * * Arguments: @struct tvec_state *tv@ = test-vector state + * @unsigned level@ = severity level (@TVlEV_...@) * @const char *msg@, @va_list ap@ = error message * * Returns: --- @@ -94,12 +117,25 @@ void tvec_notice(struct tvec_state *tv, const char *msg, ...) /*----- Test processing ---------------------------------------------------*/ +/* --- @tvec_skipgroup@, @tvec_skipgroup_v@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const char *excuse@, @va_list *ap@ = reason why skipped + * + * Returns: --- + * + * Use: Skip the current group. This should only be called from a + * test environment @setup@ function; a similar effect occurs if + * the @setup@ function fails. + */ + void tvec_skipgroup(struct tvec_state *tv, const char *excuse, ...) { va_list ap; va_start(ap, excuse); tvec_skipgroup_v(tv, excuse, &ap); va_end(ap); } + void tvec_skipgroup_v(struct tvec_state *tv, const char *excuse, va_list *ap) { if (!(tv->f&TVSF_SKIP)) { @@ -108,17 +144,38 @@ void tvec_skipgroup_v(struct tvec_state *tv, const char *excuse, va_list *ap) } } +/* --- @set_outcome@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @unsigned out@ = the new outcome + * + * Returns: --- + * + * Use: Sets the outcome bits in the test state flags, and clears + * @TVSF_ACTIVE@. + */ + static void set_outcome(struct tvec_state *tv, unsigned out) -{ - tv->f &= ~(TVSF_ACTIVE | TVSF_OUTMASK); - tv->f |= out << TVSF_OUTSHIFT; -} + { tv->f = (tv->f&~(TVSF_ACTIVE | TVSF_OUTMASK)) | (out << TVSF_OUTSHIFT); } + +/* --- @tvec_skip@, @tvec_skip_v@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const char *excuse@, @va_list *ap@ = reason why test skipped + * + * Returns: --- + * + * Use: Skip the current test. This should only be called from a + * test environment @run@ function; a similar effect occurs if + * the @before@ function fails. + */ void tvec_skip(struct tvec_state *tv, const char *excuse, ...) { va_list ap; va_start(ap, excuse); tvec_skip_v(tv, excuse, &ap); va_end(ap); } + void tvec_skip_v(struct tvec_state *tv, const char *excuse, va_list *ap) { assert(tv->f&TVSF_ACTIVE); @@ -126,11 +183,29 @@ void tvec_skip_v(struct tvec_state *tv, const char *excuse, va_list *ap) tv->output->ops->skip(tv->output, excuse, ap); } +/* --- @tvec_fail@, @tvec_fail_v@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const char *detail@, @va_list *ap@ = description of test + * + * Returns: --- + * + * Use: Report the current test as a failure. This function can be + * called multiple times for a single test, e.g., if the test + * environment's @run@ function invokes the test function + * repeatedly; but a single test that fails repeatedly still + * only counts as a single failure in the statistics. The + * @detail@ string and its format parameters can be used to + * distinguish which of several invocations failed; it can + * safely be left null if the test function is run only once. + */ + void tvec_fail(struct tvec_state *tv, const char *detail, ...) { va_list ap; va_start(ap, detail); tvec_fail_v(tv, detail, &ap); va_end(ap); } + void tvec_fail_v(struct tvec_state *tv, const char *detail, va_list *ap) { assert((tv->f&TVSF_ACTIVE) || @@ -138,18 +213,50 @@ void tvec_fail_v(struct tvec_state *tv, const char *detail, va_list *ap) set_outcome(tv, TVOUT_LOSE); tv->output->ops->fail(tv->output, detail, ap); } +/* --- @tvec_dumpreg@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @unsigned disp@ = the register disposition (@TVRD_...@) + * @const union tvec_regval *tv@ = register value, or null + * @const struct tvec_regdef *rd@ = register definition + * + * Returns: --- + * + * Use: Dump a register value to the output. This is the lowest- + * level function for dumping registers, and calls the output + * formatter directly. + * + * Usually @tvec_mismatch@ is much more convenient. Low-level + * access is required for reporting `virtual' registers + * corresponding to test environment settings. + */ + void tvec_dumpreg(struct tvec_state *tv, unsigned disp, const union tvec_regval *r, const struct tvec_regdef *rd) { tv->output->ops->dumpreg(tv->output, disp, r, rd); } +/* --- @tvec_mismatch@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @unsigned f@ = flags (@TVMF_...@) + * + * Returns: --- + * + * Use: Dumps registers suitably to report a mismatch. The flag bits + * @TVMF_IN@ and @TVF_OUT@ select input-only and output + * registers. If both are reset then nothing happens. + * Suppressing the output registers may be useful, e.g., if the + * test function crashed rather than returning outputs. + */ + void tvec_mismatch(struct tvec_state *tv, unsigned f) { const struct tvec_regdef *rd; const struct tvec_reg *rin, *rout; for (rd = tv->test->regs; rd->name; rd++) { - if (rd->i >= tv->nrout) { + if (rd->i >= tv->cfg.nrout) { if (!(f&TVMF_IN)) continue; rin = TVEC_REG(tv, in, rd->i); tvec_dumpreg(tv, TVRD_INPUT, rin->f&TVRF_LIVE ? &rin->v : 0, rd); @@ -170,6 +277,20 @@ void tvec_mismatch(struct tvec_state *tv, unsigned f) /*----- Parsing -----------------------------------------------------------*/ +/* --- @tvec_syntax@, @tvec_syntax_v@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @int ch@ = the character found (in @fgetc@ format) + * @const char *expect@, @va_list ap@ = what was expected + * + * Returns: %$-1$%. + * + * Use: Report a syntax error quoting @ch@ and @expect@. If @ch@ is + * a newline, then back up so that it can be read again (e.g., + * by @tvec_flushtoeol@ or @tvec_nexttoken@, which will also + * advance the line number). + */ + int tvec_syntax(struct tvec_state *tv, int ch, const char *expect, ...) { va_list ap; @@ -177,6 +298,7 @@ int tvec_syntax(struct tvec_state *tv, int ch, const char *expect, ...) va_start(ap, expect); tvec_syntax_v(tv, ch, expect, &ap); va_end(ap); return (-1); } + int tvec_syntax_v(struct tvec_state *tv, int ch, const char *expect, va_list *ap) { @@ -196,6 +318,48 @@ int tvec_syntax_v(struct tvec_state *tv, int ch, dstr_destroy(&d); return (-1); } +/* --- @tvec_unkregerr@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const char *name@ = register or pseudoregister name + * + * Returns: %$-1$%. + * + * Use: Reports an error that the register or pseudoregister is + * unrecognized. + */ + +int tvec_unkregerr(struct tvec_state *tv, const char *name) +{ + return (tvec_error(tv, "unknown special register `%s' for test `%s'", + name, tv->test->name)); +} + +/* --- @tvec_dupregerr@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const char *name@ = register or pseudoregister name + * + * Returns: %$-1$%. + * + * Use: Reports an error that the register or pseudoregister has been + * assigned already in the current test. + */ + +int tvec_dupregerr(struct tvec_state *tv, const char *name) + { return (tvec_error(tv, "register `%s' is already set", name)); } + +/* --- @tvec_skipspc@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * + * Returns: --- + * + * Use: Advance over any whitespace characters other than newlines. + * This will stop at `;', end-of-file, or any other kind of + * non-whitespace; and it won't consume a newline. + */ + void tvec_skipspc(struct tvec_state *tv) { int ch; @@ -207,6 +371,24 @@ void tvec_skipspc(struct tvec_state *tv) } } +/* --- @tvec_flushtoeol@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @unsigned f@ = flags (@TVFF_...@) + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Advance to the start of the next line, consuming the + * preceding newline. + * + * A syntax error is reported if no newline character is found, + * i.e., the file ends in mid-line. A syntax error is also + * reported if material other than whitespace or a comment is + * found before the end of the line end, and @TVFF_ALLOWANY@ is + * not set in @f@. The line number count is updated + * appropriately. + */ + int tvec_flushtoeol(struct tvec_state *tv, unsigned f) { int ch, rc = 0; @@ -227,6 +409,29 @@ int tvec_flushtoeol(struct tvec_state *tv, unsigned f) } } +/* --- @tvec_nexttoken@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * + * Returns: Zero if there is a next token which can be read; @-1@ if no + * token is available. + * + * Use: Advance to the next whitespace-separated token, which may be + * on the next line. + * + * Tokens are separated by non-newline whitespace, comments, and + * newlines followed by whitespace; a newline /not/ followed by + * whitespace instead begins the next assignment, and two + * newlines separated only by whitespace terminate the data for + * a test. + * + * If this function returns zero, then the next character in the + * file begins a suitable token which can be read and + * processed. If it returns @-1@ then there is no such token, + * and the file position is left correctly. The line number + * count is updated appropriately. + */ + int tvec_nexttoken(struct tvec_state *tv) { enum { TAIL, NEWLINE, INDENT, COMMENT }; @@ -261,112 +466,230 @@ int tvec_nexttoken(struct tvec_state *tv) } } -int tvec_readword(struct tvec_state *tv, dstr *d, const char *delims, - const char *expect, ...) +/* --- @tvec_readword@, @tvec_readword_v@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @dstr *d@ = string to append the word to + * @const char **p_inout@ = pointer into string, updated + * @const char *delims@ = additional delimiters to stop at + * @const char *expect@, @va_list ap@ = what was expected + * + * Returns: Zero on success, @-1@ on failure. + * + * Use: A `word' consists of characters other than whitespace, null + * characters, and other than those listed in @delims@; + * furthermore, a word does not begin with a `;'. (If you want + * reading to stop at comments not preceded by whitespace, then + * include `;' in @delims@. This is a common behaviour.) + * + * If there is no word beginning at the current file position, + * then return @-1@; furthermore, if @expect@ is not null, then + * report an appropriate error via @tvec_syntax@. + * + * Otherwise, the word is accumulated in @d@ and zero is + * returned; if @d@ was not empty at the start of the call, the + * newly read word is separated from the existing material by a + * single space character. Since null bytes are never valid + * word constituents, a null terminator is written to @d@, and + * it is safe to treat the string in @d@ as being null- + * terminated. + * + * If @p_inout@ is not null, then @*p_inout@ must be a pointer + * into @d->buf@, which will be adjusted so that it will + * continue to point at the same position even if the buffer is + * reallocated. As a subtle tweak, if @*p_inout@ initially + * points at the end of the buffer, then it will be adjusted to + * point at the beginning of the next word, rather than at the + * additional intervening space. + */ + +int tvec_readword(struct tvec_state *tv, dstr *d, const char **p_inout, + const char *delims, const char *expect, ...) { va_list ap; int rc; va_start(ap, expect); - rc = tvec_readword_v(tv, d, delims, expect, &ap); + rc = tvec_readword_v(tv, d, p_inout, delims, expect, &ap); va_end(ap); return (rc); } -int tvec_readword_v(struct tvec_state *tv, dstr *d, const char *delims, - const char *expect, va_list *ap) + +int tvec_readword_v(struct tvec_state *tv, dstr *d, const char **p_inout, + const char *delims, const char *expect, va_list *ap) { + size_t pos = 0; int ch; + tvec_skipspc(tv); + ch = getc(tv->fp); if (!ch || ch == '\n' || ch == EOF || ch == ';' || (delims && strchr(delims, ch))) { if (expect) return (tvec_syntax(tv, ch, expect, ap)); else { ungetc(ch, tv->fp); return (-1); } } - if (d->len) DPUTC(d, ' '); + if (p_inout) pos = *p_inout - d->buf; + if (d->len) { + if (pos == d->len) pos++; + DPUTC(d, ' '); + } do { DPUTC(d, ch); ch = getc(tv->fp); } while (ch && ch != EOF && !isspace(ch) && (!delims || !strchr(delims, ch))); DPUTZ(d); if (ch != EOF) ungetc(ch, tv->fp); + if (p_inout) *p_inout = d->buf + pos; return (0); } /*----- Main machinery ----------------------------------------------------*/ struct groupstate { - void *ctx; + void *ctx; /* test environment context */ + unsigned f; /* flags */ +#define GRPF_SETOUTC 1u /* set outcome */ +#define GRPF_SETMASK (GRPF_SETOUTC) /* mask of all variable flags */ }; -#define GROUPSTATE_INIT { 0 } +#define GROUPSTATE_INIT { 0, 0 } -void tvec_resetoutputs(struct tvec_state *tv) +/* --- @tvec_initregs@, @tvec_releaseregs@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * + * Returns: --- + * + * Use: Initialize or release, respectively, the registers required + * by the current test. All of the registers, both input and + * output, are effected. Initialized registers are not marked + * live. + */ + +void tvec_initregs(struct tvec_state *tv) { const struct tvec_regdef *rd; struct tvec_reg *r; for (rd = tv->test->regs; rd->name; rd++) { - assert(rd->i < tv->nreg); - if (rd->i >= tv->nrout) continue; - r = TVEC_REG(tv, out, rd->i); - rd->ty->release(&r->v, rd); - rd->ty->init(&r->v, rd); - r->f = TVEC_REG(tv, in, rd->i)->f&TVRF_LIVE; + assert(rd->i < tv->cfg.nreg); r = TVEC_REG(tv, in, rd->i); + rd->ty->init(&r->v, rd); r->f = 0; + if (rd->i < tv->cfg.nrout) + { r = TVEC_REG(tv, out, rd->i); rd->ty->init(&r->v, rd); r->f = 0; } } } -void tvec_initregs(struct tvec_state *tv) +void tvec_releaseregs(struct tvec_state *tv) { const struct tvec_regdef *rd; struct tvec_reg *r; for (rd = tv->test->regs; rd->name; rd++) { - assert(rd->i < tv->nreg); r = TVEC_REG(tv, in, rd->i); - rd->ty->init(&r->v, rd); r->f = 0; - if (rd->i < tv->nrout) - { r = TVEC_REG(tv, out, rd->i); rd->ty->init(&r->v, rd); r->f = 0; } + assert(rd->i < tv->cfg.nreg); r = TVEC_REG(tv, in, rd->i); + rd->ty->release(&r->v, rd); r->f = 0; + if (rd->i < tv->cfg.nrout) + { r = TVEC_REG(tv, out, rd->i); rd->ty->release(&r->v, rd); r->f = 0; } } } -void tvec_releaseregs(struct tvec_state *tv) +/* --- @tvec_resetoutputs@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * + * Returns: --- + * + * Use: Reset (releases and reinitializes) the output registers in + * the test state. This is mostly of use to test environment + * @run@ functions, between invocations of the test function. + * Output registers are marked live if and only if the + * corresponding input register is live. + */ + +void tvec_resetoutputs(struct tvec_state *tv) { const struct tvec_regdef *rd; struct tvec_reg *r; for (rd = tv->test->regs; rd->name; rd++) { - assert(rd->i < tv->nreg); r = TVEC_REG(tv, in, rd->i); - rd->ty->release(&r->v, rd); r->f = 0; - if (rd->i < tv->nrout) - { r = TVEC_REG(tv, out, rd->i); rd->ty->release(&r->v, rd); r->f = 0; } + assert(rd->i < tv->cfg.nreg); + if (rd->i >= tv->cfg.nrout) continue; + r = TVEC_REG(tv, out, rd->i); + rd->ty->release(&r->v, rd); + rd->ty->init(&r->v, rd); + r->f = TVEC_REG(tv, in, rd->i)->f&TVRF_LIVE; } } +/* --- @tvec_checkregs@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * + * Returns: Zero on success, @-1@ on mismatch. + * + * Use: Compare the active output registers (according to the current + * test group definition) with the corresponding input register + * values. A mismatch occurs if the two values differ + * (according to the register type's @eq@ method), or if the + * input is live but the output is dead. + * + * This function only checks for a mismatch and returns the + * result; it takes no other action. In particular, it doesn't + * report a failure, or dump register values. + */ + int tvec_checkregs(struct tvec_state *tv) { const struct tvec_regdef *rd; const struct tvec_reg *rin, *rout; for (rd = tv->test->regs; rd->name; rd++) { - if (rd->i >= tv->nrout) continue; + if (rd->i >= tv->cfg.nrout) continue; rin = TVEC_REG(tv, in, rd->i); rout = TVEC_REG(tv, out, rd->i); - if (!rin->f&TVRF_LIVE) continue; + if (!(rin->f&TVRF_LIVE)) continue; if (!(rout->f&TVRF_LIVE) || !rd->ty->eq(&rin->v, &rout->v, rd)) return (-1); } return (0); } +/* --- @tvec_check@, @tvec_check_v@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const char *detail@, @va_list *ap@ = description of test + * + * Returns: --- + * + * Use: Check the register values, reporting a failure and dumping + * the registers in the event of a mismatch. This just wraps up + * @tvec_checkregs@, @tvec_fail@ and @tvec_mismatch@ in the + * obvious way. + */ + void tvec_check(struct tvec_state *tv, const char *detail, ...) { va_list ap; va_start(ap, detail); tvec_check_v(tv, detail, &ap); va_end(ap); } + void tvec_check_v(struct tvec_state *tv, const char *detail, va_list *ap) { if (tvec_checkregs(tv)) { tvec_fail_v(tv, detail, ap); tvec_mismatch(tv, TVMF_IN | TVMF_OUT); } } +/* --- @open_test@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * + * Returns: --- + * + * Use: Note that we are now collecting data for a new test. The + * current line number is recorded in @test_lno@. The + * @TVSF_OPEN@ flag is set, and @TVSF_XFAIL@ is reset. + * + * If a test is already open, then do nothing. + */ + static void open_test(struct tvec_state *tv) { if (!(tv->f&TVSF_OPEN)) { @@ -375,12 +698,34 @@ static void open_test(struct tvec_state *tv) } } +/* --- @begin_test@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * + * Returns: --- + * + * Use: Note that we're about to try running a state. This is called + * before the test environment's @before@ function. Mark the + * test as active, clear the outcome, and inform the output + * driver. + */ + static void begin_test(struct tvec_state *tv) { tv->f |= TVSF_ACTIVE; tv->f &= ~TVSF_OUTMASK; tv->output->ops->btest(tv->output); } +/* --- @tvec_endtest@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * + * Returns: --- + * + * Use: End an ad-hoc test case, The statistics are updated and the + * outcome is reported to the output formatter. + */ + void tvec_endtest(struct tvec_state *tv) { unsigned out; @@ -394,58 +739,146 @@ void tvec_endtest(struct tvec_state *tv) tv->f &= ~TVSF_OPEN; } +/* --- @tvec_xfail@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * + * Returns: --- + * + * Use: Mark the current test as an `expected failure'. That is, the + * behaviour -- if everything works as expected -- is known to + * be incorrect, perhaps as a result of a longstanding bug, so + * calling it a `success' would be inappropriate. A failure, as + * reported by @tvec_fail@, means that the behaviour is not as + * expected -- either the long-standing bug has been fixed, or a + * new bug has been introduced -- so investigation is required. + * + * An expected failure doesn't cause the test group or the + * session as a whole to fail, but it isn't counted as a win + * either. + */ + +void tvec_xfail(struct tvec_state *tv) + { tv->f |= TVSF_XFAIL; } + +/* --- @check@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct groupstate *g@ = private test group state + * + * Returns: --- + * + * Use: Run the current test. + * + * This function is called once the data for a test has been + * collected. It's responsible for checking that all of the + * necessary registers have been assigned values. It marks the + * output registers as live if the corresponding inputs are + * live. It calls the environment's @before@, @run@, and + * @after@ functions if provided; if there is no @run@ function, + * then it calls the test function directly, passing it the + * environment's context pointer, and then calls @tvec_check@ to + * verify the output values. + */ + static void check(struct tvec_state *tv, struct groupstate *g) { const struct tvec_test *t = tv->test; - const struct tvec_env *env; + const struct tvec_env *env = t->env; const struct tvec_regdef *rd; + struct tvec_reg *r; + unsigned f = 0; +#define f_err 1u if (!(tv->f&TVSF_OPEN)) return; for (rd = t->regs; rd->name; rd++) { - if (TVEC_REG(tv, in, rd->i)->f&TVRF_LIVE) - { if (rd->i < tv->nrout) TVEC_REG(tv, out, rd->i)->f |= TVRF_LIVE; } - else if (!(rd->f&TVRF_OPT)) { + r = TVEC_REG(tv, in, rd->i); + if (r->f&TVRF_LIVE) { + if (rd->i < tv->cfg.nrout) + TVEC_REG(tv, out, rd->i)->f |= TVRF_LIVE; + } else if (!(r->f&TVRF_SEEN) && !(rd->f&TVRF_OPT)) { tvec_error(tv, "required register `%s' not set in test `%s'", rd->name, t->name); - goto end; + f |= f_err; } } if (!(tv->f&TVSF_SKIP)) { begin_test(tv); - env = t->env; + if (f&f_err) tvec_skip(tv, "erroneous test data"); if (env && env->before) env->before(tv, g->ctx); if (!(tv->f&TVSF_ACTIVE)) - /* setup forced a skip */; + /* forced a skip */; else if (env && env->run) env->run(tv, t->fn, g->ctx); else { t->fn(tv->in, tv->out, g->ctx); tvec_check(tv, 0); } - if (env && env->after) env->after(tv, g->ctx); tvec_endtest(tv); } -end: + if (env && env->after) env->after(tv, g->ctx); + g->f &= ~GRPF_SETMASK; tv->f &= ~TVSF_OPEN; tvec_releaseregs(tv); tvec_initregs(tv); + +#undef f_err } +/* --- @begin_test_group@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct groupstate *g@ = private test group state + * + * Returns: --- + * + * Use: Begins a test group. Expects @tv->test@ to have been set + * already. Calls the output driver, initializes the registers, + * clears the @tv->curr@ counters, allocates the environment + * context and calls the environment @setup@ function. + */ + static void begin_test_group(struct tvec_state *tv, struct groupstate *g) { const struct tvec_test *t = tv->test; const struct tvec_env *env = t->env; + const struct tvec_regdef *rd0, *rd1; unsigned i; +#ifndef NDEBUG + /* Check that the register names and indices are distinct. */ + for (rd0 = t->regs; rd0->name; rd0++) { + assert(rd0->i < tv->cfg.nreg); + for (rd1 = t->regs; rd1->name; rd1++) + if (rd0 != rd1) { + assert(rd0->i != rd1->i); + assert(STRCMP(rd0->name, !=, rd1->name)); + } + } +#endif + tv->output->ops->bgroup(tv->output); - tv->f &= ~TVSF_SKIP; + tv->f &= ~(TVSF_SKIP | TVSF_MUFFLE); tvec_initregs(tv); for (i = 0; i < TVOUT_LIMIT; i++) tv->curr[i] = 0; if (env && env->ctxsz) g->ctx = xmalloc(env->ctxsz); if (env && env->setup) env->setup(tv, env, 0, g->ctx); } +/* --- @report_group@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * + * Returns: --- + * + * Use: Reports the result of the test group to the output driver. + * + * If all of the tests have been skipped then report this as a + * group skip. Otherwise, determine and report the group + * outcome. + */ + static void report_group(struct tvec_state *tv) { unsigned i, out, nrun; @@ -462,6 +895,24 @@ static void report_group(struct tvec_state *tv) } } +/* --- @end_test_group@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct groupstate *g@ = private test group state + * + * Returns: --- + * + * Use: Handles the end of a test group. Called at the end of the + * input file or when a new test group header is found. + * + * If a test is open, call @check@ to see whether it worked. If + * the test group is not being skipped, report the group + * result. Call the test environment @teardown@ function. Free + * the environment context and release the registers. + * + * If there's no test group active, then nothing happens. + */ + static void end_test_group(struct tvec_state *tv, struct groupstate *g) { const struct tvec_test *t = tv->test; @@ -474,7 +925,44 @@ static void end_test_group(struct tvec_state *tv, struct groupstate *g) tvec_releaseregs(tv); tv->test = 0; xfree(g->ctx); g->ctx = 0; } +/* --- @core_findvar@, @core_setvar@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @const char *var@ = variable name to set + * @const union tvec_regval *rv@ = register value + * @void **ctx_out@ = where to put the @setvar@ context + * @void *ctx@ = context pointer + * + * Returns: @core_findvar@ returns a pointer to the variable definition, + * or null; @core_setvar@ returns zero on success or %$-1$% on + * error. + * + * Use: Find a definition for a special variable. The following + * special variables are supported. + * + * * %|@outcome|% is a token describing how a successful + * outcome of the test should be interpreted: %|success|% or + * %|win|% are the default: a successful test is counted as + * a pass; or %|expected-failure|% or %|xfail|% means a + * successful test is counted as an expected failure. A + * mismatch is always considered a failure. + */ + enum { WIN, XFAIL, NOUT }; + +static int core_setvar(struct tvec_state *tv, const char *name, + const union tvec_regval *rv, void *ctx) +{ + struct groupstate *g = ctx; + + if (STRCMP(name, ==, "@outcome")) { + if (g->f&GRPF_SETOUTC) return (tvec_dupregerr(tv, name)); + if (rv->u == XFAIL) tvec_xfail(tv); + g->f |= GRPF_SETOUTC; + } else assert(!"unknown var"); + return (0); +} + static const struct tvec_uassoc outcome_assoc[] = { { "success", WIN }, { "win", WIN }, @@ -485,8 +973,32 @@ static const struct tvec_uassoc outcome_assoc[] = { static const struct tvec_urange outcome_range = { 0, NOUT - 1 }; static const struct tvec_uenuminfo outcome_enum = { "test-outcome", outcome_assoc, &outcome_range }; -static const struct tvec_regdef outcome_regdef = - { "outcome", 0, &tvty_uenum, 0, { &outcome_enum } }; +static const struct tvec_vardef outcome_vardef = + { sizeof(struct tvec_reg), core_setvar, + { "@outcome", &tvty_uenum, -1, 0, { &outcome_enum } } }; + +static const struct tvec_vardef *core_findvar + (struct tvec_state *tv, const char *name, void **ctx_out, void *ctx) +{ + if (STRCMP(name, ==, "@outcome")) + { *ctx_out = ctx; return (&outcome_vardef); } + else + return (0); +} + +/* --- @tvec_read@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const char *infile@ = the name of the input file + * @FILE *fp@ = stream to read from + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Read test vector data from @fp@ and exercise test functions. + * THe return code doesn't indicate test failures: it's only + * concerned with whether there were problems with the input + * file or with actually running the tests. + */ int tvec_read(struct tvec_state *tv, const char *infile, FILE *fp) { @@ -494,91 +1006,200 @@ int tvec_read(struct tvec_state *tv, const char *infile, FILE *fp) const struct tvec_test *test; const struct tvec_env *env; const struct tvec_regdef *rd; - struct tvec_reg *r; + const struct tvec_vardef *vd = 0; void *varctx; + struct tvec_reg *r = 0, rbuf, *r_alloc = 0; size_t rsz = 0; struct groupstate g = GROUPSTATE_INIT; - union tvec_regval rv; - int ch, ret, rc = 0; + int ch, rc = 0; + /* Set the initial location. */ tv->infile = infile; tv->lno = 1; tv->fp = fp; for (;;) { + + /* Get the next character and dispatch. Note that we're always at the + * start of a line here. + */ ch = getc(tv->fp); switch (ch) { case EOF: + /* End of the file. Exit the loop. */ + goto end; case '[': + /* A test group header. */ + + /* End the current group, if there is one. */ end_test_group(tv, &g); - tvec_skipspc(tv); - DRESET(&d); tvec_readword(tv, &d, "];", "group name"); + + /* Read the group name. There may be leading and trailing + * whitespace. + */ + DRESET(&d); tvec_readword(tv, &d, 0, "];", "group name"); tvec_skipspc(tv); ch = getc(tv->fp); if (ch != ']') tvec_syntax(tv, ch, "`]'"); - for (test = tv->tests; test->name; test++) + + /* Find the matching test definition. */ + for (test = tv->cfg.tests; test->name; test++) if (STRCMP(d.buf, ==, test->name)) goto found_test; - tvec_error(tv, "unknown test group `%s'", d.buf); goto flush_line; + + /* There wasn't one. Report the error. Muffle errors about the + * contents of this section because they won't be interesting. + */ + tvec_error(tv, "unknown test group `%s'", d.buf); + tv->f |= TVSF_MUFFLE; goto flush_line; + found_test: - tvec_flushtoeol(tv, 0); tv->test = test; begin_test_group(tv, &g); + /* Eat trailing whitespace and comments. */ + tvec_flushtoeol(tv, 0); + + /* Set up the new test group. */ + tv->test = test; begin_test_group(tv, &g); break; case '\n': + /* A newline, so this was a completely empty line. Advance the line + * counter, and run the current test. + */ + tv->lno++; if (tv->f&TVSF_OPEN) check(tv, &g); break; + case ';': + /* A semicolon. Skip the comment. */ + + tvec_flushtoeol(tv, TVFF_ALLOWANY); + break; + default: + /* Something else. */ + if (isspace(ch)) { - tvec_skipspc(tv); - ch = getc(tv->fp); + /* Whitespace. Skip and see what we find. */ + + tvec_skipspc(tv); ch = getc(tv->fp); + + /* If the file ends, then we're done. If we find a comment then we + * skip it. If there's some non-whitespace, then report an error. + * Otherwise the line was effectively blank, so run the test. + */ if (ch == EOF) goto end; else if (ch == ';') tvec_flushtoeol(tv, TVFF_ALLOWANY); else if (tvec_flushtoeol(tv, 0)) rc = -1; else check(tv, &g); - } else if (ch == ';') - tvec_flushtoeol(tv, TVFF_ALLOWANY); - else { + } else { + /* Some non-whitespace thing. */ + + /* If there's no test, then report an error. Set the muffle flag, + * because there's no point in complaining about every assignment + * in this block. + */ + if (!tv->test) { + if (!(tv->f&TVSF_MUFFLE)) tvec_error(tv, "no current test"); + tv->f |= TVSF_MUFFLE; goto flush_line; + } + + /* Put the character back and read a word, which ought to be a + * register name. + */ ungetc(ch, tv->fp); DRESET(&d); - if (tvec_readword(tv, &d, "=:;", "register name")) goto flush_line; - tvec_skipspc(tv); ch = getc(tv->fp); - if (ch != '=' && ch != ':') - { tvec_syntax(tv, ch, "`=' or `:'"); goto flush_line; } - tvec_skipspc(tv); - if (!tv->test) - { tvec_error(tv, "no current test"); goto flush_line; } + if (tvec_readword(tv, &d, 0, "=:*;", "register name")) + goto flush_line; + + /* Open the test. This is syntactically a paragraph of settings, + * so it's fair to report on missing register assignments. + */ + open_test(tv); + + /* See what sort of thing we have found. */ if (d.buf[0] == '@') { + /* A special register assignment. */ + env = tv->test->env; - if (STRCMP(d.buf, ==, "@outcome")) { - if (tvty_uenum.parse(&rv, &outcome_regdef, tv)) - ret = -1; - else { - if (rv.u == XFAIL) tv->f |= TVSF_XFAIL; - ret = 1; - } - } else if (!env || !env->set) ret = 0; - else ret = env->set(tv, d.buf, env, g.ctx); - if (ret <= 0) { - if (!ret) - tvec_error(tv, "unknown special register `%s'", d.buf); - goto flush_line; + + /* Find a variable definition. */ + vd = core_findvar(tv, d.buf, &varctx, &g); + if (vd) goto found_var; + if (env && env->findvar) { + vd = env->findvar(tv, d.buf, &varctx, g.ctx); + if (vd) goto found_var; } - open_test(tv); + tvec_unkregerr(tv, d.buf); goto flush_line; + found_var: + rd = &vd->def; } else { + /* A standard register. */ + + /* Find the definition. */ for (rd = tv->test->regs; rd->name; rd++) if (STRCMP(rd->name, ==, d.buf)) goto found_reg; tvec_error(tv, "unknown register `%s' for test `%s'", d.buf, tv->test->name); goto flush_line; found_reg: - open_test(tv); - tvec_skipspc(tv); + + /* Complain if the register is already set. */ r = TVEC_REG(tv, in, rd->i); - if (r->f&TVRF_LIVE) { - tvec_error(tv, "register `%s' already set", rd->name); + if (r->f&TVRF_SEEN) + { tvec_dupregerr(tv, rd->name); goto flush_line; } + } + + /* Now there should be a separator. */ + tvec_skipspc(tv); ch = getc(tv->fp); + + if (ch == '*') { + /* Register explicitly marked unset. */ + + if (vd) { + tvec_error(tv, "can't unset special variables"); goto flush_line; } - if (rd->ty->parse(&r->v, rd, tv)) goto flush_line; - r->f |= TVRF_LIVE; + if (!(rd->f&(TVRF_OPT | TVRF_UNSET))) { + tvec_error(tv, "register `%s' must be assigned " + "a value in test `%s'", rd->name, tv->test->name); + goto flush_line; + } + r->f |= TVRF_SEEN; + if (tvec_flushtoeol(tv, 0)) goto bad; + } else { + /* Common case of a proper assignment. */ + + /* We must have a separator. */ + if (ch != '=' && ch != ':') + { tvec_syntax(tv, ch, "`=', `:', or `*'"); goto flush_line; } + tvec_skipspc(tv); + + if (!vd) { + /* An ordinary register. Parse a value and mark the register + * as live. + */ + + if (rd->ty->parse(&r->v, rd, tv)) goto flush_line; + r->f |= TVRF_LIVE | TVRF_SEEN; + } else { + /* A special register defined by an environment. */ + + /* Set up the register. */ + if (vd->regsz <= sizeof(rbuf)) + r = &rbuf; + else { + GROWBUF_REPLACE(&arena_stdlib, r_alloc, rsz, vd->regsz, + 8*sizeof(void *), 1); + r = r_alloc; + } + + /* Read and set the value. */ + rd->ty->init(&r->v, rd); + if (rd->ty->parse(&r->v, rd, tv)) goto flush_line; + if (!(tv->f&TVSF_SKIP) && vd->setvar(tv, d.buf, &r->v, varctx)) + goto bad; + + /* Clean up. */ + rd->ty->release(&r->v, &vd->def); vd = 0; + } } } break; @@ -586,19 +1207,48 @@ int tvec_read(struct tvec_state *tv, const char *infile, FILE *fp) continue; flush_line: - tvec_flushtoeol(tv, TVFF_ALLOWANY); rc = -1; + /* This is a general parse-failure handler. Skip to the next line and + * remember that things didn't go so well. + */ + tvec_flushtoeol(tv, TVFF_ALLOWANY); + bad: + if (vd) { vd->def.ty->release(&r->v, &vd->def); vd = 0; } + rc = -1; } + + /* We reached the end. If that was actually an I/O error then report it. + */ if (ferror(tv->fp)) { tvec_error(tv, "error reading input: %s", strerror(errno)); rc = -1; } + end: + /* Process the final test, if there was one, and wrap up the final + * group. + */ end_test_group(tv, &g); + + /* Clean up. */ tv->infile = 0; tv->fp = 0; dstr_destroy(&d); + xfree(r_alloc); return (rc); + +#undef rlive } /*----- Session lifecycle -------------------------------------------------*/ +/* --- @tvec_begin@ --- * + * + * Arguments: @struct tvec_state *tv_out@ = state structure to fill in + * @const struct tvec_config *config@ = test configuration + * @struct tvec_output *o@ = output driver + * + * Returns: --- + * + * Use: Initialize a state structure ready to do some testing. + */ + void tvec_begin(struct tvec_state *tv_out, const struct tvec_config *config, struct tvec_output *o) @@ -608,27 +1258,38 @@ void tvec_begin(struct tvec_state *tv_out, tv_out->f = 0; assert(config->nrout <= config->nreg); - tv_out->nrout = config->nrout; tv_out->nreg = config->nreg; - tv_out->regsz = config->regsz; - tv_out->in = xmalloc(tv_out->nreg*tv_out->regsz); - tv_out->out = xmalloc(tv_out->nrout*tv_out->regsz); - for (i = 0; i < tv_out->nreg; i++) { + tv_out->cfg = *config; + tv_out->in = xmalloc(tv_out->cfg.nreg*tv_out->cfg.regsz); + tv_out->out = xmalloc(tv_out->cfg.nrout*tv_out->cfg.regsz); + for (i = 0; i < tv_out->cfg.nreg; i++) { TVEC_REG(tv_out, in, i)->f = 0; - if (i < tv_out->nrout) TVEC_REG(tv_out, out, i)->f = 0; + if (i < tv_out->cfg.nrout) TVEC_REG(tv_out, out, i)->f = 0; } for (i = 0; i < TVOUT_LIMIT; i++) tv_out->curr[i] = tv_out->all[i] = tv_out->grps[i] = 0; - tv_out->tests = config->tests; tv_out->test = 0; + tv_out->test = 0; tv_out->infile = 0; tv_out->lno = 0; tv_out->fp = 0; tv_out->output = o; tv_out->output->ops->bsession(tv_out->output, tv_out); } +/* --- @tvec_end@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * + * Returns: A proposed exit code. + * + * Use: Conclude testing and suggests an exit code to be returned to + * the calling program. (The exit code comes from the output + * driver's @esession@ method.) + */ + int tvec_end(struct tvec_state *tv) { int rc = tv->output->ops->esession(tv->output); + if (tv->test) tvec_releaseregs(tv); tv->output->ops->destroy(tv->output); xfree(tv->in); xfree(tv->out); return (rc); @@ -636,6 +1297,25 @@ int tvec_end(struct tvec_state *tv) /*----- Serialization and deserialization ---------------------------------*/ +/* --- @tvec_serialize@ --- * + * + * Arguments: @const struct tvec_reg *rv@ = vector of registers + * @buf *b@ = buffer to write on + * @const struct tvec_regdef *regs@ = vector of register + * descriptions, terminated by an entry with a null + * @name@ slot + * @unsigned nr@ = number of entries in the @rv@ vector + * @size_t regsz@ = true size of each element of @rv@ + * + * Returns: Zero on success, @-1@ on failure. + * + * Use: Serialize a collection of register values. + * + * The serialized output is written to the buffer @b@. Failure + * can be caused by running out of buffer space, or a failing + * type handler. + */ + int tvec_serialize(const struct tvec_reg *rv, buf *b, const struct tvec_regdef *regs, unsigned nr, size_t regsz) @@ -660,6 +1340,30 @@ int tvec_serialize(const struct tvec_reg *rv, buf *b, return (0); } +/* --- @tvec_deserialize@ --- * + * + * Arguments: @struct tvec_reg *rv@ = vector of registers + * @buf *b@ = buffer to write on + * @const struct tvec_regdef *regs@ = vector of register + * descriptions, terminated by an entry with a null + * @name@ slot + * @unsigned nr@ = number of entries in the @rv@ vector + * @size_t regsz@ = true size of each element of @rv@ + * + * Returns: Zero on success, @-1@ on failure. + * + * Use: Deserialize a collection of register values. + * + * The size of the register vector @nr@ and the register + * definitions @regs@ must match those used when producing the + * serialization. For each serialized register value, + * deserialize and store the value into the appropriate register + * slot, and set the @TVRF_LIVE@ flag on the register. See + * @tvec_serialize@ for a description of the format. + * + * Failure results only from a failing register type handler. + */ + int tvec_deserialize(struct tvec_reg *rv, buf *b, const struct tvec_regdef *regs, unsigned nr, size_t regsz) @@ -690,32 +1394,85 @@ static const struct tvec_regdef no_regs = { 0, 0, 0, 0, { 0 } }; static void fakefn(const struct tvec_reg *in, struct tvec_reg *out, void *p) { assert(!"fake test function"); } +/* --- @tvec_adhoc@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_test *t@ = space for a test definition + * + * Returns: --- + * + * Use: Begin ad-hoc testing, i.e., without reading a file of + * test-vector data. + * + * The structure at @t@ will be used to record information about + * the tests underway, which would normally come from a static + * test definition. The other functions in this section assume + * that @tvec_adhoc@ has been called. + */ + void tvec_adhoc(struct tvec_state *tv, struct tvec_test *t) { t->name = ""; t->regs = &no_regs; t->env = 0; t->fn = fakefn; - tv->tests = t; + tv->cfg.tests = t; } +/* --- @tvec_begingroup@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const char *name@ = name for this test group + * @const char *file@, @unsigned @lno@ = calling file and line + * + * Returns: --- + * + * Use: Begin an ad-hoc test group with the given name. The @file@ + * and @lno@ can be anything, but it's usually best if they + * refer to the source code performing the test: the macro + * @TVEC_BEGINGROUP@ does this automatically. + */ + void tvec_begingroup(struct tvec_state *tv, const char *name, const char *file, unsigned lno) { - struct tvec_test *t = (/*unconst*/ struct tvec_test *)tv->tests; + struct tvec_test *t = (/*unconst*/ struct tvec_test *)tv->cfg.tests; t->name = name; tv->test = t; tv->infile = file; tv->lno = tv->test_lno = lno; begin_test_group(tv, 0); } +/* --- @tvec_endgroup@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * + * Returns: --- + * + * Use: End an ad-hoc test group. The statistics are updated and the + * outcome is reported to the output formatter. + */ + void tvec_endgroup(struct tvec_state *tv) { if (!(tv->f&TVSF_SKIP)) report_group(tv); tv->test = 0; } +/* --- @tvec_begintest@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const char *file@, @unsigned @lno@ = calling file and line + * + * Returns: --- + * + * Use: Begin an ad-hoc test case. The @file@ and @lno@ can be + * anything, but it's usually best if they refer to the source + * code performing the test: the macro @TVEC_BEGINGROUP@ does + * this automatically. + */ + void tvec_begintest(struct tvec_state *tv, const char *file, unsigned lno) { tv->infile = file; tv->lno = tv->test_lno = lno; - begin_test(tv); tv->f |= TVSF_OPEN; + open_test(tv); begin_test(tv); } struct adhoc_claim { @@ -752,6 +1509,31 @@ static void adhoc_claim_teardown(struct tvec_state *tv, if (ck->f&ACF_FRESH) tvec_endtest(tv); } +/* --- @tvec_claim@, @tvec_claim_v@, @TVEC_CLAIM@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @int ok@ = a flag + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *msg@, @va_list *ap@ = message to report on + * failure + * + * Returns: The value @ok@. + * + * Use: Check that a claimed condition holds, as (part of) a test. + * If no test case is underway (i.e., if @TVSF_OPEN@ is reset in + * @tv->f@), then a new test case is begun and ended. The + * @file@ and @lno@ are passed to the output formatter to be + * reported in case of a failure. If @ok@ is nonzero, then + * nothing else happens; so, in particular, if @tvec_claim@ + * established a new test case, then the test case succeeds. If + * @ok@ is zero, then a failure is reported, quoting @msg@. + * + * The @TVEC_CLAIM@ macro is similar, only it (a) identifies the + * file and line number of the call site automatically, and (b) + * implicitly quotes the source text of the @ok@ condition in + * the failure message. + */ + int tvec_claim_v(struct tvec_state *tv, int ok, const char *file, unsigned lno, const char *msg, va_list *ap)