From: Mark Wooding Date: Wed, 27 Dec 2023 17:58:04 +0000 (+0000) Subject: @@@ wip X-Git-Url: https://git.distorted.org.uk/~mdw/mLib/commitdiff_plain/67b5031ec6d160b5cae425466a34d1be3b211dd4?ds=sidebyside;hp=db2bf4111cde36048ac66bbac58547d105bc7e80 @@@ wip --- diff --git a/codec/baseconv.c b/codec/baseconv.c index b7cb0a0..53fceb2 100644 --- a/codec/baseconv.c +++ b/codec/baseconv.c @@ -364,8 +364,8 @@ static int ctxn##_dodecode(ctxn##_ctx *ctx, \ case PC: \ if (f & CDCF_IGNEQMID) break; \ if (f & CDCF_NOEQPAD) goto badch; \ - if (st == ST_MAIN && \ - !(f & CDCF_IGNZPAD) && (a & ((1 << nb) - 1))) \ + if (st == ST_MAIN && !(f & CDCF_IGNZPAD) && \ + ((nb && !(nb%wd)) || (a & ((1 << nb) - 1)))) \ return (CDCERR_INVZPAD); \ st = ST_PAD; \ if (!(f & CDCF_IGNEQPAD)) { \ @@ -387,8 +387,8 @@ static int ctxn##_dodecode(ctxn##_ctx *ctx, \ } \ } \ } else { \ - if (st == ST_MAIN && \ - !(f & CDCF_IGNZPAD) && (a & ((1 << nb) - 1))) \ + if (st == ST_MAIN && !(f & CDCF_IGNZPAD) && \ + ((nb && !(nb%wd)) || (a & ((1 << nb) - 1)))) \ return (CDCERR_INVZPAD); \ if (!(f & (CDCF_IGNEQPAD | CDCF_IGNEQMID | CDCF_NOEQPAD)) && nb) \ return (CDCERR_INVEQPAD); \ diff --git a/codec/codec.h b/codec/codec.h index 6155d5a..f006da5 100644 --- a/codec/codec.h +++ b/codec/codec.h @@ -70,7 +70,7 @@ typedef struct codec_class { _(OK, "No error") \ _(INVCH, "Invalid character") \ _(INVEQPAD, "Invalid padding character") \ - _(INVZPAD, "Nonzero padding bits") + _(INVZPAD, "Excess or nonzero padding bits") enum { #define DECLERR(name, text) CDCERR_##name, CODEC_ERRORS(DECLERR) diff --git a/codec/tests.at b/codec/tests.at index bc93d3a..5462dbb 100644 --- a/codec/tests.at +++ b/codec/tests.at @@ -103,8 +103,10 @@ CODEC_TESTDECODE([base64], [Z:g=:==], [], [-figninvch], [1], [Invalid padding character]) ## Test for incorrect padding bits. +CODEC_TESTDECODE([base64], [A===], [], [], + [1], [Excess or nonzero padding bits]) CODEC_TESTDECODE([base64], [Zh==], [], [], - [1], [Nonzero padding bits]) + [1], [Excess or nonzero padding bits]) CODEC_TESTDECODE([base64], [Zh==], [f], [-fignzpad]) ## Make sure the case flags are suppressed. diff --git a/struct/buf-float.c b/struct/buf-float.c index 502b96b..88d6c44 100644 --- a/struct/buf-float.c +++ b/struct/buf-float.c @@ -32,6 +32,7 @@ #include "bits.h" #include "buf.h" +#include "maths.h" /*----- Formatting primitives ---------------------------------------------*/ @@ -80,24 +81,6 @@ static kludge64 f64_to_k64(double x) /* Some machinery before we start. */ -#ifdef isnan -# define NANP(x) isnan(x) -#else -# define NANP(x) (!((x) == (x))) -#endif - -#ifdef isinf -# define INFP(x) isinf(x) -#else -# define INFP(x) ((x) > DBL_MAX || (x) < -DBL_MAX) -#endif - -#ifdef signbit -# define NEGP(x) signbit(x) -#else -# define NEGP(x) ((x) < 0) /* incorrect for negative zero! */ -#endif - if (NANP(x)) { /* A NaN. */ hi = 0x7ff80000; lo = 0; @@ -168,10 +151,6 @@ static kludge64 f64_to_k64(double x) /* Convert to external format and go home. */ SET64(k, hi, lo); return (k); - -#undef NANP -#undef INFP -#undef NEGP } /* --- @k64_to_f64@ --- * diff --git a/struct/buf-putf.c b/struct/buf-putf.c index 5b465f2..3b4261d 100644 --- a/struct/buf-putf.c +++ b/struct/buf-putf.c @@ -27,6 +27,8 @@ /*----- Header files ------------------------------------------------------*/ +#include "config.h" + #include #include #include @@ -72,9 +74,6 @@ static int nputf(void *out, size_t maxsz, const char *p, ...) va_end(ap); b->p += n; return (n); } -static int putbuf(void *out, const char *p, size_t sz) - { buf *b = out; b->p += sz; return (0); } - const struct gprintf_ops buf_printops = { putch, putm, nputf }; diff --git a/struct/dstr-putf.c b/struct/dstr-putf.c index aa492e0..48c2f6a 100644 --- a/struct/dstr-putf.c +++ b/struct/dstr-putf.c @@ -27,6 +27,8 @@ /*----- Header files ------------------------------------------------------*/ +#include "config.h" + #include #include #include diff --git a/test/bench.c b/test/bench.c index d1a00ea..9e65a6b 100644 --- a/test/bench.c +++ b/test/bench.c @@ -46,19 +46,29 @@ struct timer { struct bench_timer _t; - const struct timer_ops *clkops, *cyops; - union { int fd; } u_cy; + const struct timer_ops *clkops, *cyops; /* time and cycle measurements */ + union { int fd; } u_cy; /* state for cycle measurement */ }; struct timer_ops { - void (*now)(struct bench_time *t_out, struct timer *t); - void (*teardown)(struct timer *t); + void (*now)(struct bench_time *t_out, struct timer *t); /* read current */ + void (*teardown)(struct timer *t); /* release held resources */ }; /*----- Preliminaries -----------------------------------------------------*/ #define NS_PER_S 1000000000 +/* --- @debug@ --- * + * + * Arguments: @const char *fmt@ = format control string + * @...@ = format arguemnts + * + * Returns: --- + * + * Use: Maybe report a debugging message to standard error. + */ + static void PRINTF_LIKE(1, 2) debug(const char *fmt, ...) { const char *p; @@ -74,8 +84,58 @@ static void PRINTF_LIKE(1, 2) debug(const char *fmt, ...) } } +/* --- @timer_diff@ --- * + * + * Arguments: @struct bench_timing *delta_out@ = where to putt the result + * @const struct bench_time *t0, *t1@ = two times captured by a + * timer's @now@ function + * + * Returns: --- + * + * Use: Calculates the difference between two captured times. The + * flags are set according to whether the differences are + * meaningful; @delta_out->n@ is left unset. + */ + +static void timer_diff(struct bench_timing *delta_out, + const struct bench_time *t0, + const struct bench_time *t1) +{ + unsigned f = t0->f&t1->f; + kludge64 k; + +#ifdef HAVE_UINT64 +# define FLOATK64(k) ((double)(k).i) +#else +# define FLOATK64(k) ((double)(k).lo + 4275123318.0*(double)(k).hi) +#endif + + if (!(f&BTF_TIMEOK)) + delta_out->t = 0.0; + else { + SUB64(k, t1->s, t0->s); + delta_out->t = FLOATK64(k) - 1 + + (t1->ns + NS_PER_S - t0->ns)/(double)NS_PER_S; + } + + if (!(f&BTF_CYOK)) + delta_out->cy = 0.0; + else { + SUB64(k, t1->cy, t0->cy); + delta_out->cy = FLOATK64(k); + } + + delta_out->f = f; + +#undef FLOATK64 +} + /*----- The null clock ----------------------------------------------------*/ +/* This is a cycle counter which does nothing, in case we don't have any + * better ideas. + */ + static void null_now(struct bench_time *t_out, struct timer *t) { ; } static void null_teardown(struct timer *t) { ; } static const struct timer_ops null_ops = { null_now, null_teardown }; @@ -87,6 +147,10 @@ static int null_cyinit(struct timer *t) /*----- Linux performance counters ----------------------------------------*/ +/* This is a cycle counter which uses the Linux performance event system, + * which is probably the best choice if it's available. + */ + #if defined(HAVE_LINUX_PERF_EVENT_H) && defined(HAVE_UINT64) #include @@ -143,6 +207,11 @@ static int perfevent_init(struct timer *t) /*----- Intel time-stamp counter ------------------------------------------*/ +/* This is a cycle counter based on the Intel `rdtsc' instruction. It's not + * really suitable for performance measurement because it gets confused by + * CPU frequency adjustments. + */ + #if defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__)) #define EFLAGS_ID (1u << 21) @@ -224,6 +293,10 @@ static int x86rdtsc_init(struct timer *t) /*----- POSIX `clock_gettime' ---------------------------------------------*/ +/* This is a real-time clock based on the POSIX time interface, with up to + * nanosecond precision. + */ + #if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_THREAD_CPUTIME_ID) static void gettime_now(struct bench_time *t_out, struct timer *t) @@ -253,6 +326,10 @@ static int gettime_init(struct timer *t) /*----- Standard C `clock' ------------------------------------------------*/ +/* This is a real-time clock based on the C `clock' function which is + * guaranteed to be available, though it's not likely to be very good. + */ + static void clock_now(struct bench_time *t_out, struct timer *t) { clock_t now, x; @@ -290,6 +367,7 @@ static int clock_init(struct timer *t) /*----- Timing setup ------------------------------------------------------*/ +/* Tables of timing sources. */ static const struct timerent { const char *name; int (*init)(struct timer */*t*/); @@ -297,6 +375,17 @@ static const struct timerent { clktab[] = { GETTIME_CLKENT CLOCK_CLKENT { 0, 0 } }, cytab[] = { PERFEVENT_CYENT X86RDTSC_CYENT NULL_CYENT { 0, 0 } }; +/* --- @find_timer@ --- * + * + * Arguments: @const char *name@ = timer name + * @size_t sz@ = length of name + * @const struct timerent *timers@ = table to search + * @const char *what@ = adjective describing table + * + * Returns: The table entry matching the given name, or null if there + * isn't one. + */ + static const struct timerent *find_timer_n(const char *name, size_t sz, const struct timerent *timers, const char *what) @@ -309,6 +398,18 @@ static const struct timerent *find_timer_n(const char *name, size_t sz, debug("%s timer `%.*s' not found", what, (int)sz, name); return (0); } +/* --- @try_timer@ --- * + * + * Arguments: @struct timer *t@ = timer structure + * @const struct timerent *timer@ = timer table entry + * @const char *what@ = adjective describing table + * + * Returns: Zero on success, @-1@ if timer failed. + * + * Use: Tries to initialize the timer @t@, reporting a debug message + * if it worked. + */ + static int try_timer(struct timer *t, const struct timerent *timer, const char *what) { @@ -316,6 +417,21 @@ static int try_timer(struct timer *t, debug("selected %s timer `%s'", what, timer->name); return (0); } +/* --- @select_timer@ --- * + * + * Arguments: @struct timer *t@ = timer structure + * @const struct timerent *timer@ = timer table + * @const char *varname@ = environment variable to consult + * @const char *what@ = adjective describing table + * + * Returns: Zero on success, @-1@ if timer failed. + * + * Use: Select a timer from the table. If the environment variable + * is set, then parse a comma-separated list of timer names and + * use the first one listed that seems to work; otherwise, try + * the timers in the table in order. + */ + static int select_timer(struct timer *t, const struct timerent *timers, const char *varname, const char *what) { @@ -338,6 +454,7 @@ static int select_timer(struct timer *t, const struct timerent *timers, debug("no suitable %s timer found", what); return (-1); } +/* Bench timer operations. */ static void timer_now(struct bench_timer *tm, struct bench_time *t_out) { struct timer *t = (struct timer *)tm; @@ -345,7 +462,6 @@ static void timer_now(struct bench_timer *tm, struct bench_time *t_out) t->clkops->now(t_out, t); t->cyops->now(t_out, t); } - static void timer_destroy(struct bench_timer *tm) { struct timer *t = (struct timer *)tm; @@ -358,6 +474,16 @@ static void timer_destroy(struct bench_timer *tm) static const struct bench_timerops timer_ops = { timer_now, timer_destroy }; +/* --- @bench_createtimer@ --- * + * + * Arguments: --- + * + * Returns: A freshly constructed standard timer object. + * + * Use: Allocate a timer. Dispose of it by calling + * @tm->ops->destroy(tm)@ when you're done. + */ + struct bench_timer *bench_createtimer(void) { struct timer *t = 0; @@ -372,46 +498,62 @@ end: return (ret); } -#ifdef HAVE_UINT64 -# define FLOATK64(k) ((double)(k).i) -#else -# define FLOATK64(k) ((double)(k).lo + 4275123318.0*(double)(k).hi) -#endif - -static void timer_diff(struct bench_timing *delta_out, - const struct bench_time *t0, - const struct bench_time *t1) -{ - delta_out->f = t0->f&t1->f; - kludge64 k; - - if (!(delta_out->f&BTF_TIMEOK)) - delta_out->t = 0.0; - else { - SUB64(k, t1->s, t0->s); - delta_out->t = FLOATK64(k) - 1 + - (t1->ns + NS_PER_S - t0->ns)/(double)NS_PER_S; - } - - if (!(delta_out->f&BTF_CYOK)) - delta_out->cy = 0.0; - else { - SUB64(k, t1->cy, t0->cy); - delta_out->cy = FLOATK64(k); - } -} +/*----- Benchmarking ------------------------------------------------------*/ -/*----- Calibration -------------------------------------------------------*/ +/* --- @bench_init@ --- * + * + * Arguments: @struct bench_state *b@ = bench state to initialize + * @struct bench_timer *tm@ = timer to attach + * + * Returns: --- + * + * Use: Initialize the benchmark state. It still needs to be + * calibrated (use @bench_calibrate@) before it can be used, but + * this will be done automatically by @bench_measure@ if it's + * not done by hand earlier. The timer is now owned by the + * benchmark state and will be destroyed by @bench_destroy@. + */ void bench_init(struct bench_state *b, struct bench_timer *tm) { b->tm = tm; b->target_s = 1.0; b->f = 0; } +/* --- @bench_destroy@ --- * + * + * Arguments: @struct bench_state *b@ = bench state + * + * Returns: --- + * + * Use: Destroy the benchmark state, releasing the resources that it + * holds. + */ + void bench_destroy(struct bench_state *b) { b->tm->ops->destroy(b->tm); } -static void do_nothing(unsigned long n, void *p) +/* --- @do_nothing@ --- * + * + * Arguments: @unsigned long n@ = iteration count + * @void *ctx@ = context pointer (ignored) + * + * Returns: --- + * + * Use: Does nothing at all for @n@ iterations. Used to calibrate + * the benchmarking state. + */ + +static void do_nothing(unsigned long n, void *ctx) { while (n--) RELAX; } +/* --- @bench_calibrate@ --- * + * + * Arguments: @struct bench_state *b@ = bench state + * + * Returns: Zero on success, @-1@ if calibration failed. + * + * Use: Calibrate the benchmark state, so that it can be used to + * measure performance reasonably accurately. + */ + int bench_calibrate(struct bench_state *b) { struct linreg lr_clk = LINREG_INIT, lr_cy = LINREG_INIT; @@ -424,17 +566,32 @@ int bench_calibrate(struct bench_state *b) unsigned f = BTF_ANY; int rc; + /* The model here is that a timing loop has a fixed overhead as we enter + * and leave (e.g., to do with the indirect branch into the code), and + * per-iteration overheads as we check the counter and loop back. We aim + * to split these apart using linear regression. + */ + + /* If we've already calibrated then there's nothing to do. */ if (b->f&BTF_ANY) return (0); + /* Exercise the inner loop a few times to educate the branch predictor. */ for (i = 0; i < 10; i++) - { tm->ops->now(tm, &t0); fn(1, 0); tm->ops->now(tm, &t1); } + { tm->ops->now(tm, &t0); fn(50, 0); tm->ops->now(tm, &t1); } + /* Now we measure idle loops until they take sufficiently long -- or we run + * out of counter. + */ debug("calibrating..."); n = 1; for (;;) { + + /* Measure @n@ iterations of the idle loop. */ tm->ops->now(tm, &t0); fn(n, 0); tm->ops->now(tm, &t1); timer_diff(&delta, &t0, &t1); f &= delta.f; if (!(f&BTF_TIMEOK)) { rc = -1; goto end; } + + /* Register the timings with the regression machinery. */ linreg_update(&lr_clk, n, delta.t); if (!(f&BTF_CYOK)) debug(" n = %10lu; t = %12g s", n, delta.t); @@ -442,33 +599,64 @@ int bench_calibrate(struct bench_state *b) linreg_update(&lr_cy, n, delta.cy); debug(" n = %10lu; t = %12g s, cy = %10.0f", n, delta.t, delta.cy); } + + /* If we're done then stop. */ if (delta.t >= b->target_s/20.0) break; if (n >= ULONG_MAX - n/3) break; + + /* Update the counter and continue. */ n += n/3 + 1; } + /* Now run the linear regression to extract the constant and per-iteration + * overheads. + */ linreg_fit(&lr_clk, &b->clk.m, &b->clk.c, 0); debug("clock overhead = (%g n + %g) s", b->clk.m, b->clk.c); if (f&BTF_CYOK) { linreg_fit(&lr_clk, &b->clk.m, &b->clk.c, 0); debug("cycle overhead = (%g n + %g) cy", b->cy.m, b->cy.c); } + + /* We're done. */ b->f |= f; rc = 0; end: return (rc); } +/* --- @bench_measure@ --- * + * + * Arguments: @struct bench_timing *t_out@ = where to leave the timing + * @struct bench_state *b@ = benchmark state + * @double base@ = number of internal units per call + * @bench_fn *fn@, @void *ctx@ = benchmark function to run + * + * Returns: Zero on success, @-1@ if timing failed. + * + * Use: Measure a function. The function @fn@ is called adaptively + * with an iteration count @n@ set so as to run for + * approximately @b->target_s@ seconds. + * + * The result is left in @*t_out@, with @t_out->n@ counting the + * final product of the iteration count and @base@ (which might, + * e.g., reflect the number of inner iterations the function + * performs, or the number of bytes it processes per iteration). + */ + int bench_measure(struct bench_timing *t_out, struct bench_state *b, - double base, bench_fn *fn, void *p) + double base, bench_fn *fn, void *ctx) { struct bench_timer *tm = b->tm; struct bench_time t0, t1; unsigned long n; + /* Make sure the state is calibrated. */ if (bench_calibrate(b)) return (-1); + + /* Main adaptive measurement loop. */ debug("measuring..."); n = 1; for (;;) { - tm->ops->now(tm, &t0); fn(n, p); tm->ops->now(tm, &t1); + tm->ops->now(tm, &t0); fn(n, ctx); tm->ops->now(tm, &t1); timer_diff(t_out, &t0, &t1); if (!(t_out->f&BTF_TIMEOK)) return (-1); if (!(t_out->f&BTF_CYOK)) debug(" n = %10lu; t = %12g", n, t_out->t); @@ -476,8 +664,12 @@ int bench_measure(struct bench_timing *t_out, struct bench_state *b, if (t_out->t >= 0.72*b->target_s) break; n *= 1.44*b->target_s/t_out->t; } + + /* Adjust according to the calibration. */ t_out->t -= n*b->clk.m + b->clk.c; if (t_out->f&BTF_CYOK) t_out->cy -= n*b->cy.m + b->cy.c; + + /* Report the results, if debugging. */ if (!(t_out->f&BTF_CYOK)) debug(" adjusted t' = %12g", t_out->t); else debug(" adjusted t = %12g, cy = %10.0f", t_out->t, t_out->cy); if (!(t_out->f&BTF_CYOK)) @@ -485,6 +677,8 @@ int bench_measure(struct bench_timing *t_out, struct bench_state *b, else debug(" %g s (%g cy) per op; %g ops/s", t_out->t/n, t_out->cy/n, n/t_out->t); + + /* All done. */ t_out->n = n*base; return (0); } diff --git a/test/bench.h b/test/bench.h index 2dbce0f..e7736c4 100644 --- a/test/bench.h +++ b/test/bench.h @@ -41,49 +41,116 @@ /*----- Data structures ---------------------------------------------------*/ struct bench_time { - unsigned f; -#define BTF_TIMEOK 1u -#define BTF_CYOK 2u -#define BTF_ANY (BTF_TIMEOK | BTF_CYOK) - kludge64 s; uint32 ns; - kludge64 cy; + unsigned f; /* flags */ +#define BTF_TIMEOK 1u /* @s@ ad @ns@ slots are value */ +#define BTF_CYOK 2u /* @cy@ slot is valid */ +#define BTF_ANY (BTF_TIMEOK | BTF_CYOK) /* some part is useful */ + kludge64 s; uint32 ns; /* real time in seconds and nanos */ + kludge64 cy; /* count of CPU cycles */ }; struct bench_timing { - unsigned f; - double n, t, cy; + unsigned f; /* flags (as in @struct bench_time@) */ + double n, t, cy; /* count, time, and cycles */ }; struct bench_timer { const struct bench_timerops *ops; }; struct bench_timerops { void (*now)(struct bench_timer */*bt*/, struct bench_time */*t_out*/); + /* Fill in @*t_out@ with the current time. v*/ + void (*destroy)(struct bench_timer */*bt*/); + /* Release the timer and any resources it holds. */ }; struct bench_state { - struct bench_timer *tm; - double target_s; - unsigned f; - struct { double m, c; } clk, cy; + struct bench_timer *tm; /* a timer */ + double target_s; /* target time to run benchmarks */ + unsigned f; /* flags (@BTF_...@) for calibrations */ + struct { double m, c; } clk, cy; /* calculated overheads */ }; -typedef void bench_fn(unsigned long /*n*/, void */*p*/); +typedef void bench_fn(unsigned long /*n*/, void */*ctx*/); +/* Run the benchmark @n@ times, given a context pointer @ctx@. */ /*----- Functions provided ------------------------------------------------*/ +/* --- @bench_createtimer@ --- * + * + * Arguments: --- + * + * Returns: A freshly constructed standard timer object. + * + * Use: Allocate a timer. Dispose of it by calling + * @tm->ops->destroy(tm)@ when you're done. + */ + extern struct bench_timer *bench_createtimer(void); +/* --- @bench_init@ --- * + * + * Arguments: @struct bench_state *b@ = bench state to initialize + * @struct bench_timer *tm@ = timer to attach + * + * Returns: --- + * + * Use: Initialize the benchmark state. It still needs to be + * calibrated (use @bench_calibrate@) before it can be used, but + * this will be done automatically by @bench_measure@ if it's + * not done by hand earlier. The timer is now owned by the + * benchmark state and will be destroyed by @bench_destroy@. + */ + extern void bench_init(struct bench_state */*b*/, struct bench_timer */*tm*/); +/* --- @bench_destroy@ --- * + * + * Arguments: @struct bench_state *b@ = bench state + * + * Returns: --- + * + * Use: Destroy the benchmark state, releasing the resources that it + * holds. + */ + extern void bench_destroy(struct bench_state */*b*/); +/* --- @bench_calibrate@ --- * + * + * Arguments: @struct bench_state *b@ = bench state + * + * Returns: Zero on success, @-1@ if calibration failed. + * + * Use: Calibrate the benchmark state, so that it can be used to + * measure performance reasonably accurately. + */ + extern int bench_calibrate(struct bench_state */*b*/); +/* --- @bench_measure@ --- * + * + * Arguments: @struct bench_timing *t_out@ = where to leave the timing + * @struct bench_state *b@ = benchmark state + * @double base@ = number of internal units per call + * @bench_fn *fn@, @void *ctx@ = benchmark function to run + * + * Returns: Zero on success, @-1@ if timing failed. + * + * Use: Measure a function. The function @fn@ is called adaptively + * with an iteration count @n@ set so as to run for + * approximately @b->target_s@ seconds. + * + * The result is left in @*t_out@, with @t_out->n@ counting the + * final product of the iteration count and @base@ (which might, + * e.g., reflect the number of inner iterations the function + * performs, or the number of bytes it processes per iteration). + */ + extern int bench_measure(struct bench_timing */*t_out*/, struct bench_state */*b*/, - double /*base*/, bench_fn */*fn*/, void */*p*/); + double /*base*/, bench_fn */*fn*/, void */*ctx*/); /*----- That's all, folks -------------------------------------------------*/ diff --git a/test/t/tvec-test.c b/test/t/tvec-test.c index 91c4529..6dd0ef0 100644 --- a/test/t/tvec-test.c +++ b/test/t/tvec-test.c @@ -129,7 +129,7 @@ static const struct tvec_urange range_32 = { 0, 31 }; _(flags, RF, flags, p, &attr_info) \ _(string, RSTR, string, p, &range_32) \ _(bytes, RBY, bytes, p, &tvrange_byte) \ - _(buffer, RBUF, buffer, p, &tvrange_u16) + _(buffer, RBUF, buffer, p, 0) enum { /* Output registers, one for each register type. */ diff --git a/test/tests.at b/test/tests.at index 9a890da..74f40fa 100644 --- a/test/tests.at +++ b/test/tests.at @@ -85,7 +85,7 @@ test_parse([int], [4], [4 ; = 0x04 = '\x04']) test_parse([int], [ 17; comment], [17 ; = 0x11 = '\x11']) test_parse([int], [0x234], [564 ; = 0x0234]) -test_parse([int], [0o33], [27 ; = 0x1b = '\e']) +test_parse([int], [0o33], [27 ; = 0x1b = @%:@escape = '\e']) test_parse([int], [ +192], [192 ; = 0xc0 = '\xc0']) test_parse([int], [ -192], [-192 ; = -0xc0]) @@ -123,7 +123,7 @@ test_parserr([int], [0x], [3], [syntax error: expected end-of-line but found `x']) test_parserr([int], [], - [3], [syntax error: expected signed integer but found @%:@]) + [3], [syntax error: expected signed integer but found @%:@eol]) test_parserr([int], [123456], [3], [integer 123456 out of range (must be in @<:@-32768 .. 32767@:>@)]) @@ -136,9 +136,11 @@ AT_SETUP(tvec type-uint) test_parse([uint], [4], [4 ; = 0x04 = '\x04']) test_parse([uint], [ 17; comment], [17 ; = 0x11 = '\x11']) +test_parse([uint], [0], [0 ; = 0x00 = @%:@nul = '\0']) + test_parse([uint], [012345], [12345 ; = 0x3039]) test_parse([uint], [0x234], [564 ; = 0x0234]) -test_parse([uint], [0o33], [27 ; = 0x1b = '\e']) +test_parse([uint], [0o33], [27 ; = 0x1b = @%:@escape = '\e']) test_parse([uint], [0b1011_1101], [189 ; = 0xbd = '\xbd']) test_parse([uint], [12r123], [171 ; = 0xab = '\xab']) @@ -176,7 +178,7 @@ test_parserr([uint], [0x], [3], [syntax error: expected end-of-line but found `x']) test_parserr([uint], [], - [3], [syntax error: expected unsigned integer but found @%:@]) + [3], [syntax error: expected unsigned integer but found @%:@eol]) test_parserr([uint], [123456], [3], [integer 123456 out of range (must be in @<:@0 .. 65535@:>@)]) @@ -191,6 +193,13 @@ test_parse([float], [-0.0], [-0]) test_parse([float], [1.234], [1.234]) +test_parse([float], [@%:@nan], [@%:@nan]) +test_parse([float], [@%:@+inf], [@%:@+inf]) +test_parse([float], [@%:@inf], [@%:@+inf]) +test_parse([float], [+@%:@inf], [@%:@+inf]) +test_parse([float], [@%:@-inf], [@%:@-inf]) +test_parse([float], [-@%:@inf], [@%:@-inf]) + AT_CLEANUP ###-------------------------------------------------------------------------- @@ -211,6 +220,175 @@ test_parse([penum], [@%:@nil], [@%:@nil]) AT_CLEANUP ###-------------------------------------------------------------------------- +AT_SETUP([tvec type-char]) + +test_parse([char], [a], ['a' ; = 97 = 0x61]) +test_parse([char], [a;?], ['a' ; = 97 = 0x61]) +test_parse([char], [a ;?], ['a' ; = 97 = 0x61]) +test_parse([char], [\\], ['\\' ; = 92 = 0x5c]) +test_parse([char], ['], ['\'' ; = 39 = 0x27]) +test_parse([char], [\'], ['\'' ; = 39 = 0x27]) +test_parse([char], ["], ['"' ; = 34 = 0x22]) +test_parse([char], [';'], [';' ; = 59 = 0x3b]) +test_parse([char], [';';?], [';' ; = 59 = 0x3b]) +test_parse([char], [';' ;?], [';' ; = 59 = 0x3b]) +test_parse([char], [\"], ['"' ; = 34 = 0x22]) # " +test_parse([char], ['@%:@'], ['@%:@' ; = 35 = 0x23]) + +test_parse([char], [\n], [@%:@newline ; = '\n' = 10 = 0x0a]) +test_parse([char], [\x0a], [@%:@newline ; = '\n' = 10 = 0x0a]) + +test_parse([char], [@%:@newline], [@%:@newline ; = '\n' = 10 = 0x0a]) +test_parse([char], [@%:@lf], [@%:@newline ; = '\n' = 10 = 0x0a]) +test_parse([char], [@%:@del;?], [@%:@delete ; = '\x7f' = 127 = 0x7f]) +test_parse([char], [@%:@space ;?], [' ' ; = 32 = 0x20]) + +test_parse([char], [' '], [' ' ; = 32 = 0x20]) +test_parse([char], ['a'], ['a' ; = 97 = 0x61]) +test_parse([char], ['\n'], [@%:@newline ; = '\n' = 10 = 0x0a]) +test_parse([char], ['\12' ;?], [@%:@newline ; = '\n' = 10 = 0x0a]) +test_parse([char], ['\{12}' ;?], [@%:@newline ; = '\n' = 10 = 0x0a]) +test_parse([char], ['\x0a'], [@%:@newline ; = '\n' = 10 = 0x0a]) +test_parse([char], ['\x{0a}'], [@%:@newline ; = '\n' = 10 = 0x0a]) + +test_parse([char], [@%:@eof], [@%:@eof ; = -1 = -0x01]) +test_parse([char], [@%:@eof], [@%:@eof ; = -1 = -0x01]) + +test_parserr([char], [ ], + [3], [syntax error: expected character but found @%:@eol]) +test_parserr([char], [''], + [3], [syntax error: expected character but found `'']) +test_parserr([char], ['''], + [3], [syntax error: expected character but found `'']) +test_parserr([char], [;], + [3], [syntax error: expected character but found `;']) +test_parserr([char], [@%:@], + [3], [unknown character name `@%:@']) +test_parserr([char], [';], + [3], [syntax error: expected `'' but found @%:@eol]) +test_parserr([char], [\], + [3], [syntax error: expected string escape but found @%:@eol]) +test_parserr([char], [\q], + [3], [syntax error: expected string escape but found `q']) +test_parserr([char], ['\], + [3], [syntax error: expected string escape but found @%:@eol]) +test_parserr([char], [ab], + [3], [syntax error: expected end-of-line but found `b']) +test_parserr([char], [\x{0a], + [3], [syntax error: expected `}' but found @%:@eol]) +test_parserr([char], [\{012], + [3], [syntax error: expected `}' but found @%:@eol]) + +AT_CLEANUP + +###-------------------------------------------------------------------------- +AT_SETUP([tvec type-string]) + +test_parse([string], [foo], [foo]) +test_parse([string], [foo bar], ["foo bar"]) +test_parse([string], [foo bar], ["foo bar"]) +test_parse([string], [foo "" bar], [foobar]) +test_parse([string], [ foo bar ], ["foo bar"]) +test_parse([string], [ foo @&t@ + bar ], ["foo bar"]) + +test_parse([string], [foo @%:@nul bar], ["foo\{0}bar"]) + +test_parse([string], ["f" !repeat 2 { o } "bar"], [foobar]) +test_parse([string], ["{"!repeat 5{"abc"}"}"], ["{abcabcabcabcabc}"]) + +test_parse([string], [!hex "f" 6f "o"], [foo]) + +test_parse([string], ["foo\n"], ["foo\n"]) + +test_parse([string], [foo\ +bar], [ + "foo\n" + "bar"]) +test_parse([string], ["foo\ +bar"], [foobar]) +test_parse([string], ["foo" @%:@newline "bar" @%:@newline], [ + "foo\n" + "bar\n"]) + +test_parserr([string], [], + [4], [syntax error: expected string but found @%:@eof]) +test_parse([string], [""], [""]) +test_parse([string], [''], [""]) + +test_parse([string], ["f\x{6f}o"], [foo]) + +AT_CLEANUP + +###-------------------------------------------------------------------------- +AT_SETUP([tvec type-bytes]) + +test_parse([bytes], [""], ["" ; empty]) +test_parse([bytes], [61], [61 ; a]) +test_parse([bytes], ["abc"], [616263 ; abc]) +test_parse([bytes], ["abcd"], [61626364 ; abcd]) +test_parse([bytes], ["abcde"], [61626364 65 ; abcde]) + +test_parse([bytes], [!base64 YWJjZGVmZ2hpamtsbW5vcA==], + [61626364 65666768 696a6b6c 6d6e6f70 ; abcdefghijklmnop]) +test_parse([bytes], + [!base64 QUJDREVGR0hJSktMTU5PUGFiY2RlZmdo + a Wp rbG 1ub3A=], + [ + 41424344 45464748 494a4b4c 4d4e4f50 ; @<:@00@:>@ ABCDEFGHIJKLMNOP + 61626364 65666768 696a6b6c 6d6e6f70 ; @<:@10@:>@ abcdefghijklmnop]) + +test_parse([bytes], [6 1], [61 ; a]) +test_parserr([bytes], [6 "" 1], + [3], [invalid hex sequence end: Excess or nonzero padding bits]) + +test_parse([bytes], [!base64 AA==], [00 ; .]) +test_parse([bytes], [!base64 AAA=], [0000 ; ..]) +test_parse([bytes], [!base64 AAAA], [000000 ; ...]) +test_parse([bytes], [!base64 AA], [00 ; .]) +test_parse([bytes], [!base64 AAA], [0000 ; ..]) + +test_parserr([bytes], [0], + [4], [invalid hex sequence end: Excess or nonzero padding bits]) +test_parserr([bytes], [!base64 A], + [4], [invalid base64 sequence end: Excess or nonzero padding bits]) +test_parserr([bytes], [!base64 A=], + [3], [invalid base64 fragment `A=': Excess or nonzero padding bits]) + +AT_CLEANUP + +###-------------------------------------------------------------------------- +AT_SETUP([tvec type-buffer]) + +test_parse([buffer], [16], [16 B]) +test_parse([buffer], [16;?], [16 B]) +test_parse([buffer], [16 ;?], [16 B]) +test_parse([buffer], [16384], [16 kB]) +test_parse([buffer], [16777216], [16 MB]) +test_parse([buffer], [16k], [16 kB]) +test_parse([buffer], [16k;?], [16 kB]) +test_parse([buffer], [16k ;?], [16 kB]) +test_parse([buffer], [16 k], [16 kB]) +test_parse([buffer], [16 k;?], [16 kB]) +test_parse([buffer], [16 k ;?], [16 kB]) +test_parse([buffer], [16kB], [16 kB]) +test_parse([buffer], [16kB;?], [16 kB]) +test_parse([buffer], [16kB ;?], [16 kB]) +test_parse([buffer], [16 kB], [16 kB]) +test_parse([buffer], [16 kB;?], [16 kB]) +test_parse([buffer], [16 kB ;?], [16 kB]) + +test_parserr([buffer], [16!], [3], [invalid buffer length `16!']) +test_parserr([buffer], [16 !], [3], [invalid buffer length `16 !']) +test_parserr([buffer], [16 k!], [3], [invalid buffer length `16 k!']) +test_parserr([buffer], [16 kB!], [3], [invalid buffer length `16 kB!']) +test_parserr([buffer], [16 kB !], + [3], [syntax error: expected end-of-line but found `!']) +test_parserr([buffer], [16 EB], [3], [buffer length `16 EB' out of range]) + +AT_CLEANUP + +###-------------------------------------------------------------------------- AT_SETUP([tvec serialize]) AT_DATA([tv], diff --git a/test/tvec-bench.c b/test/tvec-bench.c index ab21b9c..b56fcbe 100644 --- a/test/tvec-bench.c +++ b/test/tvec-bench.c @@ -33,19 +33,34 @@ /*----- Data structures ---------------------------------------------------*/ struct benchrun { - struct tvec_state *tv; - const struct tvec_env *env; - void *ctx; - unsigned long *n; - const struct tvec_reg *in; struct tvec_reg *out; - tvec_testfn *fn; + struct tvec_state *tv; /* test vector state */ + const struct tvec_env *env; /* subordinate environment */ + void *ctx; /* subordinate env's context */ + unsigned long *n; /* iteration count address */ + const struct tvec_reg *in; struct tvec_reg *out; /* register vectors */ + tvec_testfn *fn; /* test function to run */ }; /*----- Global variables --------------------------------------------------*/ -struct bench_state *tvec_benchstate; +struct bench_state *tvec_benchstate; /* common benchmarking state */ -/*----- Benchmarking ------------------------------------------------------*/ +/*----- Utilities ---------------------------------------------------------*/ + +/* --- @normalize@ --- * + * + * Arguments: @double *x_inout@ = address of a value to normalize + * @const char **unit_out@ = address to store unit prefix + * @double scale@ = scale factor for unit steps + * + * Returns: --- + * + * Use: Adjust @*x_inout@ by a power of @scale@, and set @*unit_out@ + * so that printing the two reflects the original value with an + * appropriate SI unit scaling. The @scale@ should be 1024 for + * binary quantities, most notably memory sizes, or 1000 for + * other quantities. + */ static void normalize(double *x_inout, const char **unit_out, double scale) { @@ -66,6 +81,29 @@ static void normalize(double *x_inout, const char **unit_out, double scale) *x_inout = x; *unit_out = *u; } +/*----- Benchmark environment scaffolding ---------------------------------*/ + +/* --- @tvec_benchsetup@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @const struct tvec_env *env@ = environment description + * @void *pctx@ = parent context (ignored) + * @void *ctx@ = context pointer to initialize + * + * Returns: Zero on success, @-1@ on failure. + * + * Use: Initialize a benchmarking environment context. + * + * The environment description must really be a @struct + * tvec_bench@. If the @bst@ slot is null, then a temporary + * benchmark state is allocated for the current test group and + * released at the end. Otherwise, it must be the address of a + * pointer to a benchmark state: if the pointer is null, then a + * fresh state is allocated and initialized and the pointer is + * updated; otherwise, the pointer is assumed to refer to an + * existing valid benchmark state. + */ + int tvec_benchsetup(struct tvec_state *tv, const struct tvec_env *env, void *pctx, void *ctx) { @@ -74,8 +112,10 @@ int tvec_benchsetup(struct tvec_state *tv, const struct tvec_env *env, const struct tvec_env *subenv = b->env; struct bench_timer *bt; + /* Basic initialization. */ bc->b = b; bc->bst = 0; bc->subctx = 0; + /* Set up the benchmarking state if it hasn't been done before. */ if (!b->bst || !*b->bst) { bt = bench_createtimer(); if (!bt) goto fail_timer; bc->bst = xmalloc(sizeof(*bc->bst)); bench_init(bc->bst, bt); @@ -84,18 +124,41 @@ int tvec_benchsetup(struct tvec_state *tv, const struct tvec_env *env, goto fail_timer; else bc->bst = *b->bst; + + /* Set the default target time. */ bc->dflt_target = bc->bst->target_s; + /* Initialize the subordinate environment. */ if (subenv && subenv->ctxsz) bc->subctx = xmalloc(subenv->ctxsz); if (subenv && subenv->setup && subenv->setup(tv, subenv, bc, bc->subctx)) { xfree(bc->subctx); bc->subctx = 0; return (-1); } + /* All done. */ end: return (0); fail_timer: tvec_skipgroup(tv, "failed to create timer"); goto end; } +/* --- @tvec_benchset@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @const char *var@ = variable name to set + * @const struct tvec_env *env@ = environment description + * @void *ctx@ = context pointer + * + * Returns: Zero on success, @-1@ on failure. + * + * Use: Set a special variable. The following special variables are + * supported. + * + * * %|@target|% is the (approximate) number of seconds to run + * the benchmark. + * + * Unrecognized variables are passed to the subordinate + * environment, if there is one. + */ + int tvec_benchset(struct tvec_state *tv, const char *var, const struct tvec_env *env, void *ctx) { @@ -117,26 +180,62 @@ int tvec_benchset(struct tvec_state *tv, const char *var, return (0); } +/* --- @tvec_benchbefore@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @void *ctx@ = context pointer + * + * Returns: Zero on success, @-1@ on failure. + * + * Use: Invoke the subordinate environment's @before@ function to + * prepare for the benchmark. + */ + int tvec_benchbefore(struct tvec_state *tv, void *ctx) { struct tvec_benchctx *bc = ctx; const struct tvec_bench *b = bc->b; const struct tvec_env *subenv = b->env; + /* Just call the subsidiary environment. */ if (subenv && subenv->before) return (subenv->before(tv, bc->subctx)); else return (0); } +/* --- @tvec_benchafter@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @void *ctx@ = context pointer + * + * Returns: --- + * + * Use: Invoke the subordinate environment's @after@ function to + * clean up after the benchmark. + */ + void tvec_benchafter(struct tvec_state *tv, void *ctx) { struct tvec_benchctx *bc = ctx; const struct tvec_bench *b = bc->b; const struct tvec_env *subenv = b->env; + /* Restore the benchmark state's old target. */ bc->bst->target_s = bc->dflt_target; + + /* Pass the call on to the subsidiary environment. */ if (subenv && subenv->after) subenv->after(tv, bc->subctx); } +/* --- @tvec_benchteardown@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @void *ctx@ = context pointer + * + * Returns: --- + * + * Use: Tear down the benchmark environment. + */ + void tvec_benchteardown(struct tvec_state *tv, void *ctx) { struct tvec_benchctx *bc = ctx; @@ -145,38 +244,79 @@ void tvec_benchteardown(struct tvec_state *tv, void *ctx) if (!bc) return; b = bc->b; subenv = b->env; + + /* Tear down any subsidiary environment. */ if (subenv && subenv->teardown && bc->subctx) subenv->teardown(tv, bc->subctx); + + /* If the benchmark state was temporary, then dispose of it. */ if (bc->bst) { if (b->bst) bc->bst->target_s = bc->dflt_target; else { bench_destroy(bc->bst); xfree(bc->bst); } } } -static void benchloop_outer_direct(unsigned long n, void *p) +/*----- Measurement machinery ---------------------------------------------*/ + +/* --- @benchloop_...@ --- * + * + * Arguments: @unsigned long n@ = iteration count + * @void *ctx@ = benchmark running context + * + * Returns: --- + * + * Use: Run various kinds of benchmarking loops. + * + * * The @..._outer_...@ functions call the underlying + * function @n@ times in a loop; by contrast, the + * @..._inner_...@ functions set a register value to the + * chosen iteration count and expect the underlying function + * to perform the loop itself. + * + * * The @..._direct@ functions just call the underlying test + * function directly (though still through an `indirect + * jump' instruction); by contrast, the @..._indirect@ + * functions invoke a subsidiary environment's @run@ + * function, which adds additional overhead. + */ + +static void benchloop_outer_direct(unsigned long n, void *ctx) { - struct benchrun *r = p; - tvec_testfn *fn = r->fn; void *ctx = r->ctx; + struct benchrun *r = ctx; + tvec_testfn *fn = r->fn; void *tctx = r->ctx; const struct tvec_reg *in = r->in; struct tvec_reg *out = r->out; - while (n--) fn(in, out, ctx); + while (n--) fn(in, out, tctx); } -static void benchloop_inner_direct(unsigned long n, void *p) - { struct benchrun *r = p; *r->n = n; r->fn(r->in, r->out, r->ctx); } +static void benchloop_inner_direct(unsigned long n, void *ctx) + { struct benchrun *r = ctx; *r->n = n; r->fn(r->in, r->out, r->ctx); } -static void benchloop_outer_indirect(unsigned long n, void *p) +static void benchloop_outer_indirect(unsigned long n, void *ctx) { - struct benchrun *r = p; + struct benchrun *r = ctx; struct tvec_state *tv = r->tv; void (*run)(struct tvec_state *, tvec_testfn, void *) = r->env->run; - tvec_testfn *fn = r->fn; void *ctx = r->ctx; + tvec_testfn *fn = r->fn; void *tctx = r->ctx; - while (n--) run(tv, fn, ctx); + while (n--) run(tv, fn, tctx); } -static void benchloop_inner_indirect(unsigned long n, void *p) - { struct benchrun *r = p; *r->n = n; r->env->run(r->tv, r->fn, r->ctx); } +static void benchloop_inner_indirect(unsigned long n, void *ctx) + { struct benchrun *r = ctx; *r->n = n; r->env->run(r->tv, r->fn, r->ctx); } + +/* --- @tvec_benchrun@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @tvec_testfn *fn@ = test function to run + * @void *ctx@ = context pointer for the test function + * + * Returns: --- + * + * Use: Measures and reports the performance of a test function. + * + * + */ void tvec_benchrun(struct tvec_state *tv, tvec_testfn *fn, void *ctx) { @@ -194,9 +334,11 @@ void tvec_benchrun(struct tvec_state *tv, tvec_testfn *fn, void *ctx) unsigned f = 0; #define f_any 1u + /* Fill in the easy parts of the run context. */ r.tv = tv; r.env = subenv; r.ctx = bc->subctx; r.in = tv->in; r.out = tv->out; r.fn = fn; + /* Decide on the run function to select. */ if (b->riter >= 0) { r.n = &TVEC_REG(tv, in, b->riter)->v.u; loopfn = subenv && subenv->run ? @@ -207,10 +349,12 @@ void tvec_benchrun(struct tvec_state *tv, tvec_testfn *fn, void *ctx) benchloop_outer_indirect : benchloop_outer_direct; } + /* Decide on the kind of unit and the base count. */ base = b->niter; if (b->rbuf < 0) unit = TVBU_OP; else { unit = TVBU_BYTE; base *= TVEC_REG(tv, in, b->rbuf)->v.bytes.sz; } + /* Construct a description of the test using the identifier registers. */ for (rd = tv->test->regs; rd->name; rd++) if (rd->f&TVRF_ID) { if (f&f_any) dstr_puts(&d, ", "); @@ -220,6 +364,7 @@ void tvec_benchrun(struct tvec_state *tv, tvec_testfn *fn, void *ctx) TVSF_COMPACT, &dstr_printops, &d); } + /* Run the benchmark. */ o->ops->bbench(o, d.buf, unit); if (bench_measure(&tm, bc->bst, base, loopfn, &r)) o->ops->ebench(o, d.buf, unit, 0); @@ -231,6 +376,22 @@ void tvec_benchrun(struct tvec_state *tv, tvec_testfn *fn, void *ctx) #undef f_any } +/*----- Output utilities --------------------------------------------------*/ + +/* --- @tvec_benchreport@ --- * + * + * Arguments: @const struct gprintf_ops *gops@ = print operations + * @void *go@ = print destination + * @unsigned unit@ = the unit being measured (~TVBU_...@) + * @const struct bench_timing *tm@ = the benchmark result + * + * Returns: --- + * + * Use: Formats a report about the benchmark performance. This + * function is intended to be called on by an output + * @ebench@ function. + */ + void tvec_benchreport(const struct gprintf_ops *gops, void *go, unsigned unit, const struct bench_timing *tm) { diff --git a/test/tvec-core.c b/test/tvec-core.c index 03873dc..92f31d3 100644 --- a/test/tvec-core.c +++ b/test/tvec-core.c @@ -37,15 +37,41 @@ /*----- Output ------------------------------------------------------------*/ +/* --- @tvec_error@, @tvec_error_v@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const char *msg@, @va_list ap@ = error message + * + * Returns: @-1@. + * + * Use: Report an error. Errors are distinct from test failures, + * and indicate that a problem was encountered which compromised + * the activity of testing. + */ + int tvec_error(struct tvec_state *tv, const char *msg, ...) { va_list ap; va_start(ap, msg); tvec_error_v(tv, msg, &ap); va_end(ap); - tv->f |= TVSF_ERROR; return (-1); + return (-1); } int tvec_error_v(struct tvec_state *tv, const char *msg, va_list *ap) - { tv->output->ops->error(tv->output, msg, ap); return (-1); } +{ + tv->output->ops->error(tv->output, msg, ap); + tv->f |= TVSF_ERROR; return (-1); +} + +/* --- @tvec_notice@, @tvec_notice_v@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const char *msg@, @va_list ap@ = message + * + * Returns: --- + * + * Use: Output a notice: essentially, some important information + * which doesn't fit into any of the existing categories. + */ void tvec_notice(struct tvec_state *tv, const char *msg, ...) { @@ -55,35 +81,12 @@ void tvec_notice(struct tvec_state *tv, const char *msg, ...) void tvec_notice_v(struct tvec_state *tv, const char *msg, va_list *ap) { tv->output->ops->notice(tv->output, msg, ap); } -int tvec_syntax(struct tvec_state *tv, int ch, const char *expect, ...) -{ - va_list ap; - - 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) -{ - dstr d = DSTR_INIT; - char found[8]; - - switch (ch) { - case EOF: strcpy(found, "#"); break; - case '\n': strcpy(found, "#"); ungetc(ch, tv->fp); break; - default: - if (isprint(ch)) sprintf(found, "`%c'", ch); - else sprintf(found, "#<\\x%02x>", ch); - break; - } - dstr_vputf(&d, expect, ap); - tvec_error(tv, "syntax error: expected %s but found %s", expect, found); - return (-1); -} +/*----- Test processing ---------------------------------------------------*/ 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) @@ -152,12 +155,33 @@ void tvec_mismatch(struct tvec_state *tv, unsigned f) } } -/*----- Main machinery ----------------------------------------------------*/ +/*----- Parsing -----------------------------------------------------------*/ -struct groupstate { - void *ctx; -}; -#define GROUPSTATE_INIT { 0 } +int tvec_syntax(struct tvec_state *tv, int ch, const char *expect, ...) +{ + va_list ap; + + 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) +{ + dstr d = DSTR_INIT; + char found[8]; + + switch (ch) { + case EOF: strcpy(found, "#eof"); break; + case '\n': strcpy(found, "#eol"); ungetc(ch, tv->fp); break; + default: + if (isprint(ch)) sprintf(found, "`%c'", ch); + else sprintf(found, "#\\x%02x", ch); + break; + } + dstr_vputf(&d, expect, ap); + tvec_error(tv, "syntax error: expected %s but found %s", d.buf, found); + dstr_destroy(&d); return (-1); +} void tvec_skipspc(struct tvec_state *tv) { @@ -256,6 +280,13 @@ int tvec_readword_v(struct tvec_state *tv, dstr *d, const char *delims, return (0); } +/*----- Main machinery ----------------------------------------------------*/ + +struct groupstate { + void *ctx; +}; +#define GROUPSTATE_INIT { 0 } + void tvec_resetoutputs(struct tvec_state *tv) { const struct tvec_regdef *rd; diff --git a/test/tvec-main.c b/test/tvec-main.c index a9cff8f..fc1a2ae 100644 --- a/test/tvec-main.c +++ b/test/tvec-main.c @@ -47,6 +47,7 @@ /*----- Main code ---------------------------------------------------------*/ +/* Table of output formats. */ static const struct outform { const char *name; struct tvec_output *(*makefn)(FILE *fp); @@ -56,9 +57,20 @@ static const struct outform { { 0, 0 } }; +/* Configuration for ad-hoc testing. */ const struct tvec_config tvec_adhocconfig = { 0, 1, 1, sizeof(struct tvec_reg) }; +/* --- @find_outform@ --- + * + * Arguments: @const char *p@ = output name + * + * Returns: Pointer to output format record. + * + * Use: Looks up an output format by name. Reports a fatal error if + * no matching record is found. + */ + static const struct outform *find_outform(const char *p) { const struct outform *best = 0, *of; @@ -78,6 +90,15 @@ static const struct outform *find_outform(const char *p) else die(2, "unknown output format `%s'", optarg); } +/* --- @version@, @usage@, @help@ --- * + * + * Arguments: @FILE *fp@ = stream to write on + * + * Returns: --- + * + * Use: Output information about the program. + */ + static void version(FILE *fp) { pquis(fp, "$, mLib test-vector framework version " VERSION "\n"); } @@ -102,6 +123,21 @@ Options:\n\ ", fp); } +/* --- @tvec_parseargs@ --- * + * + * Arguments: @int argc@ = number of command-line arguments + * @char *argv[]@ = vector of argument strings + * @struct tvec_state *tv_out@ = test vector state to initialize + * @int *argpos_out@ = where to leave unread argument index + * @const struct tvec_config *cofig@ = test vector configuration + * + * Returns: --- + * + * Use: Parse arguments and set up the test vector state @*tv_out@. + * If errors occur, print messages to standard error and exit + * with status 2. + */ + void tvec_parseargs(int argc, char *argv[], struct tvec_state *tv_out, int *argpos_out, const struct tvec_config *config) { @@ -158,6 +194,19 @@ void tvec_parseargs(int argc, char *argv[], struct tvec_state *tv_out, tvec_begin(tv_out, config, o); *argpos_out = optind; } +/* --- @tvec_readstdin@, @tvec_readfile@, @tvec_readarg@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @const char *file@ = pathname of file to read + * @const char *arg@ = argument to interpret + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Read test vector data from stdin or a named file. The + * @tvec_readarg@ function reads from stdin if @arg@ is `%|-|%', + * and from the named file otherwise. + */ + int tvec_readstdin(struct tvec_state *tv) { return (tvec_read(tv, "", stdin)); } @@ -179,16 +228,39 @@ end: return (rc); } -int tvec_readdflt(struct tvec_state *tv, const char *file) +int tvec_readarg(struct tvec_state *tv, const char *arg) +{ + int rc; + + if (STRCMP(arg, ==, "-")) rc = tvec_readstdin(tv); + else rc = tvec_readfile(tv, arg); + return (rc); +} + +/* --- @tvec_readdflt@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @const char *dflt@ = defsault filename or null + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Reads from the default test vector data. If @file@ is null, + * then read from standard input, unless that's a terminal; if + * @file@ is not null, then read the named file, looking in the + * directory named by the `%|srcdir|%' environment variable if + * that's set, or otherwise in the current directory. + */ + +int tvec_readdflt(struct tvec_state *tv, const char *dflt) { dstr d = DSTR_INIT; const char *p; int rc; - if (file) { + if (dflt) { p = getenv("srcdir"); - if (p) { dstr_putf(&d, "%s/%s", p, file); file = d.buf; } - rc = tvec_readfile(tv, file); + if (p) { dstr_putf(&d, "%s/%s", p, dflt); dflt = d.buf; } + rc = tvec_readfile(tv, dflt); } else if (isatty(0)) rc = tvec_error(tv, "use `-' to force reading from interactive stdin"); else @@ -197,14 +269,21 @@ int tvec_readdflt(struct tvec_state *tv, const char *file) return (rc); } -int tvec_readarg(struct tvec_state *tv, const char *arg) -{ - int rc; - - if (STRCMP(arg, ==, "-")) rc = tvec_readstdin(tv); - else rc = tvec_readfile(tv, arg); - return (rc); -} +/* --- @tvec_readargs@ --- * + * + * Arguments: @int argc@ = number of command-line arguments + * @char *argv[]@ = vector of argument strings + * @struct tvec_state *tv@ = test vector state + * @int *argpos_inout@ = current argument position (updated) + * @const char *dflt@ = default filename or null + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Reads from the sources indicated by the command-line + * arguments, in order, interpreting each as for @tvec_readarg@; + * if no arguments are given then read from @dflt@ as for + * @tvec_readdflt@. + */ int tvec_readargs(int argc, char *argv[], struct tvec_state *tv, int *argpos_inout, const char *dflt) @@ -212,7 +291,8 @@ int tvec_readargs(int argc, char *argv[], struct tvec_state *tv, int i = *argpos_inout; int rc; - if (i == argc) rc = tvec_readdflt(tv, dflt); + if (i == argc) + rc = tvec_readdflt(tv, dflt); else { rc = 0; while (i < argc) @@ -222,6 +302,21 @@ int tvec_readargs(int argc, char *argv[], struct tvec_state *tv, return (rc); } +/* --- @tvec_main@ --- * + * + * Arguments: @int argc@ = number of command-line arguments + * @char *argv[]@ = vector of argument strings + * @const struct tvec_config *cofig@ = test vector configuration + * @const char *dflt@ = default filename or null + * + * Returns: Exit code. + * + * Use: All-in-one test vector front-end. Parse options from the + * command-line as for @tvec_parseargs@, and then process the + * remaining positional arguments as for @tvec_readargs@. The + * function constructs and disposes of a test vector state. + */ + int tvec_main(int argc, char *argv[], const struct tvec_config *config, const char *dflt) { diff --git a/test/tvec-output.c b/test/tvec-output.c index c4809bb..9ca4f29 100644 --- a/test/tvec-output.c +++ b/test/tvec-output.c @@ -30,6 +30,7 @@ #include "config.h" #include +#include #include #include #include @@ -39,12 +40,21 @@ #include "alloc.h" #include "bench.h" #include "dstr.h" +#include "macros.h" #include "quis.h" #include "report.h" #include "tvec.h" /*----- Common machinery --------------------------------------------------*/ +/* --- @regdisp@ --- * + * + * Arguments: @unsigned disp@ = a @TVRD_...@ disposition code + * + * Returns: A human-readable adjective describing the register + * disposition. + */ + static const char *regdisp(unsigned disp) { switch (disp) { @@ -57,6 +67,15 @@ static const char *regdisp(unsigned disp) } } +/* --- @getenv_boolean@ --- * + * + * Arguments: @const char *var@ = environment variable name + * @int dflt@ = default value + * + * Returns: @0@ if the variable is set to something falseish, @1@ if it's + * set to something truish, or @dflt@ otherwise. + */ + static int getenv_boolean(const char *var, int dflt) { const char *p; @@ -75,12 +94,19 @@ static int getenv_boolean(const char *var, int dflt) STRCMP(p, ==, "0")) return (0); else { - moan("unexpected value `%s' for boolean environment variable `%s'", + moan("ignoring unexpected value `%s' for environment variable `%s'", var, p); return (dflt); } } +/* --- @register_maxnamelen@ --- * + * + * Arguments: @const struct tvec_state *tv@ = test vector state + * + * Returns: The maximum length of a register name in the current test. + */ + static int register_maxnamelen(const struct tvec_state *tv) { const struct tvec_regdef *rd; @@ -91,6 +117,285 @@ static int register_maxnamelen(const struct tvec_state *tv) return (maxlen); } +/*----- Output formatting -------------------------------------------------*/ + +/* We have two main jobs in output formatting: trimming trailing blanks; and + * adding a prefix to each line. + * + * This is somehow much more complicated than it ought to be. + */ + +struct format { + 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 */ +}; + +/* Support macros. These assume `fmt' is defined as a pointer to the `struct + * format' state. + */ + +#define SPLIT_RANGE(tail, base, limit) do { \ + /* Set TAIL to point just after the last nonspace character between \ + * BASE and LIMIT. If there are no nonspace characters, then set \ + * TAIL to equal BASE. \ + */ \ + \ + for (tail = limit; tail > base && ISSPACE(tail[-1]); tail--); \ +} while (0) + +#define PUT_RANGE(base, limit) do { \ + /* Write the range of characters between BASE and LIMIT to the output \ + * file. Return immediately on error. \ + */ \ + \ + size_t n = limit - base; \ + if (fwrite(base, 1, n, fmt->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); \ +} 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); \ +} 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); \ +} while (0) + +#define PUT_PFXINB do { \ + /* Output the initial nonblank portion of the prefix, if there is \ + * one. Return immediately on error. \ + */ \ + \ + if (fmt->prefix) PUT_RANGE(fmt->prefix, fmt->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); \ +} while (0) + +/* --- @init_fmt@ --- * + * + * Arguments: @struct format *fmt@ = formatting state to initialize + * @FILE *fp@ = output file + * @const char *prefix@ = prefix string (or null if empty) + * + * Returns: --- + * + * Use: Initialize a formatting state. + */ + +static void init_fmt(struct format *fmt, FILE *fp, 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; + else { + fmt->prefix = prefix; + l = fmt->pfxlim = prefix + strlen(prefix); + SPLIT_RANGE(q, prefix, l); fmt->pfxtail = q; + DPUTM(&fmt->w, q, l - q); + } +} + +/* --- @destroy_fmt@ --- * + * + * Arguments: @struct format *fmt@ = formatting state + * @unsigned f@ = flags (@DFF_...@) + * + * Returns: --- + * + * Use: Releases a formatting state and the resources it holds. + * Close the file if @DFF_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) +{ + if (f&DFF_CLOSE) fclose(fmt->fp); + dstr_destroy(&fmt->w); +} + +/* --- @format_char@ --- * + * + * Arguments: @struct format *fmt@ = formatting 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) +{ + if (ch == '\n') { + if (fmt->f&FMTF_NEWL) PUT_PFXINB; + PUT_CHAR('\n'); fmt->f |= FMTF_NEWL; DRESET(&fmt->w); + } else if (isspace(ch)) + DPUTC(&fmt->w, ch); + else { + if (fmt->f&FMTF_NEWL) { PUT_PFXINB; fmt->f &= ~FMTF_NEWL; } + PUT_SAVED; PUT_CHAR(ch); DRESET(&fmt->w); + } + return (0); +} + +/* --- @format_string@ --- * + * + * Arguments: @struct format *fmt@ = formatting state + * @const char *p@ = string to write + * @size_t sz@ = length of string + * + * Returns: Zero on success, @-1@ on failure. + * + * Use: Write a string to the output. + */ + +static int format_string(struct format *fmt, const char *p, size_t sz) +{ + const char *q, *r, *l = p + sz; + + /* This is rather vexing. There are a small number of jobs to do, but the + * logic for deciding which to do when gets rather hairy if, as I've tried + * here, one aims to minimize the number of decisions being checked, so + * it's worth canning them into macros. + * + * Here, a `blank' is a whitespace character other than newline. The input + * buffer consists of one or more `segments', each of which consists of: + * + * * an initial portion, which is either empty or ends with a nonblank + * character; + * + * * a suffix which consists only of blanks; and + * + * * an optional newline. + * + * All segments except the last end with a newline. + */ + +#define SPLIT_SEGMENT do { \ + /* Determine the bounds of the current segment. If there is a final \ + * newline, then q is non-null and points to this newline; otherwise, \ + * q is null. The initial portion of the segment lies between p .. r \ + * and the blank suffix lies between r .. q (or r .. l if q is null). \ + * This sounds awkward, but the suffix is only relevant if there is \ + * no newline. \ + */ \ + \ + q = memchr(p, '\n', l - p); SPLIT_RANGE(r, p, q ? q : l); \ +} while (0) + +#define PUT_NONBLANK do { \ + /* Output the initial portion of the segment. */ \ + \ + PUT_RANGE(p, r); \ +} while (0) + +#define PUT_NEWLINE do { \ + /* Write a newline, and advance to the next segment. */ \ + \ + PUT_CHAR('\n'); p = q + 1; \ +} while (0) + +#define SAVE_TAIL do { \ + /* Save the trailing blank portion of the segment in the buffer. \ + * Assumes that there is no newline, since otherwise the suffix would \ + * be omitted. \ + */ \ + \ + DPUTM(&fmt->w, r, l - r); \ +} while (0) + + /* Determine the bounds of the first segment. Handling this is the most + * complicated part of this function. + */ + SPLIT_SEGMENT; + + if (!q) { + /* This is the only segment. We'll handle the whole thing here. + * + * If there's an initial nonblank portion, then we need to write that + * out. Furthermore, if we're at the start of the line then we'll need + * to write the prefix, and if there's saved blank material then we'll + * need to write that. Otherwise, there's only blank stuff, which we + * accumulate in the buffer. + * + * If we're at the start of a line here, then + */ + + if (r > p) { + if (fmt->f&FMTF_NEWL) { PUT_PFXINB; fmt->f &= ~FMTF_NEWL; } + PUT_SAVED; PUT_NONBLANK; DRESET(&fmt->w); + } + SAVE_TAIL; + return (0); + } + + /* There is at least one more segment, so we know that there'll be a line + * to output. + */ + if (fmt->f&FMTF_NEWL) PUT_PFXINB; + if (r > p) { PUT_SAVED; PUT_NONBLANK; } + PUT_NEWLINE; DRESET(&fmt->w); + SPLIT_SEGMENT; + + /* Main loop over whole segments with trailing newlines. For each one, we + * know that we're starting at the beginning of a line and there's a final + * newline, so we write the initial prefix and drop the trailing blanks. + */ + while (q) { + PUT_PREFIX; PUT_NONBLANK; PUT_NEWLINE; + SPLIT_SEGMENT; + } + + /* At the end, there's no final newline. If there's nonblank material, + * then we can write the prefix and the nonblank stuff. Otherwise, stash + * the blank stuff (including the trailing blanks of the prefix) and leave + * the newline flag set. + */ + if (r > p) { PUT_PREFIX; PUT_NONBLANK; fmt->f &= ~FMTF_NEWL; } + else { fmt->f |= FMTF_NEWL; SAVE_PFXTAIL; } + SAVE_TAIL; + +#undef SPLIT_SEGMENT +#undef PUT_NONBLANK +#undef PUT_NEWLINE +#undef SAVE_TAIL + + return (0); +} + +#undef SPLIT_RANGE +#undef PUT_RANGE +#undef PUT_PREFIX +#undef PUT_PFXINB +#undef PUT_SAVED +#undef PUT_CHAR +#undef SAVE_PFXTAIL + /*----- Skeleton ----------------------------------------------------------*/ /* static void ..._bsession(struct tvec_output *o, struct tvec_state *tv) @@ -152,7 +457,8 @@ static const struct tvec_outops ..._ops = { struct human_output { struct tvec_output _o; struct tvec_state *tv; - FILE *fp; + struct format fmt; + char *outbuf; size_t outsz; dstr scoreboard; unsigned attr; int maxlen; @@ -178,27 +484,29 @@ static void setattr(struct human_output *h, unsigned attr) int sep = 0; if (!diff || !(h->f&HOF_COLOUR)) return; - fputs("\x1b[", h->fp); + fputs("\x1b[", h->fmt.fp); if (diff&HAF_BOLD) { - if (attr&HAF_BOLD) putc('1', h->fp); - else { putc('0', h->fp); diff = h->attr; } + if (attr&HAF_BOLD) putc('1', h->fmt.fp); + else { putc('0', h->fmt.fp); diff = h->attr; } sep = ';'; } if (diff&(HAF_FG | HAF_FGMASK)) { if (attr&HAF_FG) - set_colour(h->fp, &sep, "3", "9", (attr&HAF_FGMASK) >> HAF_FGSHIFT); + set_colour(h->fmt.fp, &sep, "3", "9", + (attr&HAF_FGMASK) >> HAF_FGSHIFT); else - { if (sep) putc(sep, h->fp); fputs("39", h->fp); sep = ';'; } + { if (sep) putc(sep, h->fmt.fp); fputs("39", h->fmt.fp); sep = ';'; } } if (diff&(HAF_BG | HAF_BGMASK)) { if (attr&HAF_BG) - set_colour(h->fp, &sep, "4", "10", (attr&HAF_BGMASK) >> HAF_BGSHIFT); + set_colour(h->fmt.fp, &sep, "4", "10", + (attr&HAF_BGMASK) >> HAF_BGSHIFT); else - { if (sep) putc(sep, h->fp); fputs("49", h->fp); sep = ';'; } + { if (sep) putc(sep, h->fmt.fp); fputs("49", h->fmt.fp); sep = ';'; } } - putc('m', h->fp); h->attr = attr; + putc('m', h->fmt.fp); h->attr = attr; #undef f_any } @@ -209,7 +517,7 @@ static void clear_progress(struct human_output *h) if (h->f&HOF_PROGRESS) { n = strlen(h->tv->test->name) + 2 + h->scoreboard.len; - for (i = 0; i < n; i++) fputs("\b \b", h->fp); + for (i = 0; i < n; i++) fputs("\b \b", h->fmt.fp); h->f &= ~HOF_PROGRESS; } } @@ -221,7 +529,7 @@ static void write_scoreboard_char(struct human_output *h, int ch) case '_': setattr(h, HA_SKIP); break; default: setattr(h, 0); break; } - putc(ch, h->fp); setattr(h, 0); + putc(ch, h->fmt.fp); setattr(h, 0); } static void show_progress(struct human_output *h) @@ -230,12 +538,12 @@ static void show_progress(struct human_output *h) const char *p, *l; if (tv->test && (h->f&HOF_TTY) && !(h->f&HOF_PROGRESS)) { - fprintf(h->fp, "%s: ", tv->test->name); + fprintf(h->fmt.fp, "%s: ", tv->test->name); if (!(h->f&HOF_COLOUR)) - dstr_write(&h->scoreboard, h->fp); + dstr_write(&h->scoreboard, h->fmt.fp); else for (p = h->scoreboard.buf, l = p + h->scoreboard.len; p < l; p++) write_scoreboard_char(h, *p); - fflush(h->fp); h->f |= HOF_PROGRESS; + fflush(h->fmt.fp); h->f |= HOF_PROGRESS; } } @@ -247,29 +555,55 @@ static void report_location(struct human_output *h, FILE *fp, #define FLUSH(fp) do if (f&f_flush) fflush(fp); while (0) - if (fp != h->fp) f |= f_flush; + if (fp != h->fmt.fp) f |= f_flush; if (file) { - setattr(h, HFG(CYAN)); FLUSH(h->fp); fputs(file, fp); FLUSH(fp); - setattr(h, HFG(BLUE)); FLUSH(h->fp); fputc(':', fp); FLUSH(fp); - setattr(h, HFG(CYAN)); FLUSH(h->fp); fprintf(fp, "%u", lno); FLUSH(fp); - setattr(h, HFG(BLUE)); FLUSH(h->fp); fputc(':', fp); FLUSH(fp); - setattr(h, 0); FLUSH(h->fp); fputc(' ', fp); + 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 } +static int human_writech(void *go, int ch) + { struct human_output *h = go; return (format_char(&h->fmt, 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)); } + +static int human_nwritef(void *go, size_t maxsz, const char *p, ...) +{ + struct human_output *h = go; + size_t n; + va_list ap; + + va_start(ap, p); + n = gprintf_memputf(&h->outbuf, &h->outsz, maxsz, p, ap); + va_end(ap); + return (format_string(&h->fmt, h->outbuf, n)); +} + +static const struct gprintf_ops human_printops = + { human_writech, human_writem, human_nwritef }; + 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) { if (n) { - fprintf(h->fp, " (%u ", n); - setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0); - fputc(')', h->fp); + fprintf(h->fmt.fp, " (%u ", n); + setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0); + fputc(')', h->fmt.fp); } } @@ -284,28 +618,28 @@ static int human_esession(struct tvec_output *o) all_run = all_win + all_lose, grps_run = grps_win + grps_lose; if (!all_lose) { - setattr(h, HA_WIN); fputs("PASSED", h->fp); setattr(h, 0); - fprintf(h->fp, " %s%u %s", + setattr(h, HA_WIN); fputs("PASSED", h->fmt.fp); setattr(h, 0); + fprintf(h->fmt.fp, " %s%u %s", !(all_skip || grps_skip) ? "all " : "", all_win, all_win == 1 ? "test" : "tests"); report_skipped(h, all_skip); - fprintf(h->fp, " in %u %s", + fprintf(h->fmt.fp, " in %u %s", grps_win, grps_win == 1 ? "group" : "groups"); report_skipped(h, grps_skip); } else { - setattr(h, HA_LOSE); fputs("FAILED", h->fp); setattr(h, 0); - fprintf(h->fp, " %u out of %u %s", + setattr(h, HA_LOSE); fputs("FAILED", h->fmt.fp); setattr(h, 0); + fprintf(h->fmt.fp, " %u out of %u %s", all_lose, all_run, all_run == 1 ? "test" : "tests"); report_skipped(h, all_skip); - fprintf(h->fp, " in %u out of %u %s", + fprintf(h->fmt.fp, " in %u out of %u %s", grps_lose, grps_run, grps_run == 1 ? "group" : "groups"); report_skipped(h, grps_skip); } - fputc('\n', h->fp); + fputc('\n', h->fmt.fp); if (tv->f&TVSF_ERROR) { - setattr(h, HA_ERR); fputs("ERRORS", h->fp); setattr(h, 0); - fputs(" found in input; tests may not have run correctly\n", h->fp); + 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); } h->tv = 0; return (tv->f&TVSF_ERROR ? 2 : tv->all[TVOUT_LOSE] ? 1 : 0); @@ -326,13 +660,13 @@ static void human_skipgroup(struct tvec_output *o, if (!(~h->f&(HOF_TTY | HOF_PROGRESS))) { h->f &= ~HOF_PROGRESS; - setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0); + setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0); } else { - fprintf(h->fp, "%s: ", h->tv->test->name); - setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0); + fprintf(h->fmt.fp, "%s: ", h->tv->test->name); + setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0); } - if (excuse) { fputs(": ", h->fp); vfprintf(h->fp, excuse, *ap); } - fputc('\n', h->fp); + if (excuse) { fputs(": ", h->fmt.fp); vfprintf(h->fmt.fp, excuse, *ap); } + fputc('\n', h->fmt.fp); } static void human_egroup(struct tvec_output *o) @@ -343,17 +677,18 @@ static void human_egroup(struct tvec_output *o) skip = tv->curr[TVOUT_SKIP], run = win + lose; if (h->f&HOF_TTY) h->f &= ~HOF_PROGRESS; - else fprintf(h->fp, "%s:", h->tv->test->name); + else fprintf(h->fmt.fp, "%s:", h->tv->test->name); if (lose) { - fprintf(h->fp, " %u/%u ", lose, run); - setattr(h, HA_LOSE); fputs("FAILED", h->fp); setattr(h, 0); + fprintf(h->fmt.fp, " %u/%u ", lose, run); + setattr(h, HA_LOSE); fputs("FAILED", h->fmt.fp); setattr(h, 0); report_skipped(h, skip); } else { - fputc(' ', h->fp); setattr(h, HA_WIN); fputs("ok", h->fp); setattr(h, 0); + fputc(' ', h->fmt.fp); setattr(h, HA_WIN); + fputs("ok", h->fmt.fp); setattr(h, 0); report_skipped(h, skip); } - fputc('\n', h->fp); + fputc('\n', h->fmt.fp); } static void human_btest(struct tvec_output *o) @@ -366,11 +701,11 @@ static void human_skip(struct tvec_output *o, struct tvec_state *tv = h->tv; clear_progress(h); - report_location(h, h->fp, tv->infile, tv->test_lno); - fprintf(h->fp, "`%s' ", tv->test->name); - setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0); - if (excuse) { fputs(": ", h->fp); vfprintf(h->fp, excuse, *ap); } - fputc('\n', h->fp); + 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); } static void human_fail(struct tvec_output *o, @@ -380,11 +715,11 @@ static void human_fail(struct tvec_output *o, struct tvec_state *tv = h->tv; clear_progress(h); - report_location(h, h->fp, tv->infile, tv->test_lno); - fprintf(h->fp, "`%s' ", tv->test->name); - setattr(h, HA_LOSE); fputs("FAILED", h->fp); setattr(h, 0); - if (detail) { fputs(": ", h->fp); vfprintf(h->fp, detail, *ap); } - fputc('\n', h->fp); + 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); } static void human_dumpreg(struct tvec_output *o, @@ -394,15 +729,17 @@ static void human_dumpreg(struct tvec_output *o, struct human_output *h = (struct human_output *)o; const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name); - fprintf(h->fp, "%*s%s %s = ", 10 + h->maxlen - n, "", ds, rd->name); + clear_progress(h); + gprintf(&human_printops, h, "%*s%s %s = ", + 10 + h->maxlen - n, "", ds, rd->name); if (h->f&HOF_COLOUR) { if (!rv) setattr(h, HFG(YELLOW)); else if (disp == TVRD_FOUND) setattr(h, HFG(RED)); else if (disp == TVRD_EXPECT) setattr(h, HFG(GREEN)); } - if (!rv) fprintf(h->fp, "#"); - else rd->ty->dump(rv, rd, 0, &file_printops, h->fp); - setattr(h, 0); fputc('\n', h->fp); + 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'); } static void human_etest(struct tvec_output *o, unsigned outcome) @@ -419,7 +756,7 @@ static void human_etest(struct tvec_output *o, unsigned outcome) default: abort(); } dstr_putc(&h->scoreboard, ch); - write_scoreboard_char(h, ch); fflush(h->fp); + write_scoreboard_char(h, ch); fflush(h->fmt.fp); } } @@ -430,7 +767,7 @@ static void human_bbench(struct tvec_output *o, struct tvec_state *tv = h->tv; clear_progress(h); - fprintf(h->fp, "%s: %s: ", tv->test->name, ident); fflush(h->fp); + fprintf(h->fmt.fp, "%s: %s: ", tv->test->name, ident); fflush(h->fmt.fp); } static void human_ebench(struct tvec_output *o, @@ -438,7 +775,9 @@ static void human_ebench(struct tvec_output *o, const struct bench_timing *tm) { struct human_output *h = (struct human_output *)o; - tvec_benchreport(&file_printops, h->fp, unit, tm); fputc('\n', h->fp); + + tvec_benchreport(&human_printops, h->fmt.fp, unit, tm); + fputc('\n', h->fmt.fp); } static void human_report(struct tvec_output *o, const char *msg, va_list *ap) @@ -449,14 +788,14 @@ static void human_report(struct tvec_output *o, const char *msg, va_list *ap) dstr_vputf(&d, msg, ap); dstr_putc(&d, '\n'); - clear_progress(h); fflush(h->fp); + clear_progress(h); fflush(h->fmt.fp); fprintf(stderr, "%s: ", QUIS); report_location(h, stderr, tv->infile, tv->lno); fwrite(d.buf, 1, d.len, stderr); if (h->f&HOF_DUPERR) { - report_location(h, h->fp, tv->infile, tv->lno); - fwrite(d.buf, 1, d.len, h->fp); + report_location(h, h->fmt.fp, tv->infile, tv->lno); + fwrite(d.buf, 1, d.len, h->fmt.fp); } show_progress(h); } @@ -465,9 +804,9 @@ static void human_destroy(struct tvec_output *o) { struct human_output *h = (struct human_output *)o; - if (h->f&HOF_DUPERR) fclose(h->fp); + destroy_fmt(&h->fmt, h->f&HOF_DUPERR ? DFF_CLOSE : 0); dstr_destroy(&h->scoreboard); - xfree(h); + xfree(h->outbuf); xfree(h); } static const struct tvec_outops human_ops = { @@ -487,7 +826,8 @@ struct tvec_output *tvec_humanoutput(FILE *fp) h = xmalloc(sizeof(*h)); h->_o.ops = &human_ops; h->f = 0; h->attr = 0; - h->fp = fp; + init_fmt(&h->fmt, fp, 0); + h->outbuf = 0; h->outsz = 0; switch (getenv_boolean("TVEC_TTY", -1)) { case 1: h->f |= HOF_TTY; break; @@ -517,61 +857,27 @@ struct tvec_output *tvec_humanoutput(FILE *fp) struct tap_output { struct tvec_output _o; struct tvec_state *tv; - FILE *fp; - dstr d; + struct format fmt; + char *outbuf; size_t outsz; int maxlen; - unsigned f; -#define TOF_FRESHLINE 1u }; static int tap_writech(void *go, int ch) -{ - struct tap_output *t = go; - - if (t->f&TOF_FRESHLINE) { - if (fputs("## ", t->fp) < 0) return (-1); - t->f &= ~TOF_FRESHLINE; - } - if (putc(ch, t->fp) < 0) return (-1); - if (ch == '\n') t->f |= TOF_FRESHLINE; - return (1); -} + { struct tap_output *t = go; return (format_char(&t->fmt, ch)); } static int tap_writem(void *go, const char *p, size_t sz) -{ - struct tap_output *t = go; - const char *q, *l = p + sz; - size_t n; - - if (p == l) return (0); - if (t->f&TOF_FRESHLINE) - if (fputs("## ", t->fp) < 0) return (-1); - for (;;) { - q = memchr(p, '\n', l - p); if (!q) break; - n = q + 1 - p; if (fwrite(p, 1, n, t->fp) < n) return (-1); - p = q + 1; - if (p == l) { t->f |= TOF_FRESHLINE; return (sz); } - if (fputs("## ", t->fp) < 0) return (-1); - } - n = l - p; if (fwrite(p, 1, n, t->fp) < n) return (-1); - t->f &= ~TOF_FRESHLINE; return (0); -} + { struct human_output *t = go; return (format_string(&t->fmt, p, sz)); } static int tap_nwritef(void *go, size_t maxsz, const char *p, ...) { - struct tap_output *t = go; + struct human_output *t = go; + size_t n; va_list ap; - int n; - - va_start(ap, p); DRESET(&t->d); DENSURE(&t->d, maxsz + 1); -#ifdef HAVE_SNPRINTF - n = vsnprintf(t->d.buf, maxsz + 1, p, ap); -#else - n = vsprintf(t->d.buf, p, ap); -#endif - assert(0 <= n && n <= maxsz); + + va_start(ap, p); + n = gprintf_memputf(&t->outbuf, &t->outsz, maxsz, p, ap); va_end(ap); - return (tap_writem(t, t->d.buf, n)); + return (format_string(&t->fmt, t->outbuf, n)); } static const struct gprintf_ops tap_printops = @@ -582,7 +888,7 @@ 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->fp); + fputs("TAP version 13\n", t->fmt.fp); } static unsigned tap_grpix(struct tap_output *t) @@ -602,11 +908,11 @@ 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->fp); + t->fmt.fp); return (2); } - fprintf(t->fp, "1..%u\n", tap_grpix(t)); + fprintf(t->fmt.fp, "1..%u\n", tap_grpix(t)); t->tv = 0; return (tv->all[TVOUT_LOSE] ? 1 : 0); } @@ -621,9 +927,9 @@ static void tap_skipgroup(struct tvec_output *o, { struct tap_output *t = (struct tap_output *)o; - fprintf(t->fp, "ok %u %s # SKIP", tap_grpix(t), t->tv->test->name); - if (excuse) { fputc(' ', t->fp); vfprintf(t->fp, excuse, *ap); } - fputc('\n', t->fp); + 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); } static void tap_egroup(struct tvec_output *o) @@ -637,39 +943,37 @@ static void tap_egroup(struct tvec_output *o) skip = tv->curr[TVOUT_SKIP]; if (lose) { - fprintf(t->fp, "not ok %u - %s: FAILED %u/%u", + fprintf(t->fmt.fp, "not ok %u - %s: FAILED %u/%u", grpix, tv->test->name, lose, win + lose); - if (skip) fprintf(t->fp, " (skipped %u)", skip); + if (skip) fprintf(t->fmt.fp, " (skipped %u)", skip); } else { - fprintf(t->fp, "ok %u - %s: passed %u", grpix, tv->test->name, win); - if (skip) fprintf(t->fp, " (skipped %u)", skip); + 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->fp); + fputc('\n', t->fmt.fp); } static void tap_btest(struct tvec_output *o) { ; } -static void tap_skip(struct tvec_output *o, const char *excuse, va_list *ap) +static void tap_outcome(struct tvec_output *o, const char *outcome, + const char *detail, va_list *ap) { struct tap_output *t = (struct tap_output *)o; struct tvec_state *tv = t->tv; - fprintf(t->fp, "## %s:%u: `%s' skipped", - tv->infile, tv->test_lno, tv->test->name); - if (excuse) { fputs(": ", t->fp); vfprintf(t->fp, excuse, *ap); } - fputc('\n', t->fp); + 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'); } +static void tap_skip(struct tvec_output *o, const char *excuse, va_list *ap) + { tap_outcome(o, "skipped", excuse, ap); } static void tap_fail(struct tvec_output *o, const char *detail, va_list *ap) -{ - struct tap_output *t = (struct tap_output *)o; - struct tvec_state *tv = t->tv; - - fprintf(t->fp, "## %s:%u: `%s' FAILED", - tv->infile, tv->test_lno, tv->test->name); - if (detail) { fputs(": ", t->fp); vfprintf(t->fp, detail, *ap); } - fputc('\n', t->fp); -} + { tap_outcome(o, "FAILED", detail, ap); } static void tap_dumpreg(struct tvec_output *o, unsigned disp, const union tvec_regval *rv, @@ -678,14 +982,11 @@ static void tap_dumpreg(struct tvec_output *o, struct tap_output *t = (struct tap_output *)o; const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name); - fprintf(t->fp, "## %*s%s %s = ", 10 + t->maxlen - n, "", ds, rd->name); - if (!rv) - fprintf(t->fp, "#\n"); - else { - t->f &= ~TOF_FRESHLINE; - rd->ty->dump(rv, rd, 0, &tap_printops, t); - fputc('\n', t->fp); - } + gprintf(&tap_printops, t, "%*s%s %s = ", + 10 + t->maxlen - n, "", ds, rd->name); + if (!rv) gprintf(&tap_printops, t, "#"); + else rd->ty->dump(rv, rd, 0, &tap_printops, t); + format_char(&t->fmt, '\n'); } static void tap_etest(struct tvec_output *o, unsigned outcome) { ; } @@ -701,38 +1002,43 @@ static void tap_ebench(struct tvec_output *o, struct tap_output *t = (struct tap_output *)o; struct tvec_state *tv = t->tv; - fprintf(t->fp, "## %s: %s: ", tv->test->name, ident); - t->f &= ~TOF_FRESHLINE; tvec_benchreport(&tap_printops, t, unit, tm); - fputc('\n', t->fp); + gprintf(&tap_printops, t, "%s: %s: ", tv->test->name, ident); + tvec_benchreport(&tap_printops, t, unit, tm); + format_char(&t->fmt, '\n'); } -static void tap_report(struct tap_output *t, const char *msg, va_list *ap) +static void tap_report(struct tap_output *t, + const struct gprintf_ops *gops, void *go, + const char *msg, va_list *ap) { struct tvec_state *tv = t->tv; - if (tv->infile) fprintf(t->fp, "%s:%u: ", tv->infile, tv->lno); - vfprintf(t->fp, msg, *ap); fputc('\n', t->fp); + if (tv->infile) gprintf(gops, go, "%s:%u: ", tv->infile, tv->lno); + gprintf(gops, go, msg, ap); gops->putch(go, '\n'); } static void tap_error(struct tvec_output *o, const char *msg, va_list *ap) { struct tap_output *t = (struct tap_output *)o; - fputs("Bail out! ", t->fp); tap_report(t, msg, ap); + + fputs("Bail out! ", t->fmt.fp); + tap_report(t, &file_printops, t->fmt.fp, msg, ap); } static void tap_notice(struct tvec_output *o, const char *msg, va_list *ap) { struct tap_output *t = (struct tap_output *)o; - fputs("## ", t->fp); tap_report(t, msg, ap); + + tap_report(t, &tap_printops, t, msg, ap); } static void tap_destroy(struct tvec_output *o) { struct tap_output *t = (struct tap_output *)o; - if (t->fp != stdout && t->fp != stderr) fclose(t->fp); - dstr_destroy(&t->d); - xfree(t); + destroy_fmt(&t->fmt, + t->fmt.fp == stdout || t->fmt.fp == stderr ? 0 : DFF_CLOSE); + xfree(t->outbuf); xfree(t); } static const struct tvec_outops tap_ops = { @@ -749,8 +1055,8 @@ struct tvec_output *tvec_tapoutput(FILE *fp) struct tap_output *t; t = xmalloc(sizeof(*t)); t->_o.ops = &tap_ops; - dstr_create(&t->d); - t->f = 0; t->fp = fp; + init_fmt(&t->fmt, fp, "## "); + t->outbuf = 0; t->outsz = 0; return (&t->_o); } diff --git a/test/tvec-remote.c b/test/tvec-remote.c index e03297e..a00f046 100644 --- a/test/tvec-remote.c +++ b/test/tvec-remote.c @@ -114,7 +114,7 @@ static int recv_all(struct tvec_state *tv, struct tvec_remote *r, #undef f_any } -int tvec_send(struct tvec_state *tv, struct tvec_reomte *r) +int tvec_send(struct tvec_state *tv, struct tvec_remote *r) { kludge64 k; unsigned char lenbuf[8]; const char *p; size_t sz; @@ -131,7 +131,7 @@ int tvec_send(struct tvec_state *tv, struct tvec_reomte *r) return (0); } -int tvec_recv(struct tvec_state *tv, struct tvec_reomte *r, buf *b_out) +int tvec_recv(struct tvec_state *tv, struct tvec_remote *r, buf *b_out) { kludge64 k, szmax; unsigned char lenbuf[8]; unsigned char *p; @@ -176,6 +176,7 @@ int tvec_recv(struct tvec_state *tv, struct tvec_reomte *r, buf *b_out) /*----- The output driver -------------------------------------------------*/ #define SENDPK(ro, pk) \ + if ((ro)->r.f&TVRF_BROKEN) /* do nothing */; else \ MC_BEFORE(setpk, \ { buf_reset(&(ro)->r.bout); \ buf_putu16l(&(ro)->r.bout.b, (pk)); }) \ @@ -187,29 +188,26 @@ static int sendstr(struct tvec_output *o, unsigned pk, { struct remote_output *ro = (struct remote_output *)o; - if (ro->r.f&TVRF_BROKEN) return (-1); - dbuf_reset(&ro->r.bout); - buf_putu16l(&ro->r.bout.b, TVPK_ERROR); - buf_vputstrf16l(&ro->r.bout.b, msg, ap); - return (tvec_send(ro->_o.tv, &ro->r)); + SENDPK(ro, pk) buf_vputstrf16l(&ro->r.bout.b, msg, ap); + return (ro->r.f&TVRF_BROKEN ? -1 : 0); } -static void report(struct tvec_output *o, unsigned pk, +static void report(struct tvec_output *o, unsigned pk, const char *what, const char *msg, va_list *ap) { if (sendstr(o, pk, msg, ap)) { - fprintf(stderr, "%s: ", QUIS); + fprintf(stderr, "%s %s: ", QUIS, what); vfprintf(stderr, msg, *ap); fputc('\n', stderr); } } static void remote_error(struct tvec_output *o, const char *msg, va_list *ap) - { report(o, TVPK_ERROR, msg, ap); } + { report(o, TVPK_ERROR, "ERROR", msg, ap); } static void remote_notice(struct tvec_output *o, const char *msg, va_list *ap) - { report(o, TVPK_NOTICE, msg, ap); } + { report(o, TVPK_NOTICE, "notice", msg, ap); } static void remote_setstatus(struct tvec_ouptut *o, int st) { diff --git a/test/tvec-types.c b/test/tvec-types.c index 9709b60..695d66d 100644 --- a/test/tvec-types.c +++ b/test/tvec-types.c @@ -42,32 +42,42 @@ # include "base64.h" # include "hex.h" #include "dstr.h" +#include "maths.h" #include "tvec.h" /*----- Preliminary utilities ---------------------------------------------*/ -#ifdef isnan -# define NANP(x) isnan(x) -#else -# define NANP(x) (!((x) == (x))) -#endif - -#ifdef isinf -# define INFP(x) isinf(x) -#else -# define INFP(x) ((x) > DBL_MAX || (x) < DBL_MIN) -#endif - -#ifdef signbit -# define NEGP(x) signbit(x) -#else -# define NEGP(x) ((x) < 0.0) -#endif +/* --- @trivial_release@ --- * + * + * Arguments: @union tvec_regval *rv@ = a register value + * @const struct tvec_regdef@ = the register definition + * + * Returns: --- + * + * Use: Does nothing. Used for register values which don't retain + * resources. + */ static void trivial_release(union tvec_regval *rv, const struct tvec_regdef *rd) { ; } +/*----- Integer utilities -------------------------------------------------*/ + +/* --- @unsigned_to_buf@, @signed_to_buf@ --- * + * + * Arguments: @buf *b@ = buffer to write on + * @unsigned long u@ or @long i@ = integer to write + * + * Returns: Zero on success, @-1@ on failure. + * + * Use: Write @i@ to the buffer, in big-endian (two's-complement, it + * signed) format. + */ + +static int unsigned_to_buf(buf *b, unsigned long u) + { kludge64 k; ASSIGN64(k, u); return (buf_putk64l(b, k)); } + static int signed_to_buf(buf *b, long i) { kludge64 k; @@ -79,23 +89,17 @@ static int signed_to_buf(buf *b, long i) return (buf_putk64l(b, k)); } -static int signed_from_buf(buf *b, long *i_out) -{ - kludge64 k, lmax, not_lmin; - - ASSIGN64(lmax, LONG_MAX); ASSIGN64(not_lmin, ~(unsigned long)LONG_MIN); - if (buf_getk64l(b, &k)) return (-1); - if (CMP64(k, <=, lmax)) *i_out = (long)GET64(unsigned long, k); - else { - CPL64(k, k); - if (CMP64(k, <=, not_lmin)) *i_out = -(long)GET64(unsigned long, k) - 1; - else return (-1); - } - return (0); -} - -static int unsigned_to_buf(buf *b, unsigned long u) - { kludge64 k; ASSIGN64(k, u); return (buf_putk64l(b, k)); } +/* --- @unsigned_from_buf@, @signed_from_buf@ --- * + * + * Arguments: @buf *b@ = buffer to write on + * @unsigned long *u_out@ or @long *i_out@ = where to put the + * result + * + * Returns: Zero on success, @-1@ on failure. + * + * Use: Read an integer, in big-endian (two's-complement, if signed) + * format, from the buffer. + */ static int unsigned_from_buf(buf *b, unsigned long *u_out) { @@ -107,6 +111,15 @@ static int unsigned_from_buf(buf *b, unsigned long *u_out) *u_out = GET64(unsigned long, k); return (0); } +/* --- @hex_width@ --- * + * + * Arguments: @unsigned long u@ = an integer + * + * Returns: A suitable number of digits to use in order to display @u@ in + * hex. Currently, we select a power of two sufficient to show + * the value, but at least 2. + */ + static int hex_width(unsigned long u) { int wd; @@ -116,6 +129,57 @@ static int hex_width(unsigned long u) return (wd/4); } +/* --- @format_unsigned_hex@, @format_signed_hex@ --- * + * + * Arguments: @const struct gprintf_ops *gops@ = print operations + * @void *go@ = print destination + * @unsigned long u@ or @long i@ = integer to print + * + * Returns: --- + * + * Use: Print an unsigned or signed integer in hexadecimal. + */ + +static void format_unsigned_hex(const struct gprintf_ops *gops, void *go, + unsigned long u) + { gprintf(gops, go, "0x%0*lx", hex_width(u), u); } + +static void format_signed_hex(const struct gprintf_ops *gops, void *go, + long i) +{ + unsigned long u = i >= 0 ? i : -(unsigned long)i; + gprintf(gops, go, "%s0x%0*lx", i < 0 ? "-" : "", hex_width(u), u); +} + +static int signed_from_buf(buf *b, long *i_out) +{ + kludge64 k, lmax, not_lmin; + + ASSIGN64(lmax, LONG_MAX); ASSIGN64(not_lmin, ~(unsigned long)LONG_MIN); + if (buf_getk64l(b, &k)) return (-1); + if (CMP64(k, <=, lmax)) *i_out = (long)GET64(unsigned long, k); + else { + CPL64(k, k); + if (CMP64(k, <=, not_lmin)) *i_out = -(long)GET64(unsigned long, k) - 1; + else return (-1); + } + return (0); +} + +/* --- @check_unsigned_range@, @check_signed_range@ --- * + * + * Arguments: @unsigned long u@ or @long i@ = an integer + * @const struct tvec_urange *ur@ or + * @const struct tvec_irange *ir@ = range specification, + * or null + * @struct tvec_state *tv@ = test vector state + * + * Returns: Zero on success, or @-1@ on error. + * + * Use: Check that the integer is within bounds. If not, report a + * suitable error and return a failure indication. + */ + static int check_signed_range(long i, const struct tvec_irange *ir, struct tvec_state *tv) @@ -140,6 +204,15 @@ static int check_unsigned_range(unsigned long u, return (0); } +/* --- @chtodig@ --- * + * + * Arguments: @int ch@ = a character + * + * Returns: The numeric value of the character as a digit, or @-1@ if + * it's not a digit. Letters count as extended digits starting + * with value 10; case is not significant. + */ + static int chtodig(int ch) { if ('0' <= ch && ch <= '9') return (ch - '0'); @@ -148,6 +221,29 @@ static int chtodig(int ch) else return (-1); } +/* --- @parse_unsigned_integer@, @parse_signed_integer@ --- * + * + * Arguments: @unsigned long *u_out@, @long *i_out@ = where to put the + * result + * @const char **q_out@ = where to put the end position + * @const char *p@ = pointer to the string to parse + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Parse an integer from a string in the test-vector format. + * This is mostly extension of the traditional C @strtoul@ + * format: supported inputs include: + * + * * NNN -- a decimal number (even if it starts with `0'); + * * 0xNNN -- hexadecimal; + * * 0oNNN -- octal; + * * 0bNNN -- binary; + * * NNrNNN -- base NN. + * + * Furthermore, single underscores are permitted internally as + * an insignificant digit separator. + */ + static int parse_unsigned_integer(unsigned long *u_out, const char **q_out, const char *p) { @@ -155,10 +251,15 @@ static int parse_unsigned_integer(unsigned long *u_out, const char **q_out, int ch, d, r; const char *q; unsigned f = 0; -#define f_implicit 1u -#define f_digit 2u -#define f_uscore 4u - +#define f_implicit 1u /* implicitly reading base 10 */ +#define f_digit 2u /* read a real digit */ +#define f_uscore 4u /* found an underscore */ + + /* Initial setup + * + * This will deal with the traditional `0[box]...' prefixes. We'll leave + * our new `NNr...' syntax for later. + */ if (p[0] != '0' || !p[1]) { d = chtodig(*p); if (0 > d || d >= 10) return (-1); r = 10; u = d; p++; f |= f_implicit | f_digit; @@ -173,24 +274,38 @@ static int parse_unsigned_integer(unsigned long *u_out, const char **q_out, q = p; for (;;) { - ch = *p; - if (ch == '_') { - if (f&f_uscore) break; - p++; f = (f&~f_implicit) | f_uscore; - } - else if (ch == 'r' || ch == 'R') { - if (!(f&f_implicit) || !u || u >= 36) break; - d = chtodig(p[1]); if (0 > d || d >= u) break; - r = u; u = d; f = (f&~f_implicit) | f_digit; p += 2; q = p; - } else { - d = chtodig(ch); - if (d < 0 || d >= r) break; - if (u > ULONG_MAX/r) return (-1); - u *= r; if (u > ULONG_MAX - d) return (-1); - u += d; f = (f&~f_uscore) | f_digit; p++; q = p; + /* Work through the string a character at a time. */ + + ch = *p; switch (ch) { + + case '_': + /* An underscore is OK if we haven't just seen one. */ + + if (f&f_uscore) goto done; + p++; f = (f&~f_implicit) | f_uscore; + break; + + case 'r': case 'R': + /* An `r' is OK if the number so far is small enough to be a sensible + * base, and we're scanning decimal implicitly. + */ + + if (!(f&f_implicit) || !u || u >= 36) goto done; + d = chtodig(p[1]); if (0 > d || d >= u) goto done; + r = u; u = d; f = (f&~f_implicit) | f_digit; p += 2; q = p; + break; + + default: + /* Otherwise we expect a valid digit and accumulate it. */ + d = chtodig(ch); if (d < 0 || d >= r) goto done; + if (u > ULONG_MAX/r) return (-1); + u *= r; if (u > ULONG_MAX - d) return (-1); + u += d; f = (f&~f_uscore) | f_digit; p++; q = p; + break; } } +done: if (!(f&f_digit)) return (-1); *u_out = u; *q_out = q; return (0); @@ -206,11 +321,14 @@ static int parse_signed_integer(long *i_out, const char **q_out, unsigned f = 0; #define f_neg 1u + /* Read an initial sign. */ if (*p == '+') p++; else if (*p == '-') { f |= f_neg; p++; } + /* Scan an unsigned number. */ if (parse_unsigned_integer(&u, q_out, p)) return (-1); + /* Check for signed overflow and apply the sign. */ if (!(f&f_neg)) { if (u > LONG_MAX) return (-1); *i_out = u; @@ -224,6 +342,38 @@ static int parse_signed_integer(long *i_out, const char **q_out, #undef f_neg } +/* --- @parse_unsigned@, @parse_signed@ --- * + * + * Arguments: @unsigned long *u_out@ or @long *i_out@ = where to put the + * result + * @const char *p@ = string to parse + * @const struct tvec_urange *ur@ or + * @const struct tvec_irange *ir@ = range specification, + * or null + * @struct tvec_state *tv@ = test vector state + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Parse and range-check an integer. Unlike @parse_(un)signed_ + * integer@, these functions check that there's no cruft + * following the final digit, and report errors as they find + * them rather than leaving that to the caller. + */ + +static int parse_unsigned(unsigned long *u_out, const char *p, + const struct tvec_urange *ur, + struct tvec_state *tv) +{ + unsigned long u; + const char *q; + + if (parse_unsigned_integer(&u, &q, p)) + return (tvec_error(tv, "invalid unsigned integer `%s'", p)); + if (*q) return (tvec_syntax(tv, *q, "end-of-line")); + if (check_unsigned_range(u, ur, tv)) return (-1); + *u_out = u; return (0); +} + static int parse_signed(long *i_out, const char *p, const struct tvec_irange *ir, struct tvec_state *tv) @@ -238,30 +388,47 @@ static int parse_signed(long *i_out, const char *p, *i_out = i; return (0); } -static int parse_unsigned(unsigned long *u_out, const char *p, - const struct tvec_urange *ur, - struct tvec_state *tv) -{ - unsigned long u; - const char *q; +/*----- Floating-point utilities ------------------------------------------*/ - if (parse_unsigned_integer(&u, &q, p)) - return (tvec_error(tv, "invalid unsigned integer `%s'", p)); - if (*q) return (tvec_syntax(tv, *q, "end-of-line")); - if (check_unsigned_range(u, ur, tv)) return (-1); - *u_out = u; return (0); -} +/* --- @eqish_floating_p@ --- * + * + * Arguments: @double x, y@ = two numbers to compare + * @const struct tvec_floatinfo *fi@ = floating-point info + * + * Returns: Nonzero if the comparand @y@ is sufficiently close to the + * reference @x@, or zero if it's definitely different. + */ -static void format_signed_hex(const struct gprintf_ops *gops, void *go, - long i) +static int eqish_floating_p(double x, double y, + const struct tvec_floatinfo *fi) { - unsigned long u = i >= 0 ? i : -(unsigned long)i; - gprintf(gops, go, "%s0x%0*lx", i < 0 ? "-" : "", hex_width(u), u); + double t; + + if (NANP(x)) return (NANP(y)); else if (NANP(y)) return (0); + if (INFP(x)) return (x == y); else if (INFP(y)) return (0); + + switch (fi ? fi->f&TVFF_EQMASK : TVFF_EXACT) { + case TVFF_EXACT: + return (x == y && NEGP(x) == NEGP(y)); + case TVFF_ABSDELTA: + t = x - y; if (t < 0) t = -t; return (t < fi->delta); + case TVFF_RELDELTA: + t = 1.0 - y/x; if (t < 0) t = -t; return (t < fi->delta); + default: + abort(); + } } -static void format_unsigned_hex(const struct gprintf_ops *gops, void *go, - unsigned long u) - { gprintf(gops, go, "0x%0*lx", hex_width(u), u); } +/* --- @format_floating@ --- * + * + * Arguments: @const struct gprintf_ops *gops@ = print operations + * @void *go@ = print destination + * @double x@ = number to print + * + * Returns: --- + * + * Use: Print a floating-point number, accurately. + */ static void format_floating(const struct gprintf_ops *gops, void *go, double x) @@ -355,25 +522,18 @@ static void format_floating(const struct gprintf_ops *gops, void *go, } } -static int eqish_floating_p(double x, double y, - const struct tvec_floatinfo *fi) -{ - double t; - - if (NANP(x)) return (NANP(y)); else if (NANP(y)) return (0); - if (INFP(x)) return (x == y); else if (INFP(y)) return (0); - - switch (fi ? fi->f&TVFF_EQMASK : TVFF_EXACT) { - case TVFF_EXACT: - return (x == y && NEGP(x) == NEGP(y)); - case TVFF_ABSDELTA: - t = x - y; if (t < 0) t = -t; return (t < fi->delta); - case TVFF_RELDELTA: - t = 1.0 - y/x; if (t < 0) t = -t; return (t < fi->delta); - default: - abort(); - } -} +/* --- @parse_floating@ --- * + * + * Arguments: @double *x_out@ = where to put the result + * @const char *p@ = string to parse + * @const struct tvec_floatinfo *fi@ = floating-point info + * @struct tvec_state *tv@ = test vector state + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Parse a floating-point number from a string. Reports any + * necessary errors. + */ static int parse_floating(double *x_out, const char *p, const struct tvec_floatinfo *fi, @@ -384,6 +544,7 @@ static int parse_floating(double *x_out, const char *p, double x; int olderr, rc; + /* Check for special tokens. */ if (STRCMP(p, ==, "#nan")) { #ifdef NAN x = NAN; rc = 0; @@ -391,24 +552,31 @@ static int parse_floating(double *x_out, const char *p, tvec_error(tv, "NaN not supported on this system"); rc = -1; goto end; #endif - } else if (STRCMP(p, ==, "#inf") || - STRCMP(p, ==, "#+inf") || - STRCMP(p, ==, "+#inf")) { + } + + else if (STRCMP(p, ==, "#inf") || + STRCMP(p, ==, "#+inf") || STRCMP(p, ==, "+#inf")) { #ifdef INFINITY x = INFINITY; rc = 0; #else tvec_error(tv, "infinity not supported on this system"); rc = -1; goto end; #endif - } else if (STRCMP(p, ==, "#-inf") || - STRCMP(p, ==, "-#inf")) { + } + + else if (STRCMP(p, ==, "#-inf") || STRCMP(p, ==, "-#inf")) { #ifdef INFINITY x = -INFINITY; rc = 0; #else tvec_error(tv, "infinity not supported on this system"); rc = -1; goto end; #endif - } else { + } + + /* Check that this looks like a number, so we can exclude `strtod' + * recognizing its own non-finite number tokens. + */ + else { pp = p; if (*pp == '+' || *pp == '-') pp++; if (*pp == '.') pp++; @@ -416,6 +584,8 @@ static int parse_floating(double *x_out, const char *p, tvec_syntax(tv, *p ? *p : fgetc(tv->fp), "floating-point number"); rc = -1; goto end; } + + /* Parse the number using the system parser. */ olderr = errno; errno = 0; x = strtod(p, &q); if (*q) { @@ -430,10 +600,12 @@ static int parse_floating(double *x_out, const char *p, errno = olderr; } + /* Check that the number is acceptable. */ if (NANP(x) && fi && !(fi->f&TVFF_NANOK)) { tvec_error(tv, "#nan not allowed here"); rc = -1; goto end; } + if (fi && ((!(fi->f&TVFF_NOMIN) && x < fi->min) || (!(fi->f&TVFF_NOMAX) && x > fi->max))) { dstr_puts(&d, "floating-point number "); @@ -452,21 +624,214 @@ static int parse_floating(double *x_out, const char *p, tvec_error(tv, "%s", d.buf); rc = -1; goto end; } - *x_out = x; rc = 0; -end: - dstr_destroy(&d); - return (rc); + /* All done. */ + *x_out = x; rc = 0; +end: + dstr_destroy(&d); + return (rc); +} + +/*----- String utilities --------------------------------------------------*/ + +/* Special character name table. */ +static const struct chartab { + const char *name; /* character name */ + int ch; /* character value */ + unsigned f; /* flags: */ +#define CTF_PREFER 1u /* preferred name */ +#define CTF_SHORT 2u /* short name (compact style) */ +} chartab[] = { + { "#eof", EOF, CTF_PREFER | CTF_SHORT }, + { "#nul", '\0', CTF_PREFER }, + { "#bell", '\a', CTF_PREFER }, + { "#ding", '\a', 0 }, + { "#bel", '\a', CTF_SHORT }, + { "#backspace", '\b', CTF_PREFER }, + { "#bs", '\b', CTF_SHORT }, + { "#escape", '\x1b', CTF_PREFER }, + { "#esc", '\x1b', CTF_SHORT }, + { "#formfeed", '\f', CTF_PREFER }, + { "#ff", '\f', CTF_SHORT }, + { "#newline", '\n', CTF_PREFER }, + { "#linefeed", '\n', 0 }, + { "#lf", '\n', CTF_SHORT }, + { "#nl", '\n', 0 }, + { "#return", '\r', CTF_PREFER }, + { "#carriage-return", '\r', 0 }, + { "#cr", '\r', CTF_SHORT }, + { "#tab", '\t', CTF_PREFER | CTF_SHORT }, + { "#horizontal-tab", '\t', 0 }, + { "#ht", '\t', 0 }, + { "#vertical-tab", '\v', CTF_PREFER }, + { "#vt", '\v', CTF_SHORT }, + { "#space", ' ', 0 }, + { "#spc", ' ', CTF_SHORT }, + { "#delete", '\x7f', CTF_PREFER }, + { "#del", '\x7f', CTF_SHORT }, + { 0, 0, 0 } +}; + +/* --- @find_charname@ --- * + * + * Arguments: @int ch@ = character to match + * @unsigned f@ = flags (@CTF_...@) to match + * + * Returns: The name of the character, or null if no match is found. + * + * Use: Looks up a name for a character. Specifically, it returns + * the first entry in the @chartab@ table which matches @ch@ and + * which has one of the flags @f@ set. + */ + +static const char *find_charname(int ch, unsigned f) +{ + const struct chartab *ct; + + for (ct = chartab; ct->name; ct++) + if (ct->ch == ch && (ct->f&f)) return (ct->name); + return (0); +} + +/* --- @read_charname@ --- * + * + * Arguments: @int *ch_out@ = where to put the character + * @const char *p@ = character name + * @unsigned f@ = flags (@TCF_...@) + * + * Returns: Zero if a match was found, @-1@ if not. + * + * Use: Looks up a character by name. If @RCF_EOFOK@ is set in @f@, + * then the @EOF@ marker can be matched; otherwise it can't. + */ + +#define RCF_EOFOK 1u +static int read_charname(int *ch_out, const char *p, unsigned f) +{ + const struct chartab *ct; + + for (ct = chartab; ct->name; ct++) + if (STRCMP(p, ==, ct->name) && ((f&RCF_EOFOK) || ct->ch >= 0)) + { *ch_out = ct->ch; return (0); } + return (-1); +} + +/* --- @format_charesc@ --- * + * + * Arguments: @const struct gprintf_ops *gops@ = print operations + * @void *go@ = print destination + * @int ch@ = character to format + * @unsigned f@ = flags (@FCF_...@) + * + * Returns: --- + * + * Use: Format a character as an escape sequence, possibly as part of + * a larger string. If @FCF_BRACE@ is set in @f@, then put + * braces around a `\x...' code, so that it's suitable for use + * in a longer string. + */ + +#define FCF_BRACE 1u +static void format_charesc(const struct gprintf_ops *gops, void *go, + int ch, unsigned f) +{ + switch (ch) { + case '\a': gprintf(gops, go, "\\a"); break; + case '\b': gprintf(gops, go, "\\b"); break; + case '\x1b': gprintf(gops, go, "\\e"); break; + case '\f': gprintf(gops, go, "\\f"); break; + case '\r': gprintf(gops, go, "\\r"); break; + case '\n': gprintf(gops, go, "\\n"); break; + case '\t': gprintf(gops, go, "\\t"); break; + case '\v': gprintf(gops, go, "\\v"); break; + case '\\': gprintf(gops, go, "\\\\"); break; + case '\'': gprintf(gops, go, "\\'"); break; + case '\0': + if (f&FCF_BRACE) gprintf(gops, go, "\\{0}"); + else gprintf(gops, go, "\\0"); + break; + default: + if (f&FCF_BRACE) + gprintf(gops, go, "\\x{%0*x}", hex_width(UCHAR_MAX), ch); + else + gprintf(gops, go, "\\x%0*x", hex_width(UCHAR_MAX), ch); + break; + } +} + +/* --- @format_char@ --- * + * + * Arguments: @const struct gprintf_ops *gops@ = print operations + * @void *go@ = print destination + * @int ch@ = character to format + * + * Returns: --- + * + * Use: Format a single character. + */ + +static void format_char(const struct gprintf_ops *gops, void *go, int ch) +{ + switch (ch) { + case '\\': case '\'': escape: + gprintf(gops, go, "'"); + format_charesc(gops, go, ch, 0); + gprintf(gops, go, "'"); + break; + default: + if (!isprint(ch)) goto escape; + gprintf(gops, go, "'%c'", ch); + break; + } +} + +/* --- @maybe_format_unsigned_char@, @maybe_format_signed_char@ --- * + * + * Arguments: @const struct gprintf_ops *gops@ = print operations + * @void *go@ = print destination + * @unsigned long u@ or @long i@ = an integer + * + * Returns: --- + * + * Use: Format a (signed or unsigned) integer as a character, if it's + * in range, printing something like `= 'q''. It's assumed that + * a comment marker has already been output. + */ + +static void maybe_format_unsigned_char + (const struct gprintf_ops *gops, void *go, unsigned long u) +{ + const char *p; + + p = find_charname(u, CTF_PREFER); + if (p) gprintf(gops, go, " = %s", p); + if (u < UCHAR_MAX) + { gprintf(gops, go, " = "); format_char(gops, go, u); } } -static int convert_hex(char ch, int *v_out) +static void maybe_format_signed_char + (const struct gprintf_ops *gops, void *go, long i) { - if ('0' <= ch && ch <= '9') { *v_out = ch - '0'; return (0); } - else if ('a' <= ch && ch <= 'f') { *v_out = ch - 'a' + 10; return (0); } - else if ('A' <= ch && ch <= 'F') { *v_out = ch - 'A' + 10; return (0); } - else return (-1); + const char *p; + + p = find_charname(i, CTF_PREFER); + if (p) gprintf(gops, go, " = %s", p); + if (0 <= i && i < UCHAR_MAX) + { gprintf(gops, go, " = "); format_char(gops, go, i); } } -static int read_escape(int *ch_out, struct tvec_state *tv) +/* --- @read_charesc@ --- * + * + * Arguments: @int *ch_out@ = where to put the result + * @struct tvec_state *tv@ = test vector state + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Parse and convert an escape sequence from @tv@'s input + * stream, assuming that the initial `\' has already been read. + * Reports errors as appropriate. + */ + +static int read_charesc(int *ch_out, struct tvec_state *tv) { int ch, i, esc; unsigned f = 0; @@ -474,7 +839,11 @@ static int read_escape(int *ch_out, struct tvec_state *tv) ch = getc(tv->fp); switch (ch) { - case EOF: case '\n': tvec_syntax(tv, ch, "string escape"); + + /* Things we shouldn't find. */ + case EOF: case '\n': return (tvec_syntax(tv, ch, "string escape")); + + /* Single-character escapes. */ case '\'': *ch_out = '\''; break; case '\\': *ch_out = '\\'; break; case '"': *ch_out = '"'; break; @@ -487,15 +856,16 @@ static int read_escape(int *ch_out, struct tvec_state *tv) case 't': *ch_out = '\t'; break; case 'v': *ch_out = '\v'; break; + /* Hex escapes, with and without braces. */ case 'x': ch = getc(tv->fp); if (ch == '{') { f |= f_brace; ch = getc(tv->fp); } else f &= ~f_brace; - if (convert_hex(ch, &esc)) - return (tvec_syntax(tv, ch, "hex digit")); + esc = chtodig(ch); + if (esc < 0 || esc >= 16) return (tvec_syntax(tv, ch, "hex digit")); for (;;) { - ch = getc(tv->fp); if (convert_hex(ch, &i)) break; - esc = 8*esc + i; + ch = getc(tv->fp); i = chtodig(ch); if (i < 0 || i >= 16) break; + esc = 16*esc + i; if (esc > UCHAR_MAX) return (tvec_error(tv, "character code %d out of range", esc)); @@ -505,6 +875,10 @@ static int read_escape(int *ch_out, struct tvec_state *tv) *ch_out = esc; break; + /* Other things, primarily octal escapes. */ + case '{': + f |= f_brace; ch = getc(tv->fp); + /* fall through */ default: if ('0' <= ch && ch < '8') { i = 1; esc = ch - '0'; @@ -514,19 +888,39 @@ static int read_escape(int *ch_out, struct tvec_state *tv) esc = 8*esc + ch - '0'; i++; if (i >= 3) break; } + if (f&f_brace) { + ch = getc(tv->fp); + if (ch != '}') return (tvec_syntax(tv, ch, "`}'")); + } if (esc > UCHAR_MAX) return (tvec_error(tv, "character code %d out of range", esc)); - *ch_out = esc; + *ch_out = esc; break; } else return (tvec_syntax(tv, ch, "string escape")); } + /* Done. */ return (0); #undef f_brace } +/* --- @read_quoted_string@ --- * + * + * Arguments: @dstr *d@ = string to write to + * @int quote@ = initial quote, `'' or `"' + * @struct tvec_state *tv@ = test vector state + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Read the rest of a quoted string into @d@, reporting errors + * as appropriate. + * + * A single-quoted string is entirely literal. A double-quoted + * string may contain C-like escapes. + */ + static int read_quoted_string(dstr *d, int quote, struct tvec_state *tv) { int ch; @@ -539,7 +933,7 @@ static int read_quoted_string(dstr *d, int quote, struct tvec_state *tv) case '\\': if (quote == '\'') goto ordinary; ch = getc(tv->fp); if (ch == '\n') { tv->lno++; break; } - ungetc(ch, tv->fp); if (read_escape(&ch, tv)) return (-1); + ungetc(ch, tv->fp); if (read_charesc(&ch, tv)) return (-1); goto ordinary; default: if (ch == quote) goto end; @@ -554,58 +948,16 @@ end: return (0); } -#define FCF_BRACE 1u -static void format_charesc(const struct gprintf_ops *gops, void *go, - int ch, unsigned f) -{ - switch (ch) { - case '\a': gprintf(gops, go, "\\a"); break; - case '\b': gprintf(gops, go, "\\b"); break; - case '\x1b': gprintf(gops, go, "\\e"); break; - case '\f': gprintf(gops, go, "\\f"); break; - case '\r': gprintf(gops, go, "\\r"); break; - case '\n': gprintf(gops, go, "\\n"); break; - case '\t': gprintf(gops, go, "\\t"); break; - case '\v': gprintf(gops, go, "\\v"); break; - case '\'': gprintf(gops, go, "\\'"); break; - case '"': gprintf(gops, go, "\\\""); break; - default: - if (f&FCF_BRACE) - gprintf(gops, go, "\\x{%0*x}", hex_width(UCHAR_MAX), ch); - else - gprintf(gops, go, "\\x%0*x", hex_width(UCHAR_MAX), ch); - break; - } -} - -static void format_char(const struct gprintf_ops *gops, void *go, int ch) -{ - if (ch == EOF) - gprintf(gops, go, "#eof"); - else if (isprint(ch) && ch != '\'') - gprintf(gops, go, "'%c'", ch); - else { - gprintf(gops, go, "'"); - format_charesc(gops, go, ch, 0); - gprintf(gops, go, "'"); - } -} - -static void maybe_format_signed_char - (const struct gprintf_ops *gops, void *go, long i) -{ - if (i == EOF || (0 <= i && i < UCHAR_MAX)) - { gprintf(gops, go, " = "); format_char(gops, go, i); } -} - -static void maybe_format_unsigned_char - (const struct gprintf_ops *gops, void *go, unsigned long u) -{ - if (u < UCHAR_MAX) - { gprintf(gops, go, " = "); format_char(gops, go, u); } -} - -enum { TVCODE_BARE, TVCODE_HEX, TVCODE_BASE64, TVCODE_BASE32 }; +/* --- @collect_bare@ --- * + * + * Arguments: @dstr *d@ = string to write to + * @struct tvec_state *tv@ = test vector state + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Read barewords and the whitespace between them. Stop when we + * encounter something which can't start a bareword. + */ static int collect_bare(dstr *d, struct tvec_state *tv) { @@ -625,7 +977,7 @@ static int collect_bare(dstr *d, struct tvec_state *tv) ungetc(ch, tv->fp); if (tvec_nexttoken(tv)) { rc = -1; goto end; } DPUTC(d, ' '); s = SPACE; break; - case '"': case '\'': case '!': + case '"': case '\'': case '!': case '#': case ')': case '}': case ']': if (s == SPACE) { ungetc(ch, tv->fp); goto done; } goto addch; case '\\': @@ -649,6 +1001,22 @@ end: return (rc); } +/* --- @set_up_encoding@ --- * + * + * Arguments: @const codec_class **ccl_out@ = where to put the class + * @unsigned *f_out@ = where to put the flags + * @unsigned code@ = the coding scheme to use (@TVEC_...@) + * + * Returns: --- + * + * Use: Helper for @read_compound_string@ below. + * + * Return the appropriate codec class and flags for @code@. + * Leaves @*ccl_out@ null if the coding scheme doesn't have a + * backing codec class (e.g., @TVCODE_BARE@). + */ + +enum { TVCODE_BARE, TVCODE_HEX, TVCODE_BASE64, TVCODE_BASE32 }; static void set_up_encoding(const codec_class **ccl_out, unsigned *f_out, unsigned code) { @@ -670,63 +1038,205 @@ static void set_up_encoding(const codec_class **ccl_out, unsigned *f_out, } } +/* --- @flush_codec@ --- * + * + * Arguments: @codec *cdc@ = a codec, or null + * @dstr *d@ = output string + * @struct tvec_state *tv@ = test vector state + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Helper for @read_compound_string@ below. + * + * Flush out any final buffered material from @cdc@, and check + * that it's in a good state. Frees the codec on success. Does + * nothing if @cdc@ is null. + */ + +static int flush_codec(codec *cdc, dstr *d, struct tvec_state *tv) +{ + int err; + + if (cdc) { + err = cdc->ops->code(cdc, 0, 0, d); + if (err) + return (tvec_error(tv, "invalid %s sequence end: %s", + cdc->ops->c->name, codec_strerror(err))); + cdc->ops->destroy(cdc); + } + return (0); +} + +/* --- @read_compound_string@ --- * + * + * Arguments: @void **p_inout@ = address of output buffer pointer + * @size_t *sz_inout@ = address of buffer size + * @unsigned code@ = initial interpretation of barewords + * @unsigned f@ = other flags (@RCSF_...@) + * @struct tvec_state *tv@ = test vector state + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Parse a compound string, i.e., a sequence of stringish pieces + * which might be quoted strings, character names, or barewords + * to be decoded accoding to @code@, interspersed with + * additional directives. + * + * If the initial buffer pointer is non-null and sufficiently + * large, then it will be reused; otherwise, it is freed and a + * fresh, sufficiently large buffer is allocated and returned. + */ + +#define RCSF_NESTED 1u static int read_compound_string(void **p_inout, size_t *sz_inout, - unsigned code, struct tvec_state *tv) + unsigned code, unsigned f, + struct tvec_state *tv) { - const codec_class *ccl; unsigned f; + const codec_class *ccl; unsigned cdf; codec *cdc; dstr d = DSTR_INIT, w = DSTR_INIT; char *p; + const char *q; + void *pp = 0; size_t sz; + unsigned long n; int ch, err, rc; - set_up_encoding(&ccl, &f, code); - if (tvec_nexttoken(tv)) tvec_syntax(tv, fgetc(tv->fp), "string"); + set_up_encoding(&ccl, &cdf, code); cdc = 0; + + if (tvec_nexttoken(tv)) return (tvec_syntax(tv, fgetc(tv->fp), "string")); do { ch = getc(tv->fp); - if (ch == '"' || ch == '\'') - { if (read_quoted_string(&d, ch, tv)) { rc = -1; goto end; } } - else if (ch == '!') { - ungetc(ch, tv->fp); - DRESET(&w); tvec_readword(tv, &w, ";", "`!'-keyword"); - if (STRCMP(w.buf, ==, "!bare")) code = TVCODE_BARE; - else if (STRCMP(w.buf, ==, "!hex")) code = TVCODE_HEX; - else if (STRCMP(w.buf, ==, "!base32")) code = TVCODE_BASE32; - else if (STRCMP(w.buf, ==, "!base64")) code = TVCODE_BASE64; - else { - tvec_error(tv, "unknown string keyword `%s'", w.buf); - rc = -1; goto end; - } - set_up_encoding(&ccl, &f, code); - } else if (ccl) { - ungetc(ch, tv->fp); - DRESET(&w); - if (tvec_readword(tv, &w, ";", "%s-encoded fragment", ccl->name)) - { rc = -1; goto end; } - cdc = ccl->decoder(f); - err = cdc->ops->code(cdc, w.buf, w.len, &d); - if (!err) err = cdc->ops->code(cdc, 0, 0, &d); - if (err) { - tvec_error(tv, "invalid %s fragment `%s': %s", - ccl->name, w.buf, codec_strerror(err)); - rc = -1; goto end; - } - cdc->ops->destroy(cdc); - } else switch (code) { - case TVCODE_BARE: + switch (ch) { + + case ')': case ']': case '}': + /* Close brackets. Leave these for recursive caller if there is one, + * or just complain. + */ + + if (!(f&RCSF_NESTED)) + { rc = tvec_syntax(tv, ch, "string"); goto end; } + ungetc(ch, tv->fp); goto done; + + case '"': case '\'': + /* Quotes. Read a quoted string. */ + + if (cdc && flush_codec(cdc, &d, tv)) { rc = -1; goto end; } + cdc = 0; + if (read_quoted_string(&d, ch, tv)) { rc = -1; goto end; } + break; + + case '#': + /* A named character. */ + + ungetc(ch, tv->fp); + if (cdc && flush_codec(cdc, &d, tv)) { rc = -1; goto end; } + cdc = 0; + DRESET(&w); tvec_readword(tv, &w, ";", "character name"); + if (read_charname(&ch, w.buf, RCF_EOFOK)) { + rc = tvec_error(tv, "unknown character name `%s'", d.buf); + goto end; + } + DPUTC(&d, ch); break; + + case '!': + /* A magic keyword. */ + + if (cdc && flush_codec(cdc, &d, tv)) { rc = -1; goto end; } + cdc = 0; ungetc(ch, tv->fp); - if (collect_bare(&d, tv)) goto done; + DRESET(&w); tvec_readword(tv, &w, ";", "`!'-keyword"); + + /* Change bareword coding system. */ + if (STRCMP(w.buf, ==, "!bare")) + { code = TVCODE_BARE; set_up_encoding(&ccl, &cdf, code); } + else if (STRCMP(w.buf, ==, "!hex")) + { code = TVCODE_HEX; set_up_encoding(&ccl, &cdf, code); } + else if (STRCMP(w.buf, ==, "!base32")) + { code = TVCODE_BASE32; set_up_encoding(&ccl, &cdf, code); } + else if (STRCMP(w.buf, ==, "!base64")) + { code = TVCODE_BASE64; set_up_encoding(&ccl, &cdf, code); } + + /* Repeated substrings. */ + else if (STRCMP(w.buf, ==, "!repeat")) { + if (tvec_nexttoken(tv)) { + rc = tvec_syntax(tv, fgetc(tv->fp), "repeat count"); + goto end; + } + DRESET(&w); + if (tvec_readword(tv, &w, ";{", "repeat count")) + { rc = -1; goto end; } + if (parse_unsigned_integer(&n, &q, w.buf)) { + rc = tvec_error(tv, "invalid repeat count `%s'", w.buf); + goto end; + } + if (*q) { rc = tvec_syntax(tv, *q, "`{'"); goto end; } + if (tvec_nexttoken(tv)) + { rc = tvec_syntax(tv, fgetc(tv->fp), "`{'"); goto end; } + ch = getc(tv->fp); if (ch != '{') + { rc = tvec_syntax(tv, ch, "`{'"); goto end; } + sz = 0; + if (read_compound_string(&pp, &sz, code, f | RCSF_NESTED, tv)) + { rc = -1; goto end; } + ch = getc(tv->fp); if (ch != '}') + { rc = tvec_syntax(tv, ch, "`}'"); goto end; } + if (sz) { + if (n > (size_t)-1/sz) + { rc = tvec_error(tv, "repeat size out of range"); goto end; } + dstr_ensure(&d, n*sz); + if (sz == 1) + { memset(d.buf + d.len, *(unsigned char *)pp, n); d.len += n; } + else + for (; n--; d.len += sz) memcpy(d.buf + d.len, pp, sz); + } + xfree(pp); pp = 0; + } + + /* Anything else is an error. */ + else { + tvec_error(tv, "unknown string keyword `%s'", w.buf); + rc = -1; goto end; + } break; + default: - abort(); + /* A bareword. Process it according to the current coding system. */ + + switch (code) { + case TVCODE_BARE: + ungetc(ch, tv->fp); + if (collect_bare(&d, tv)) goto done; + break; + default: + assert(ccl); + ungetc(ch, tv->fp); DRESET(&w); + if (tvec_readword(tv, &w, ";", "%s-encoded fragment", ccl->name)) + { rc = -1; goto end; } + if (!cdc) cdc = ccl->decoder(cdf); + err = cdc->ops->code(cdc, w.buf, w.len, &d); + if (err) { + tvec_error(tv, "invalid %s fragment `%s': %s", + ccl->name, w.buf, codec_strerror(err)); + rc = -1; goto end; + } + break; + } + break; } } while (!tvec_nexttoken(tv)); done: + /* Wrap things up. */ + if (cdc && flush_codec(cdc, &d, tv)) { rc = -1; goto end; } + cdc = 0; if (*sz_inout <= d.len) { xfree(*p_inout); *p_inout = xmalloc(d.len + 1); } p = *p_inout; memcpy(p, d.buf, d.len); p[d.len] = 0; *sz_inout = d.len; rc = 0; + end: + /* Clean up any debris. */ + if (cdc) cdc->ops->destroy(cdc); + if (pp) xfree(pp); dstr_destroy(&d); dstr_destroy(&w); return (rc); } @@ -880,6 +1390,23 @@ const struct tvec_urange tvrange_u16 = { 0, 65535 }, tvrange_u32 = { 0, 4294967296 }; +/* --- @tvec_claimeq_int@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @long i0, i1@ = two signed integers + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *expr@ = the expression to quote on failure + * + * Returns: Nonzero if @i0@ and @i1@ are equal, otherwise zero. + * + * Use: Check that values of @i0@ and @i1@ are equal. As for + * @tvec_claim@ above, a test case is automatically begun and + * ended if none is already underway. If the values are + * unequal, then @tvec_fail@ is called, quoting @expr@, and the + * mismatched values are dumped: @i0@ is printed as the output + * value and @i1@ is printed as the input reference. + */ + int tvec_claimeq_int(struct tvec_state *tv, long i0, long i1, const char *file, unsigned lno, const char *expr) { @@ -887,6 +1414,23 @@ int tvec_claimeq_int(struct tvec_state *tv, long i0, long i1, return (tvec_claimeq(tv, &tvty_int, 0, file, lno, expr)); } +/* --- @tvec_claimeq_uint@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @unsigned long u0, u1@ = two unsigned integers + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *expr@ = the expression to quote on failure + * + * Returns: Nonzero if @u0@ and @u1@ are equal, otherwise zero. + * + * Use: Check that values of @u0@ and @u1@ are equal. As for + * @tvec_claim@ above, a test case is automatically begun and + * ended if none is already underway. If the values are + * unequal, then @tvec_fail@ is called, quoting @expr@, and the + * mismatched values are dumped: @u0@ is printed as the output + * value and @u1@ is printed as the input reference. + */ + int tvec_claimeq_uint(struct tvec_state *tv, unsigned long u0, unsigned long u1, const char *file, unsigned lno, const char *expr) @@ -942,14 +1486,46 @@ const struct tvec_regty tvty_float = { parse_float, dump_float }; -int tvec_claimeq_float(struct tvec_state *tv, - double f0, double f1, - const char *file, unsigned lno, - const char *expr) -{ - return (tvec_claimeqish_float(tv, f0, f1, TVFF_EXACT, 0.0, - file, lno, expr)); -} +/* --- @tvec_claimeqish_float@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @double f0, f1@ = two floating-point numbers + * @unsigned f@ = flags (@TVFF_...@) + * @double delta@ = maximum tolerable difference + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *expr@ = the expression to quote on failure + * + * Returns: Nonzero if @f0@ and @u1@ are sufficiently close, otherwise + * zero. + * + * Use: Check that values of @f0@ and @f1@ are sufficiently close. + * As for @tvec_claim@ above, a test case is automatically begun + * and ended if none is already underway. If the values are + * too far apart, then @tvec_fail@ is called, quoting @expr@, + * and the mismatched values are dumped: @f0@ is printed as the + * output value and @f1@ is printed as the input reference. + * + * The details for the comparison are as follows. + * + * * A NaN value matches any other NaN, and nothing else. + * + * * An infinity matches another infinity of the same sign, + * and nothing else. + * + * * If @f&TVFF_EQMASK@ is @TVFF_EXACT@, then any + * representable number matches only itself: in particular, + * positive and negative zero are considered distinct. + * (This allows tests to check that they land on the correct + * side of branch cuts, for example.) + * + * * If @f&TVFF_EQMASK@ is @TVFF_ABSDELTA@, then %$x$% matches + * %$y$% when %$|x - y| < \delta$%. + * + * * If @f&TVFF_EQMASK@ is @TVFF_RELDELTA@, then %$x$% matches + * %$y$% when %$|1 - y/x| < \delta$%. (Note that this + * criterion is asymmetric FIXME + */ + int tvec_claimeqish_float(struct tvec_state *tv, double f0, double f1, unsigned f, double delta, const char *file, unsigned lno, @@ -963,6 +1539,29 @@ int tvec_claimeqish_float(struct tvec_state *tv, return (tvec_claimeq(tv, &tvty_float, &arg, file, lno, expr)); } +/* --- @tvec_claimeq_float@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @double f0, f1@ = two floating-point numbers + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *expr@ = the expression to quote on failure + * + * Returns: Nonzero if @f0@ and @u1@ are identical, otherwise zero. + * + * Use: Check that values of @f0@ and @f1@ are identical. The + * function is exactly equivalent to @tvec_claimeqish_float@ + * with @f == TVFF_EXACT@. + */ + +int tvec_claimeq_float(struct tvec_state *tv, + double f0, double f1, + const char *file, unsigned lno, + const char *expr) +{ + return (tvec_claimeqish_float(tv, f0, f1, TVFF_EXACT, 0.0, + file, lno, expr)); +} + /*----- Enumerations ------------------------------------------------------*/ #define init_ienum init_int @@ -1171,6 +1770,24 @@ static const struct tvec_iassoc bool_assoc[] = { const struct tvec_ienuminfo tvenum_bool = { "bool", bool_assoc, &tvrange_int }; +/* --- @tvec_claimeq_tenum@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const struct tvec_typeenuminfo *ei@ = enumeration type info + * @ty t0, t1@ = two values + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *expr@ = the expression to quote on failure + * + * Returns: Nonzero if @t0@ and @t1@ are equal, otherwise zero. + * + * Use: Check that values of @t0@ and @t1@ are equal. As for + * @tvec_claim@ above, a test case is automatically begun and + * ended if none is already underway. If the values are + * unequal, then @tvec_fail@ is called, quoting @expr@, and the + * mismatched values are dumped: @t0@ is printed as the output + * value and @t1@ is printed as the input reference. + */ + #define DEFCLAIM(tag, ty, slot) \ int tvec_claimeq_##slot##enum \ (struct tvec_state *tv, \ @@ -1265,6 +1882,24 @@ const struct tvec_regty tvty_flags = { parse_flags, dump_flags }; +/* --- @tvec_claimeq_flags@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const struct tvec_flaginfo *fi@ = flags type info + * @unsigned long f0, f1@ = two values + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *expr@ = the expression to quote on failure + * + * Returns: Nonzero if @f0@ and @f1@ are equal, otherwise zero. + * + * Use: Check that values of @f0@ and @f1@ are equal. As for + * @tvec_claim@ above, a test case is automatically begun and + * ended if none is already underway. If the values are + * unequal, then @tvec_fail@ is called, quoting @expr@, and the + * mismatched values are dumped: @f0@ is printed as the output + * value and @f1@ is printed as the input reference. + */ + int tvec_claimeq_flags(struct tvec_state *tv, const struct tvec_flaginfo *fi, unsigned long f0, unsigned long f1, @@ -1279,7 +1914,7 @@ int tvec_claimeq_flags(struct tvec_state *tv, /*----- Characters --------------------------------------------------------*/ static int tobuf_char(buf *b, const union tvec_regval *rv, - const struct tvec_regdef *rd) + const struct tvec_regdef *rd) { uint32 u; if (0 <= rv->i && rv->i <= UCHAR_MAX) u = rv->i; @@ -1289,7 +1924,7 @@ static int tobuf_char(buf *b, const union tvec_regval *rv, } static int frombuf_char(buf *b, union tvec_regval *rv, - const struct tvec_regdef *rd) + const struct tvec_regdef *rd) { uint32 u; @@ -1312,23 +1947,29 @@ static int parse_char(union tvec_regval *rv, const struct tvec_regdef *rd, if (ch == '#') { ungetc(ch, tv->fp); if (tvec_readword(tv, &d, ";", "character name")) { rc = -1; goto end; } - if (STRCMP(d.buf, ==, "#eof")) - rv->i = EOF; - else { + if (read_charname(&ch, d.buf, RCF_EOFOK)) { rc = tvec_error(tv, "unknown character name `%s'", d.buf); goto end; } - rc = 0; goto end; + if (tvec_flushtoeol(tv, 0)) { rc = -1; goto end; } + rv->i = ch; rc = 0; goto end; } if (ch == '\'') { f |= f_quote; ch = getc(tv->fp); } switch (ch) { + case ';': + if (!(f&f_quote)) { rc = tvec_syntax(tv, ch, "character"); goto end; } + goto plain; + case '\n': + if (f&f_quote) + { f &= ~f_quote; ungetc(ch, tv->fp); ch = '\''; goto plain; } + case EOF: + if (f&f_quote) { f &= ~f_quote; ch = '\''; goto plain; } + /* fall through */ case '\'': - if (!(f&f_quote)) goto plain; - case EOF: case '\n': rc = tvec_syntax(tv, ch, "character"); goto end; case '\\': - if (read_escape(&ch, tv)) return (-1); + if (read_charesc(&ch, tv)) return (-1); default: plain: rv->i = ch; break; } @@ -1350,15 +1991,37 @@ static void dump_char(const union tvec_regval *rv, unsigned style, const struct gprintf_ops *gops, void *go) { - if ((style&TVSF_COMPACT) && isprint(rv->i) && rv->i != '\'') - gprintf(gops, go, "%c", (int)rv->i); - else - format_char(gops, go, rv->i); + const char *p; + unsigned f = 0; +#define f_semi 1u + + p = find_charname(rv->i, (style&TVSF_COMPACT) ? CTF_SHORT : CTF_PREFER); + if (p) { + gprintf(gops, go, "%s", p); + if (style&TVSF_COMPACT) return; + else { gprintf(gops, go, " ;"); f |= f_semi; } + } + + if (rv->i >= 0) { + if (f&f_semi) gprintf(gops, go, " = "); + switch (rv->i) { + case ' ': case '\\': case '\'': quote: + format_char(gops, go, rv->i); + break; + default: + if (!(style&TVSF_COMPACT) || !isprint(rv->i)) goto quote; + gprintf(gops, go, "%c", (int)rv->i); + return; + } + } if (!(style&TVSF_COMPACT)) { - gprintf(gops, go, " ; = %ld = ", rv->i); + if (!(f&f_semi)) gprintf(gops, go, " ;"); + gprintf(gops, go, " = %ld = ", rv->i); format_signed_hex(gops, go, rv->i); } + +#undef f_semi } const struct tvec_regty tvty_char = { @@ -1367,6 +2030,23 @@ const struct tvec_regty tvty_char = { parse_char, dump_char }; +/* --- @tvec_claimeq_char@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @int ch0, ch1@ = two character codes + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *expr@ = the expression to quote on failure + * + * Returns: Nonzero if @ch0@ and @ch1@ are equal, otherwise zero. + * + * Use: Check that values of @ch0@ and @ch1@ are equal. As for + * @tvec_claim@ above, a test case is automatically begun and + * ended if none is already underway. If the values are + * unequal, then @tvec_fail@ is called, quoting @expr@, and the + * mismatched values are dumped: @ch0@ is printed as the output + * value and @ch1@ is printed as the input reference. + */ + int tvec_claimeq_char(struct tvec_state *tv, int c0, int c1, const char *file, unsigned lno, const char *expr) { @@ -1376,18 +2056,6 @@ int tvec_claimeq_char(struct tvec_state *tv, int c0, int c1, /*----- Text and byte strings ---------------------------------------------*/ -void tvec_allocstring(union tvec_regval *rv, size_t sz) -{ - if (rv->str.sz < sz) { xfree(rv->str.p); rv->str.p = xmalloc(sz); } - rv->str.sz = sz; -} - -void tvec_allocbytes(union tvec_regval *rv, size_t sz) -{ - if (rv->bytes.sz < sz) { xfree(rv->bytes.p); rv->bytes.p = xmalloc(sz); } - rv->bytes.sz = sz; -} - static void init_string(union tvec_regval *rv, const struct tvec_regdef *rd) { rv->str.p = 0; rv->str.sz = 0; } @@ -1435,7 +2103,7 @@ static int frombuf_string(buf *b, union tvec_regval *rv, size_t sz; p = buf_getmem32l(b, &sz); if (!p) return (-1); - tvec_allocstring(rv, sz); memcpy(rv->str.p, p, sz); + tvec_allocstring(rv, sz); memcpy(rv->str.p, p, sz); rv->str.p[sz] = 0; return (0); } @@ -1455,7 +2123,7 @@ static int check_string_length(size_t sz, const struct tvec_urange *ur, { if (ur && (ur->min > sz || sz > ur->max)) return (tvec_error(tv, - "invalid string length %lu; must be in [%lu..%lu]", + "invalid string length %lu; must be in [%lu .. %lu]", (unsigned long)sz, ur->min, ur->max)); return (0); } @@ -1465,7 +2133,8 @@ static int parse_string(union tvec_regval *rv, const struct tvec_regdef *rd, { void *p = rv->str.p; - if (read_compound_string(&p, &rv->str.sz, TVCODE_BARE, tv)) return (-1); + if (read_compound_string(&p, &rv->str.sz, TVCODE_BARE, 0, tv)) + return (-1); rv->str.p = p; if (check_string_length(rv->str.sz, rd->arg.p, tv)) return (-1); return (0); @@ -1476,7 +2145,8 @@ static int parse_bytes(union tvec_regval *rv, const struct tvec_regdef *rd, { void *p = rv->bytes.p; - if (read_compound_string(&p, &rv->bytes.sz, TVCODE_HEX, tv)) return (-1); + if (read_compound_string(&p, &rv->bytes.sz, TVCODE_HEX, 0, tv)) + return (-1); rv->bytes.p = p; if (check_string_length(rv->bytes.sz, rd->arg.p, tv)) return (-1); return (0); @@ -1495,23 +2165,32 @@ static void dump_string(const union tvec_regval *rv, if (!rv->str.sz) { gprintf(gops, go, "\"\""); return; } p = (const unsigned char *)rv->str.p; l = p + rv->str.sz; - if (*p == '!' || *p == ';' || *p == '"' || *p == '\'') goto quote; + switch (*p) { + case '!': case '#': case ';': case '"': case '\'': + case '(': case '{': case '[': case ']': case '}': case ')': + f |= f_nonword; break; + } for (q = p; q < l; q++) if (*q == '\n' && q != l - 1) f |= f_newline; else if (!*q || !isgraph(*q) || *q == '\\') f |= f_nonword; if (f&f_newline) { gprintf(gops, go, "\n\t"); goto quote; } else if (f&f_nonword) goto quote; - gops->putm(go, (const char *)p, rv->str.sz); return; + + gops->putm(go, (const char *)p, rv->str.sz); + return; quote: gprintf(gops, go, "\""); for (q = p; q < l; q++) if (!isprint(*q) || *q == '"') { if (p < q) gops->putm(go, (const char *)p, q - p); - if (*q == '\n' && !(style&TVSF_COMPACT)) - gprintf(gops, go, "\\n\"\t\""); - else + if (*q != '\n' || (style&TVSF_COMPACT)) format_charesc(gops, go, *q, FCF_BRACE); + else { + if (q + 1 == l) { gprintf(gops, go, "\\n\""); return; } + else gprintf(gops, go, "\\n\"\n\t\""); + } + p = q + 1; } if (p < q) gops->putm(go, (const char *)p, q - p); gprintf(gops, go, "\""); @@ -1547,10 +2226,10 @@ static void dump_bytes(const union tvec_regval *rv, if (l - p < 16) n = l - p; else n = 16; - for (i = 0; i < 16; i++) { + for (i = 0; i < n; i++) { if (i < n) gprintf(gops, go, "%02x", p[i]); else gprintf(gops, go, " "); - if (i%4 == 3) gprintf(gops, go, " "); + if (i < n - 1 && i%4 == 3) gprintf(gops, go, " "); } gprintf(gops, go, " ; "); if (sz > 16) gprintf(gops, go, "[%0*lx] ", wd, (unsigned long)off); @@ -1573,6 +2252,25 @@ const struct tvec_regty tvty_bytes = { parse_bytes, dump_bytes }; +/* --- @tvec_claimeq_string@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const char *p0@, @size_t sz0@ = first string with length + * @const char *p1@, @size_t sz1@ = second string with length + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *expr@ = the expression to quote on failure + * + * Returns: Nonzero if the strings at @p0@ and @p1@ are equal, otherwise + * zero. + * + * Use: Check that strings at @p0@ and @p1@ are equal. As for + * @tvec_claim@ above, a test case is automatically begun and + * ended if none is already underway. If the values are + * unequal, then @tvec_fail@ is called, quoting @expr@, and the + * mismatched values are dumped: @p0@ is printed as the output + * value and @p1@ is printed as the input reference. + */ + int tvec_claimeq_string(struct tvec_state *tv, const char *p0, size_t sz0, const char *p1, size_t sz1, @@ -1583,6 +2281,22 @@ int tvec_claimeq_string(struct tvec_state *tv, return (tvec_claimeq(tv, &tvty_string, 0, file, lno, expr)); } +/* --- @tvec_claimeq_strz@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const char *p0, *p1@ = two strings to compare + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *expr@ = the expression to quote on failure + * + * Returns: Nonzero if the strings at @p0@ and @p1@ are equal, otherwise + * zero. + * + * Use: Check that strings at @p0@ and @p1@ are equal, as for + * @tvec_claimeq_string@, except that the strings are assumed + * null-terminated, so their lengths don't need to be supplied + * explicitly. + */ + int tvec_claimeq_strz(struct tvec_state *tv, const char *p0, const char *p1, const char *file, unsigned lno, const char *expr) @@ -1594,6 +2308,25 @@ int tvec_claimeq_strz(struct tvec_state *tv, return (tvec_claimeq(tv, &tvty_string, 0, file, lno, expr)); } +/* --- @tvec_claimeq_bytes@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const void *p0@, @size_t sz0@ = first string with length + * @const void *p1@, @size_t sz1@ = second string with length + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *expr@ = the expression to quote on failure + * + * Returns: Nonzero if the strings at @p0@ and @p1@ are equal, otherwise + * zero. + * + * Use: Check that binary strings at @p0@ and @p1@ are equal. As for + * @tvec_claim@ above, a test case is automatically begun and + * ended if none is already underway. If the values are + * unequal, then @tvec_fail@ is called, quoting @expr@, and the + * mismatched values are dumped: @p0@ is printed as the output + * value and @p1@ is printed as the input reference. + */ + int tvec_claimeq_bytes(struct tvec_state *tv, const void *p0, size_t sz0, const void *p1, size_t sz1, @@ -1606,6 +2339,39 @@ int tvec_claimeq_bytes(struct tvec_state *tv, return (tvec_claimeq(tv, &tvty_bytes, 0, file, lno, expr)); } +/* --- @tvec_allocstring@, @tvec_allocbytes@ --- * + * + * Arguments: @union tvec_regval *rv@ = register value + * @size_t sz@ = required size + * + * Returns: --- + * + * Use: Allocated space in a text or binary string register. If the + * current register size is sufficient, its buffer is left + * alone; otherwise, the old buffer, if any, is freed and a + * fresh buffer allocated. These functions are not intended to + * be used to adjust a buffer repeatedly, e.g., while building + * output incrementally: (a) they will perform badly, and (b) + * the old buffer contents are simply discarded if reallocation + * is necessary. Instead, use a @dbuf@ or @dstr@. + * + * The @tvec_allocstring@ function sneakily allocates an extra + * byte for a terminating zero. The @tvec_allocbytes@ function + * doesn't do this. + */ + +void tvec_allocstring(union tvec_regval *rv, size_t sz) +{ + if (rv->str.sz <= sz) { xfree(rv->str.p); rv->str.p = xmalloc(sz + 1); } + rv->str.sz = sz; +} + +void tvec_allocbytes(union tvec_regval *rv, size_t sz) +{ + if (rv->bytes.sz < sz) { xfree(rv->bytes.p); rv->bytes.p = xmalloc(sz); } + rv->bytes.sz = sz; +} + /*----- Buffer type -------------------------------------------------------*/ static int eq_buffer(const union tvec_regval *rv0, @@ -1654,12 +2420,13 @@ static int parse_buffer(union tvec_regval *rv, for (t = u, unit = units; *unit; unit++) { if (t > (size_t)-1/1024) f |= f_range; else t *= 1024; - if (*q == *unit && (!q[1] || q[1] == 'B')) { + if (*q == *unit) { if (f&f_range) goto rangerr; - u = t; q += 2; break; + u = t; q++; break; } } - if (*q && *q != ';') goto bad; + if (*q == 'B') q++; + if (*q) goto bad; if (check_string_length(u, rd->arg.p, tv)) { rc = -1; goto end; } if (tvec_flushtoeol(tv, 0)) { rc = -1; goto end; } diff --git a/test/tvec.h b/test/tvec.h index 7f19f50..5ea18ca 100644 --- a/test/tvec.h +++ b/test/tvec.h @@ -186,79 +186,6 @@ struct tvec_regdef { #define TVEC_GREG(vec, i, regsz) \ ((struct tvec_reg *)((unsigned char *)(vec) + (i)*(regsz))) -/*------ Serialization utilities ------------------------------------------*/ - -/* --- @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 `candidate register definitions' are those entries @r@ in - * the @regs@ vector whose index @r.i@ is strictly less than - * @nr@. The `selected register definitions' are those - * candidate register definitions @r@ for which the indicated - * register @rv[r.i]@ has the @TVRF_LIVE@ flag set. The - * serialized output begins with a header bitmap: if there are - * %$n$% candidate register definitions then the header bitmap - * consists of %$\lceil n/8 \rceil$% bytes. Bits are ordered - * starting from the least significant bit of the first byte, - * end ending at the most significant bit of the final byte. - * The bit corresponding to a candidate register definition is - * set if and only if that register defintion is selected. The - * header bitmap is then followed by the serializations of the - * selected registers -- i.e., for each selected register - * definition @r@, the serialized value of register @rv[r.i]@ -- - * simply concatenated together, with no padding or alignment. - * - * The serialized output is written to the buffer @b@. Failure - * can be caused by running out of buffer space, or a failing - * type handler. - */ - -extern int tvec_serialize(const struct tvec_reg */*rv*/, buf */*b*/, - const struct tvec_regdef */*regs*/, - unsigned /*nr*/, size_t /*regsz*/); - -/* --- @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. - * - * On successful completion, store the address of the first byte - * after the serialized data in @*end_out@ if @end_out@ is not - * null. Failure results only from a failing register type - * handler. - */ - -extern int tvec_deserialize(struct tvec_reg */*rv*/, buf */*b*/, - const struct tvec_regdef */*regs*/, - unsigned /*nr*/, size_t /*regsz*/); - /*----- Test state --------------------------------------------------------*/ enum { @@ -308,6 +235,135 @@ struct tvec_state { */ #define TVEC_REG(tv, vec, i) TVEC_GREG((tv)->vec, (i), (tv)->regsz) +struct tvec_config { + /* An overall test configuration. */ + + const struct tvec_test *tests; /* the tests to be performed */ + unsigned nrout, nreg; /* number of output/total regs */ + size_t regsz; /* size of a register */ +}; + +/*----- Output formatting -------------------------------------------------*/ + +struct tvec_output { + /* An output formatter. */ + const struct tvec_outops *ops; /* pointer to operations */ +}; + +/* Benchmarking details. */ +enum { + TVBU_OP, /* counting operations of some kind */ + TVBU_BYTE /* counting bytes (@rbuf >= 0@) */ +}; +struct bench_timing; /* forward declaration */ + +struct tvec_outops { + /* Output operations. */ + + void (*bsession)(struct tvec_output */*o*/, struct tvec_state */*tv*/); + /* Begin a test session. The output driver will probably want to + * save @tv@, because this isn't provided to any other methods. + */ + + int (*esession)(struct tvec_output */*o*/); + /* End a session, and return the suggested exit code. */ + + void (*bgroup)(struct tvec_output */*o*/); + /* Begin a test group. The test group description is @tv->test@. */ + + void (*skipgroup)(struct tvec_output */*o*/, + const char */*excuse*/, va_list */*ap*/); + /* The group is being skipped; @excuse@ may be null or a format + * string explaining why. The @egroup@ method will not be called + * separately. + */ + + void (*egroup)(struct tvec_output */*o*/); + /* End a test group. At least one test was attempted or @skipgroup@ + * would have been called instead. If @tv->curr[TVOUT_LOSE]@ is + * nonzero then the test group as a whole failed; otherwise it + * passed. + */ + + void (*btest)(struct tvec_output */*o*/); + /* Begin a test case. */ + + void (*skip)(struct tvec_output */*o*/, + const char */*excuse*/, va_list */*ap*/); + /* The test case is being skipped; @excuse@ may be null or a format + * string explaining why. The @etest@ function will still be called + * (so this works differently from @skipgroup@ and @egroup@). A test + * case can be skipped at most once, and, if skipped, it cannot fail. + */ + + void (*fail)(struct tvec_output */*o*/, + const char */*detail*/, va_list */*ap*/); + /* The test case failed. + * + * The output driver should preferably report the filename (@infile@) + * and line number (@test_lno@, not @lno@) for the failing test. + * + * The @detail@ may be null or a format string describing detail + * about how the failing test was run which can't be determined from + * the registers; a @detail@ is usually provided when (and only when) + * the test environment potentially invokes the test function more + * than once. + * + * A single test case can fail multiple times! + */ + + void (*dumpreg)(struct tvec_output */*o*/, + unsigned /*disp*/, const union tvec_regval */*rv*/, + const struct tvec_regdef */*rd*/); + /* Dump a register. The `disposition' @disp@ explains what condition + * the register is in; see @tvec_dumpreg@ and the @TVRD_...@ codes. + * The register value is at @rv@, and its definition, including its + * type, at @rd@. Note that this function may be called on virtual + * registers which aren't in either of the register vectors or + * mentioned by the test description. + */ + + void (*etest)(struct tvec_output */*o*/, unsigned /*outcome*/); + /* The test case concluded with the given @outcome@ (one of the + * @TVOUT_...@ codes. + */ + + void (*bbench)(struct tvec_output */*o*/, + const char */*ident*/, unsigned /*unit*/); + /* Begin a benchmark. The @ident@ is a formatted string identifying + * the benchmark based on the values of the input registered marked + * @TVRF_ID@; the output driver is free to use this or come up with + * its own way to identify the test, e.g., by inspecting the register + * values for itself. The @unit@ is one of the @TVBU_...@ constants + * explaining what sort of thing is being measured. + */ + + void (*ebench)(struct tvec_output */*o*/, + const char */*ident*/, unsigned /*unit*/, + const struct bench_timing */*tm*/); + /* End a benchmark. The @ident@ and @unit@ arguments are as for + * @bbench@. If @tm@ is zero then the measurement failed; otherwise + * @tm->n@ counts total number of things (operations or bytes, as + * indicated by @unit@) processed in the indicated time. + */ + + void (*error)(struct tvec_output */*o*/, + const char */*msg*/, va_list */*ap*/); + /* Report an error. The driver should ideally report the filename + * (@infile@) and line number (@lno@) prompting the error. + */ + + void (*notice)(struct tvec_output */*o*/, + const char */*msg*/, va_list */*ap*/); + /* Report a miscellaneous message. The driver should ideally report + * the filename (@infile@) and line number (@lno@) prompting the + * error. + */ + + void (*destroy)(struct tvec_output */*o*/); + /* Release any resources acquired by the driver. */ +}; + /*----- Test descriptions -------------------------------------------------*/ typedef void tvec_testfn(const struct tvec_reg */*in*/, @@ -342,9 +398,10 @@ struct tvec_env { int (*setup)(struct tvec_state */*tv*/, const struct tvec_env */*env*/, void */*pctx*/, void */*ctx*/); - /* Initialize the context; called at the start of a test group. Return - * zero on success, or @-1@ on failure. If setup fails, the context is - * freed, and the test group is skipped. + /* Initialize the context; called at the start of a test group; @pctx@ is + * null for environments called by the core, but may be non-null for + * subordinate environments. Return zero on success, or @-1@ on failure. + * If setup fails, the context is freed, and the test group is skipped. */ int (*set)(struct tvec_state */*tv*/, const char */*var*/, @@ -402,55 +459,328 @@ enum { TVRD_EXPECT /* mismatching input register */ }; -/* --- @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. - */ - -extern void PRINTF_LIKE(2, 3) - tvec_skipgroup(struct tvec_state */*tv*/, const char */*excuse*/, ...); -extern void tvec_skipgroup_v(struct tvec_state */*tv*/, - const char */*excuse*/, va_list */*ap*/); - -/* --- @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. - */ +/*----- Register types ----------------------------------------------------*/ -extern void PRINTF_LIKE(2, 3) - tvec_skip(struct tvec_state */*tv*/, const char */*excuse*/, ...); -extern void tvec_skip_v(struct tvec_state */*tv*/, - const char */*excuse*/, va_list */*ap*/); +struct tvec_regty { + /* A register type. */ -/* --- @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. - */ + void (*init)(union tvec_regval */*rv*/, const struct tvec_regdef */*rd*/); + /* Initialize the value in @*rv@. This will be called before any + * other function acting on the value, including @release@. + */ -extern void tvec_resetoutputs(struct tvec_state */*tv*/); + void (*release)(union tvec_regval */*rv*/, + const struct tvec_regdef */*rd*/); + /* Release any resources associated with the value in @*rv@. */ -/* --- @tvec_checkregs@ --- * - * + int (*eq)(const union tvec_regval */*rv0*/, + const union tvec_regval */*rv1*/, + const struct tvec_regdef */*rd*/); + /* Return nonzero if @*rv0@ and @*rv1@ are equal values. + * Asymmetric criteria are permitted: @tvec_checkregs@ calls @eq@ + * with the input register as @rv0@ and the output as @rv1@. + */ + + int (*tobuf)(buf */*b*/, const union tvec_regval */*rv*/, + const struct tvec_regdef */*rd*/); + /* Serialize the value @*rv@, writing the result to @b@. Return + * zero on success, or @-1@ on error. + */ + + int (*frombuf)(buf */*b*/, union tvec_regval */*rv*/, + const struct tvec_regdef */*rd*/); + /* Deserialize a value from @b@, storing it in @*rv@. Return zero on + * success, or @-1@ on error. + */ + + int (*parse)(union tvec_regval */*rv*/, const struct tvec_regdef */*rd*/, + struct tvec_state */*tv*/); + /* Parse a value from @tv->fp@, storing it in @*rv@. Return zero on + * success, or @-1@ on error, having reported one or more errors via + * @tvec_error@ or @tvec_syntax@. A successful return should leave + * the input position at the start of the next line; the caller will + * flush the remainder of the line itself. + */ + + void (*dump)(const union tvec_regval */*rv*/, + const struct tvec_regdef */*rd*/, + unsigned /*style*/, + const struct gprintf_ops */*gops*/, void */*go*/); +#define TVSF_COMPACT 1u + /* Write a human-readable representation of the value @*rv@ using + * @gprintf@ on @gops@ and @go@. The @style@ is a collection of + * flags: if @TVSF_COMPACT@ is set, then output should be minimal, + * and must fit on a single line; otherwise, output may consist of + * multiple lines and may contain redundant information if that is + * likely to be useful to a human reader. + */ +}; + +/*----- Session lifecycle -------------------------------------------------*/ + +/* Here's the overall flow for a testing session. + * + * @tvec_begin@ + * -> output @bsession@ + * @tvec_read@ + * -> output @bgroup@ + * -> env @setup@ + * one or more tests + * -> type @init@ (input and output) + * -> type @parse@ (input) + * -> output @btest@ + * -> env @before@ + * -> env @run@ + * -> @tvec_skip@ + * -> output @skip@ + * -> test @fn@ + * -> @tvec_checkregs@ + * -> type @eq@ + * -> @tvec_fail@ + * -> output @fail@ + * -> @tvec_mismatch@ + * -> output @dumpreg@ + * -> type @dump@ + * -> env @after@ + * -> output @etest@ + * or + * -> output @skipgroup@ + * finally + * -> output @egroup@ + * -> env @teardown@ + * + * @tvec_adhoc@ + * @tvec_begingroup@ + * -> output @bgroup@ + * -> env @setup@ + * @tvec_begintest@ + * -> output @btest@ + * @tvec_skip@ + * -> output @skip@ + * @tvec_claimeq@ + * -> @tvec_fail@ + * -> output @fail@ + * -> @tvec_mismatch@ + * -> output @dumpreg@ + * -> type @dump@ + * @tvec_endtest@ + * -> output @etest@ + * or @tvec_skipgroup@ + * -> output @skipgroup@ + * @tvec_endgroup@ + * -> output @egroup@ + * + * @tvec_end@ + * -> output @esession@ + * -> output @destroy@ + * + * @tvec_benchrun@ + * -> type @dump@ (compact style) + * -> output @bbench@ + * -> subenv @run@ + * -> test @fn@ + * -> output @ebench@ + * -> @tvec_benchreport@ + * + * The output functions @error@ and @notice@ can be called at arbitrary + * times. + */ + +/* --- @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. + */ + +extern void tvec_begin(struct tvec_state */*tv_out*/, + const struct tvec_config */*config*/, + struct tvec_output */*o*/); + +/* --- @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.) + */ + +extern int tvec_end(struct tvec_state */*tv*/); + +/* --- @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. + */ + +extern int tvec_read(struct tvec_state */*tv*/, + const char */*infile*/, FILE */*fp*/); + +/*----- Command-line interface --------------------------------------------*/ + +extern const struct tvec_config tvec_adhocconfig; +/* A special @struct tvec_config@ to use for programs which perform ad-hoc + * testing. + */ + +/* --- @tvec_parseargs@ --- * + * + * Arguments: @int argc@ = number of command-line arguments + * @char *argv[]@ = vector of argument strings + * @struct tvec_state *tv_out@ = test vector state to initialize + * @int *argpos_out@ = where to leave unread argument index + * @const struct tvec_config *cofig@ = test vector configuration + * + * Returns: --- + * + * Use: Parse arguments and set up the test vector state @*tv_out@. + * If errors occur, print messages to standard error and exit + * with status 2. + */ + +extern void tvec_parseargs(int /*argc*/, char */*argv*/[], + struct tvec_state */*tv_out*/, + int */*argpos_out*/, + const struct tvec_config */*config*/); + +/* --- @tvec_readstdin@, @tvec_readfile@, @tvec_readarg@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @const char *file@ = pathname of file to read + * @const char *arg@ = argument to interpret + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Read test vector data from stdin or a named file. The + * @tvec_readarg@ function reads from stdin if @arg@ is `%|-|%', + * and from the named file otherwise. + */ + +extern int tvec_readstdin(struct tvec_state */*tv*/); +extern int tvec_readfile(struct tvec_state */*tv*/, const char */*file*/); +extern int tvec_readarg(struct tvec_state */*tv*/, const char */*arg*/); + +/* --- @tvec_readdflt@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @const char *dflt@ = defsault filename or null + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Reads from the default test vector data. If @file@ is null, + * then read from standard input, unless that's a terminal; if + * @file@ is not null, then read the named file, looking in the + * directory named by the `%|srcdir|%' environment variable if + * that's set, or otherwise in the current directory. + */ + +extern int tvec_readdflt(struct tvec_state */*tv*/, const char */*file*/); + +/* --- @tvec_readargs@ --- * + * + * Arguments: @int argc@ = number of command-line arguments + * @char *argv[]@ = vector of argument strings + * @struct tvec_state *tv@ = test vector state + * @int *argpos_inout@ = current argument position (updated) + * @const char *dflt@ = default filename or null + * + * Returns: Zero on success, @-1@ on error. + * + * Use: Reads from the sources indicated by the command-line + * arguments, in order, interpreting each as for @tvec_readarg@; + * if no arguments are given then read from @dflt@ as for + * @tvec_readdflt@. + */ + +extern int tvec_readargs(int /*argc*/, char */*argv*/[], + struct tvec_state */*tv*/, + int */*argpos_inout*/, const char */*dflt*/); + +/* --- @tvec_main@ --- * + * + * Arguments: @int argc@ = number of command-line arguments + * @char *argv[]@ = vector of argument strings + * @const struct tvec_config *cofig@ = test vector configuration + * @const char *dflt@ = default filename or null + * + * Returns: Exit code. + * + * Use: All-in-one test vector front-end. Parse options from the + * command-line as for @tvec_parseargs@, and then process the + * remaining positional arguments as for @tvec_readargs@. The + * function constructs and disposes of a test vector state. + */ + +extern int tvec_main(int /*argc*/, char */*argv*/[], + const struct tvec_config */*config*/, + const char */*dflt*/); + +/*----- 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. + */ + +extern void PRINTF_LIKE(2, 3) + tvec_skipgroup(struct tvec_state */*tv*/, const char */*excuse*/, ...); +extern void tvec_skipgroup_v(struct tvec_state */*tv*/, + const char */*excuse*/, va_list */*ap*/); + +/* --- @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. + */ + +extern void PRINTF_LIKE(2, 3) + tvec_skip(struct tvec_state */*tv*/, const char */*excuse*/, ...); +extern void tvec_skip_v(struct tvec_state */*tv*/, + const char */*excuse*/, va_list */*ap*/); + +/* --- @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. + */ + +extern void tvec_resetoutputs(struct tvec_state */*tv*/); + +/* --- @tvec_checkregs@ --- * + * * Arguments: @struct tvec_state *tv@ = test-vector state * * Returns: Zero on success, @-1@ on mismatch. @@ -548,239 +878,53 @@ extern void PRINTF_LIKE(2, 3) extern void tvec_check_v(struct tvec_state */*tv*/, const char */*detail*/, va_list */*ap*/); -/*----- Session lifecycle -------------------------------------------------*/ - -struct tvec_config { - /* An overall test configuration. */ - - const struct tvec_test *tests; /* the tests to be performed */ - unsigned nrout, nreg; /* number of output/total regs */ - size_t regsz; /* size of a register */ -}; +/*----- Ad-hoc testing ----------------------------------------------------*/ -/* --- @tvec_begin@ --- * +/* --- @tvec_adhoc@ --- * * - * Arguments: @struct tvec_state *tv_out@ = state structure to fill in - * @const struct tvec_config *config@ = test configuration - * @struct tvec_output *o@ = output driver + * Arguments: @struct tvec_state *tv@ = test-vector state + * @struct tvec_test *t@ = space for a test definition * * Returns: --- * - * Use: Initialize a state structure ready to do some testing. + * 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. */ -extern void tvec_begin(struct tvec_state */*tv_out*/, - const struct tvec_config */*config*/, - struct tvec_output */*o*/); +extern void tvec_adhoc(struct tvec_state */*tv*/, struct tvec_test */*t*/); -/* --- @tvec_end@ --- * +/* --- @tvec_begingroup@, @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: A proposed exit code. + * Returns: --- * - * 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.) + * 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. */ -extern int tvec_end(struct tvec_state */*tv*/); +extern void tvec_begingroup(struct tvec_state */*tv*/, const char */*name*/, + const char */*file*/, unsigned /*lno*/); +#define TVEC_BEGINGROUP(tv, name) \ + do tvec_begingroup(tv, name, __FILE__, __LINE__); while (0) -/* --- @tvec_read@ --- * +/* --- @tvec_endgroup@ --- * * * 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. + * Returns: --- * - * 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. - */ - -extern int tvec_read(struct tvec_state */*tv*/, - const char */*infile*/, FILE */*fp*/); - -/*----- Input utilities ---------------------------------------------------*/ - -/* These are provided by the core for the benefit of type @parse@ methods, - * and test-environment @set@ functions, which get to read from the test - * input file. The latter are usually best implemented by calling on the - * former. - * - * The two main rules are as follows. - * - * * Leave the file position at the beginning of the line following - * whatever it was that you read. - * - * * When you read and consume a newline (which you do at least once, by - * the previous rule), then increment @tv->lno@ to keep track of the - * current line number. - */ - -/* --- @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. - */ - -extern void tvec_skipspc(struct tvec_state */*tv*/); - -/* --- @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). - */ - -extern int PRINTF_LIKE(3, 4) - tvec_syntax(struct tvec_state */*tv*/, int /*ch*/, - const char */*expect*/, ...); -extern int tvec_syntax_v(struct tvec_state */*tv*/, int /*ch*/, - const char */*expect*/, va_list */*ap*/); - -/* --- @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. - */ - -#define TVFF_ALLOWANY 1u -extern 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. - */ - -extern int tvec_nexttoken(struct tvec_state */*tv*/); - -/* --- @tvec_readword@ --- * - * - * Arguments: @struct tvec_state *tv@ = test-vector state - * @dstr *d@ = string to append the word to - * @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. - */ - -extern int PRINTF_LIKE(4, 5) - tvec_readword(struct tvec_state */*tv*/, dstr */*d*/, - const char */*delims*/, const char */*expect*/, ...); -extern int tvec_readword_v(struct tvec_state */*tv*/, dstr */*d*/, - const char */*delims*/, const char */*expect*/, - va_list */*ap*/); - -/*----- Ad-hoc testing ----------------------------------------------------*/ - -/* --- @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. - */ - -extern void tvec_adhoc(struct tvec_state */*tv*/, struct tvec_test */*t*/); - -/* --- @tvec_begingroup@, @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. - */ - -extern void tvec_begingroup(struct tvec_state */*tv*/, const char */*name*/, - const char */*file*/, unsigned /*lno*/); -#define TVEC_BEGINGROUP(tv, name) \ - do tvec_begingroup(tv, name, __FILE__, __LINE__); while (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. + * Use: End an ad-hoc test group. The statistics are updated and the + * outcome is reported to the output formatter. */ extern void tvec_endgroup(struct tvec_state */*tv*/); @@ -925,126 +1069,154 @@ extern int tvec_claimeq(struct tvec_state */*tv*/, const char */*file*/, unsigned /*lno*/, const char */*expr*/); -/*----- Output formatting -------------------------------------------------*/ +/*----- Benchmarking ------------------------------------------------------*/ -struct tvec_output { - /* An output formatter. */ - const struct tvec_outops *ops; /* pointer to operations */ +struct tvec_bench { + struct tvec_env _env; /* benchmarking is an environment */ + struct bench_state **bst; /* benchmark state anchor or null */ + unsigned long niter; /* iterations done per unit */ + int riter, rbuf; /* iterations and buffer registers */ + const struct tvec_env *env; /* subordinate environment */ }; +#define TVEC_BENCHENV \ + { sizeof(struct tvec_benchctx), \ + tvec_benchsetup, \ + tvec_benchset, \ + tvec_benchbefore, \ + tvec_benchrun, \ + tvec_benchafter, \ + tvec_benchteardown } +#define TVEC_BENCHINIT TVEC_BENCHENV, &tvec_benchstate -/* Benchmarking details. */ -enum { - TVBU_OP, /* counting operations of some kind */ - TVBU_BYTE /* counting bytes (@RBUF >= 0@) */ +struct tvec_benchctx { + const struct tvec_bench *b; + struct bench_state *bst; + double dflt_target; + void *subctx; }; -struct bench_timing; /* forward declaration */ -struct tvec_outops { - /* Output operations. */ +extern struct bench_state *tvec_benchstate; - void (*bsession)(struct tvec_output */*o*/, struct tvec_state */*tv*/); - /* Begin a test session. The output driver will probably want to - * save @tv@, because this isn't provided to any other methods. - */ +/* --- @tvec_benchsetup@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @const struct tvec_env *env@ = environment description + * @void *pctx@ = parent context (ignored) + * @void *ctx@ = context pointer to initialize + * + * Returns: Zero on success, @-1@ on failure. + * + * Use: Initialize a benchmarking environment context. + * + * The environment description must really be a @struct + * tvec_bench@. If the @bst@ slot is null, then a temporary + * benchmark state is allocated for the current test group and + * released at the end. Otherwise, it must be the address of a + * pointer to a benchmark state: if the pointer is null, then a + * fresh state is allocated and initialized and the pointer is + * updated; otherwise, the pointer is assumed to refer to an + * existing valid benchmark state. + */ - int (*esession)(struct tvec_output */*o*/); - /* End a session, and return the suggested exit code. */ +extern int tvec_benchsetup(struct tvec_state */*tv*/, + const struct tvec_env */*env*/, + void */*pctx*/, void */*ctx*/); - void (*bgroup)(struct tvec_output */*o*/); - /* Begin a test group. The test group description is @tv->test@. */ +/* --- @tvec_benchset@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @const char *var@ = variable name to set + * @const struct tvec_env *env@ = environment description + * @void *ctx@ = context pointer + * + * Returns: Zero on success, @-1@ on failure. + * + * Use: Set a special variable. The following special variables are + * supported. + * + * * %|@target|% is the (approximate) number of seconds to run + * the benchmark. + * + * Unrecognized variables are passed to the subordinate + * environment, if there is one. + */ - void (*skipgroup)(struct tvec_output */*o*/, - const char */*excuse*/, va_list */*ap*/); - /* The group is being skipped; @excuse@ may be null or a format - * string explaining why. The @egroup@ method will not be called - * separately. - */ +extern int tvec_benchset(struct tvec_state */*tv*/, const char */*var*/, + const struct tvec_env */*env*/, void */*ctx*/); - void (*egroup)(struct tvec_output */*o*/); - /* End a test group. At least one test was attempted or @skipgroup@ - * would have been called instead. If @tv->curr[TVOUT_LOSE]@ is - * nonzero then the test group as a whole failed; otherwise it - * passed. - */ +/* --- @tvec_benchbefore@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @void *ctx@ = context pointer + * + * Returns: Zero on success, @-1@ on failure. + * + * Use: Invoke the subordinate environment's @before@ function to + * prepare for the benchmark. + */ - void (*btest)(struct tvec_output */*o*/); - /* Begin a test case. */ +extern int tvec_benchbefore(struct tvec_state */*tv*/, void */*ctx*/); - void (*skip)(struct tvec_output */*o*/, - const char */*excuse*/, va_list */*ap*/); - /* The test case is being skipped; @excuse@ may be null or a format - * string explaining why. The @etest@ function will still be called - * (so this works differently from @skipgroup@ and @egroup@). A test - * case can be skipped at most once, and, if skipped, it cannot fail. - */ +/* --- @tvec_benchrun@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @tvec_testfn *fn@ = test function to run + * @void *ctx@ = context pointer for the test function + * + * Returns: --- + * + * Use: Measures and reports the performance of a test function. + * + * + */ - void (*fail)(struct tvec_output */*o*/, - const char */*detail*/, va_list */*ap*/); - /* The test case failed. - * - * The output driver should preferably report the filename (@infile@) - * and line number (@test_lno@, not @lno@) for the failing test. - * - * The @detail@ may be null or a format string describing detail - * about how the failing test was run which can't be determined from - * the registers; a @detail@ is usually provided when (and only when) - * the test environment potentially invokes the test function more - * than once. - * - * A single test case can fail multiple times! - */ +extern void tvec_benchrun(struct tvec_state */*tv*/, + tvec_testfn */*fn*/, void */*ctx*/); - void (*dumpreg)(struct tvec_output */*o*/, - unsigned /*disp*/, const union tvec_regval */*rv*/, - const struct tvec_regdef */*rd*/); - /* Dump a register. The `disposition' @disp@ explains what condition - * the register is in; see @tvec_dumpreg@ and the @TVRD_...@ codes. - * The register value is at @rv@, and its definition, including its - * type, at @rd@. Note that this function may be called on virtual - * registers which aren't in either of the register vectors or - * mentioned by the test description. - */ +/* --- @tvec_benchafter@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @void *ctx@ = context pointer + * + * Returns: --- + * + * Use: Invoke the subordinate environment's @after@ function to + * clean up after the benchmark. + */ - void (*etest)(struct tvec_output */*o*/, unsigned /*outcome*/); - /* The test case concluded with the given @outcome@ (one of the - * @TVOUT_...@ codes. - */ +extern void tvec_benchafter(struct tvec_state */*tv*/, void */*ctx*/); - void (*bbench)(struct tvec_output */*o*/, - const char */*ident*/, unsigned /*unit*/); - /* Begin a benchmark. The @ident@ is a formatted string identifying - * the benchmark based on the values of the input registered marked - * @TVRF_ID@; the output driver is free to use this or come up with - * its own way to identify the test, e.g., by inspecting the register - * values for itself. The @unit@ is one of the @TVBU_...@ constants - * explaining what sort of thing is being measured. - */ +/* --- @tvec_benchteardown@ --- * + * + * Arguments: @struct tvec_state *tv@ = test vector state + * @void *ctx@ = context pointer + * + * Returns: --- + * + * Use: Tear down the benchmark environment. + */ - void (*ebench)(struct tvec_output */*o*/, - const char */*ident*/, unsigned /*unit*/, - const struct bench_timing */*tm*/); - /* End a benchmark. The @ident@ and @unit@ arguments are as for - * @bbench@. If @tm@ is zero then the measurement failed; otherwise - * @tm->n@ counts total number of things (operations or bytes, as - * indicated by @unit@) processed in the indicated time. - */ +extern void tvec_benchteardown(struct tvec_state */*tv*/, void */*ctx*/); - void (*error)(struct tvec_output */*o*/, - const char */*msg*/, va_list */*ap*/); - /* Report an error. The driver should ideally report the filename - * (@infile@) and line number (@lno@) prompting the error. - */ +/* --- @tvec_benchreport@ --- * + * + * Arguments: @const struct gprintf_ops *gops@ = print operations + * @void *go@ = print destination + * @unsigned unit@ = the unit being measured (~TVBU_...@) + * @const struct bench_timing *tm@ = the benchmark result + * + * Returns: --- + * + * Use: Formats a report about the benchmark performance. This + * function is intended to be called on by an output + * @ebench@ function. + */ - void (*notice)(struct tvec_output */*o*/, - const char */*msg*/, va_list */*ap*/); - /* Report a miscellaneous message. The driver should ideally report - * the filename (@infile@) and line number (@lno@) prompting the - * error. - */ +extern void tvec_benchreport + (const struct gprintf_ops */*gops*/, void */*go*/, + unsigned /*unit*/, const struct bench_timing */*tm*/); - void (*destroy)(struct tvec_output */*o*/); - /* Release any resources acquired by the driver. */ -}; +/*----- Output functions --------------------------------------------------*/ /* --- @tvec_error@, @tvec_error_v@ --- * * @@ -1134,64 +1306,211 @@ extern struct tvec_output *tvec_tapoutput(FILE */*fp*/); extern struct tvec_output *tvec_dfltout(FILE */*fp*/); -/*----- Register types ----------------------------------------------------*/ +/*------ Serialization utilities ------------------------------------------*/ -struct tvec_regty { - /* A register type. */ +/* --- @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 `candidate register definitions' are those entries @r@ in + * the @regs@ vector whose index @r.i@ is strictly less than + * @nr@. The `selected register definitions' are those + * candidate register definitions @r@ for which the indicated + * register @rv[r.i]@ has the @TVRF_LIVE@ flag set. The + * serialized output begins with a header bitmap: if there are + * %$n$% candidate register definitions then the header bitmap + * consists of %$\lceil n/8 \rceil$% bytes. Bits are ordered + * starting from the least significant bit of the first byte, + * end ending at the most significant bit of the final byte. + * The bit corresponding to a candidate register definition is + * set if and only if that register defintion is selected. The + * header bitmap is then followed by the serializations of the + * selected registers -- i.e., for each selected register + * definition @r@, the serialized value of register @rv[r.i]@ -- + * simply concatenated together, with no padding or alignment. + * + * The serialized output is written to the buffer @b@. Failure + * can be caused by running out of buffer space, or a failing + * type handler. + */ - void (*init)(union tvec_regval */*rv*/, const struct tvec_regdef */*rd*/); - /* Initialize the value in @*rv@. This will be called before any - * other function acting on the value, including @release@. - */ +extern int tvec_serialize(const struct tvec_reg */*rv*/, buf */*b*/, + const struct tvec_regdef */*regs*/, + unsigned /*nr*/, size_t /*regsz*/); - void (*release)(union tvec_regval */*rv*/, - const struct tvec_regdef */*rd*/); - /* Release any resources associated with the value in @*rv@. */ +/* --- @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. + * + * On successful completion, store the address of the first byte + * after the serialized data in @*end_out@ if @end_out@ is not + * null. Failure results only from a failing register type + * handler. + */ - int (*eq)(const union tvec_regval */*rv0*/, - const union tvec_regval */*rv1*/, - const struct tvec_regdef */*rd*/); - /* Return nonzero if @*rv0@ and @*rv1@ are equal values. - * Asymmetric criteria are permitted: @tvec_checkregs@ calls @eq@ - * with the input register as @rv0@ and the output as @rv1@. - */ +extern int tvec_deserialize(struct tvec_reg */*rv*/, buf */*b*/, + const struct tvec_regdef */*regs*/, + unsigned /*nr*/, size_t /*regsz*/); - int (*tobuf)(buf */*b*/, const union tvec_regval */*rv*/, - const struct tvec_regdef */*rd*/); - /* Serialize the value @*rv@, writing the result to @b@. Return - * zero on success, or @-1@ on error. - */ +/*----- Input utilities ---------------------------------------------------*/ + +/* These are provided by the core for the benefit of type @parse@ methods, + * and test-environment @set@ functions, which get to read from the test + * input file. The latter are usually best implemented by calling on the + * former. + * + * The two main rules are as follows. + * + * * Leave the file position at the beginning of the line following + * whatever it was that you read. + * + * * When you read and consume a newline (which you do at least once, by + * the previous rule), then increment @tv->lno@ to keep track of the + * current line number. + */ + +/* --- @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. + */ + +extern void tvec_skipspc(struct tvec_state */*tv*/); + +/* --- @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). + */ + +extern int PRINTF_LIKE(3, 4) + tvec_syntax(struct tvec_state */*tv*/, int /*ch*/, + const char */*expect*/, ...); +extern int tvec_syntax_v(struct tvec_state */*tv*/, int /*ch*/, + const char */*expect*/, va_list */*ap*/); + +/* --- @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. + */ + +#define TVFF_ALLOWANY 1u +extern 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 (*frombuf)(buf */*b*/, union tvec_regval */*rv*/, - const struct tvec_regdef */*rd*/); - /* Deserialize a value from @b@, storing it in @*rv@. Return zero on - * success, or @-1@ on error. - */ +extern int tvec_nexttoken(struct tvec_state */*tv*/); - int (*parse)(union tvec_regval */*rv*/, const struct tvec_regdef */*rd*/, - struct tvec_state */*tv*/); - /* Parse a value from @tv->fp@, storing it in @*rv@. Return zero on - * success, or @-1@ on error, having reported one or more errors via - * @tvec_error@ or @tvec_syntax@. A successful return should leave - * the input position at the start of the next line; the caller will - * flush the remainder of the line itself. - */ +/* --- @tvec_readword@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @dstr *d@ = string to append the word to + * @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. + */ - void (*dump)(const union tvec_regval */*rv*/, - const struct tvec_regdef */*rd*/, - unsigned /*style*/, - const struct gprintf_ops */*gops*/, void */*go*/); -#define TVSF_COMPACT 1u - /* Write a human-readable representation of the value @*rv@ using - * @gprintf@ on @gops@ and @go@. The @style@ is a collection of - * flags: if @TVSF_COMPACT@ is set, then output should be minimal, - * and must fit on a single line; otherwise, output may consist of - * multiple lines and may contain redundant information if that is - * likely to be useful to a human reader. - */ -}; +extern int PRINTF_LIKE(4, 5) + tvec_readword(struct tvec_state */*tv*/, dstr */*d*/, + const char */*delims*/, const char */*expect*/, ...); +extern int tvec_readword_v(struct tvec_state */*tv*/, dstr */*d*/, + const char */*delims*/, const char */*expect*/, + va_list */*ap*/); -/*----- Integer types: signed and unsigned ------------------------------- */ +/*----- Integer types: signed and unsigned --------------------------------*/ /* Integers may be input in decimal, hex, binary, or octal, following * approximately usual conventions. @@ -1233,7 +1552,7 @@ extern const struct tvec_urange tvrange_uchar, tvrange_ushort, tvrange_uint, tvrange_ulong, tvrange_size, tvrange_byte, tvrange_u16, tvrange_u32; -/* --- tvec_claimeq_int@, @TVEC_CLAIMEQ_INT@ --- * +/* --- @tvec_claimeq_int@, @TVEC_CLAIMEQ_INT@ --- * * * Arguments: @struct tvec_state *tv@ = test-vector state * @long i0, i1@ = two signed integers @@ -1399,10 +1718,22 @@ extern int tvec_claimeq_float(struct tvec_state */*tv*/, /*----- Enumerated types --------------------------------------------------*/ -/* There is a distinct enumerated type for each of the branches of +/* An enumeration describes a set of values of some underlying type, each of + * which has a symbolic name. Values outside of the defined set can occur -- + * on output, because of bugs in the tested code, or on input to test + * handling of unexpected values. + * + * There is a distinct enumerated type for each of the branches of * @tvec_misc@. In the following, we write @t@ for the type code, which is * @i@ for signed integer, @u@ for unsigned integer, @f@ for floating-point, * and @p@ for pointer. + * + * On input, an enumerated value may be given by name or as a literal value. + * For enumerations based on numeric types, the literal values can be written + * in the same syntax as the underlying values. For enumerations based on + * pointers, the only permitted literal is `#nil', which denotes a null + * pointer. On output, names are preferred (with the underlying value given + * in a comment). */ #define DEFENUMTY(tag, ty, slot) \ @@ -1416,6 +1747,8 @@ TVEC_MISCSLOTS(DEFENUMTY) TVEC_MISCSLOTS(DEFASSOC) #undef DEFASSOC +#define TVEC_ENDENUM { 0, 0 } + /* Information about an enumerated type. */ #define DEFINFO(tag, ty, slot) \ struct tvec_##slot##enuminfo { \ @@ -1450,6 +1783,7 @@ const struct tvec_ienuminfo tvenum_bool; /* --- @tvec_claimeq_tenum@, @TVEC_CLAIMEQ_TENUM@ --- * * * Arguments: @struct tvec_state *tv@ = test-vector state + * @const struct tvec_typeenuminfo *ei@ = enumeration type info * @ty t0, t1@ = two values * @const char *file@, @unsigned @lno@ = calling file and line * @const char *expr@ = the expression to quote on failure @@ -1460,13 +1794,13 @@ const struct tvec_ienuminfo tvenum_bool; * @tvec_claim@ above, a test case is automatically begun and * ended if none is already underway. If the values are * unequal, then @tvec_fail@ is called, quoting @expr@, and the - * mismatched values are dumped: @u0@ is printed as the output - * value and @u1@ is printed as the input reference. + * mismatched values are dumped: @t0@ is printed as the output + * value and @t1@ is printed as the input reference. * * The @TVEC_CLAIM_TENUM@ 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 @u0@ and @u1@ arguments in the failure message. + * the @t0@ and @t1@ arguments in the failure message. */ #define DECLCLAIM(tag, ty, slot) \ @@ -1490,7 +1824,26 @@ TVEC_MISCSLOTS(DECLCLAIM) (tvec_claimeq_penum(tv, ei, p0, p1, \ __FILE__, __LINE__, #p0 " /= " #p1)) -/*----- Flags type -------------------------------------------------------*/ +/*----- Flags type --------------------------------------------------------*/ + +/* A flags value packs a number of fields into a single nonnegative integer. + * Symbolic names are associated with the possible values of the various + * fields; more precisely, each name is associated with a value and a + * covering bitmask. + * + * The input syntax is a sequence of items separated by `|' signs. Each item + * may be the symbolic name of a field value, or a literal unsigned integer. + * The masks associated with the given symbolic names must be disjoint. The + * resulting numerical value is simply the bitwise OR of the given values. + * + * On output, the table of symbolic names and their associated values and + * masks is repeatedly scanned, in order, to find disjoint matches -- i.e., + * entries whose value matches the target value in the bit positions + * indicated by the mask, and whose mask doesn't overlap with any previously + * found matches; the names are then output, separated by `|'. Any remaining + * nonzero bits not covered by any of the matching masks are output as a + * single literal integer, in hex. + */ extern const struct tvec_regty tvty_flags; @@ -1498,20 +1851,46 @@ struct tvec_flag { /* Definition of a single flag or bitfield value. * * Each named setting comes with a value @v@ and a mask @m@; the mask - * should cover all of the value bits, i.e., @(v&~m) == 0@. On input, - * exactly + * should cover all of the value bits, i.e., @(v&~m) == 0@. */ const char *tag; /* name */ unsigned long m, v; /* mask and value */ }; +#define TVEC_ENDFLAGS { 0, 0, 0 } + struct tvec_flaginfo { - const char *name; - const struct tvec_flag *fv; - const struct tvec_urange *range; + /* Information about a flags type. */ + + const char *name; /* type name for diagnostics */ + const struct tvec_flag *fv; /* name/mask/value mappings */ + const struct tvec_urange *range; /* permitted range for literals */ }; +/* --- @tvec_claimeq_flags@, @TVEC_CLAIMEQ_FLAGS@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const struct tvec_flaginfo *fi@ = flags type info + * @unsigned long f0, f1@ = two values + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *expr@ = the expression to quote on failure + * + * Returns: Nonzero if @f0@ and @f1@ are equal, otherwise zero. + * + * Use: Check that values of @f0@ and @f1@ are equal. As for + * @tvec_claim@ above, a test case is automatically begun and + * ended if none is already underway. If the values are + * unequal, then @tvec_fail@ is called, quoting @expr@, and the + * mismatched values are dumped: @f0@ is printed as the output + * value and @f1@ is printed as the input reference. + * + * The @TVEC_CLAIM_FLAGS@ 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 @f0@ and @f1@ arguments in the failure message. + */ + extern int tvec_claimeq_flags(struct tvec_state */*tv*/, const struct tvec_flaginfo */*fi*/, unsigned long /*f0*/, unsigned long /*f1*/, @@ -1521,7 +1900,89 @@ extern int tvec_claimeq_flags(struct tvec_state */*tv*/, (tvec_claimeq_flags(tv, fi, f0, f1, \ __FILE__, __LINE__, #f0 " /= " #f1)) +/*----- Character type ----------------------------------------------------*/ + +/* A character value holds a character, as read by @fgetc@. The special + * @EOF@ value can also be represented. + * + * On input, a character value can be given by name, with a leading `%|#|%'; + * or a character or `%|\|%'-escape sequence, optionally in single quotes. + * + * The following escape sequences and character names are recognized. + * + * * `%|#eof|%' is the special end-of-file marker. + * + * * `%|#nul|%' is the NUL character, sometimes used to terminate strings. + * + * * `%|bell|%', `%|bel|%', `%|ding|%', or `%|\a|%' is the BEL character + * used to ring the terminal bell (or do some other thing to attract the + * user's attention). + * + * * %|#backspace|%, %|#bs|%, or %|\b|% is the backspace character, used to + * move the cursor backwords by one cell. + * + * * %|#escape|% %|#esc|%, or%|\e|% is the escape character, used to + * introduce special terminal commands. + * + * * %|#formfeed|%, %|#ff|%, or %|\f|% is the formfeed character, used to + * separate pages of text. + * + * * %|#newline|%, %|#linefeed|%, %|#lf|%, %|#nl|%, or %|\n|% is the + * newline character, used to terminate lines of text or advance the + * cursor to the next line (perhaps without returning it to the start of + * the line). + * + * * %|#return|%, %|#carriage-return|%, %|#cr|%, or %|\r|% is the + * carriage-return character, used to return the cursor to the start of + * the line. + * + * * %|#tab|%, %|#horizontal-tab|%, %|#ht|%, or %|\t|% is the tab + * character, used to advance the cursor to the next tab stop on the + * current line. + * + * * %|#vertical-tab|%, %|#vt|%, %|\v|% is the vertical tab character. + * + * * %|#space|%, %|#spc|% is the space character. + * + * * %|#delete|%, %|#del|% is the delete character, used to erase the most + * recent character. + * + * * %|\'|% is the single-quote character. + * + * * %|\\|% is the backslash character. + * + * * %|\"|% is the double-quote character. + * + * * %|\NNN|% or %|\{NNN}|% is the character with code NNN in octal. The + * NNN may be up to three digits long. + * + * * %|\xNN|% or %|\x{NN}|% is the character with code NNN in hexadecimal. + */ + extern const struct tvec_regty tvty_char; + +/* --- @tvec_claimeq_char@, @TVEC_CLAIMEQ_CHAR@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @int ch0, ch1@ = two character codes + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *expr@ = the expression to quote on failure + * + * Returns: Nonzero if @ch0@ and @ch1@ are equal, otherwise zero. + * + * Use: Check that values of @ch0@ and @ch1@ are equal. As for + * @tvec_claim@ above, a test case is automatically begun and + * ended if none is already underway. If the values are + * unequal, then @tvec_fail@ is called, quoting @expr@, and the + * mismatched values are dumped: @ch0@ is printed as the output + * value and @ch1@ is printed as the input reference. + * + * The @TVEC_CLAIM_CHAR@ 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 @ch0@ and @ch1@ arguments in the failure message. + */ + extern int tvec_claimeq_char(struct tvec_state */*tv*/, int /*ch0*/, int /*ch1*/, const char */*file*/, unsigned /*lno*/, @@ -1529,100 +1990,177 @@ extern int tvec_claimeq_char(struct tvec_state */*tv*/, #define TVEC_CLAIMEQ_CHAR(tv, c0, c1) \ (tvec_claimeq_char(tv, c0, c1, __FILE__, __LINE__, #c0 " /= " #c1)) +/*----- Text and binary string types --------------------------------------*/ + +/* A string is a sequence of octets. Text and binary strings differ + * primarily in presentation: text strings are shown as raw characters where + * possible; binary strings are shown as hex dumps with an auxiliary text + * display. + * + * The input format for both kinds of strings is basically the same: a + * `compound string', consisting of + * + * * single-quoted strings, which are interpreted entirely literally, but + * can't contain single quotes or newlines; + * + * * double-quoted strings, in which `%|\|%'-escapes are interpreted as for + * characters; + * + * * character names, marked by an initial `%|#|%' sign; + * + * * special tokens marked by an initial `%|!|%' sign; or + * + * * barewords interpreted according to the current coding scheme. + * + * The special tokens are + * + * * `%|!bare|%', which causes subsequent sequences of barewords to be + * treated as plain text; + * + * * `%|!hex|%', `%|!base32|%', `%|!base64|%', which cause subsequent + * barewords to be decoded in the requested manner. + * + * * `%|!repeat|% %$n$% %|{|% %%\textit{string}%% %|}|%', which includes + * %$n$% copies of the (compound) string. + * + * Either kind of string can contain internal nul characters. A trailing nul + * is appended -- beyond the stated input length -- to input strings as a + * convenience to test functions. Test functions may include such a nul + * character on output but this is not checked by the equality test. + * + * A @struct tvec_urange@ may be supplied as an argument: the length of the + * string (in bytes) will be checked against the permitted range. + */ + extern const struct tvec_regty tvty_string, tvty_bytes; +/* --- @tvec_claimeq_string@, @TVEC_CLAIMEQ_STRING@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const char *p0@, @size_t sz0@ = first string with length + * @const char *p1@, @size_t sz1@ = second string with length + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *expr@ = the expression to quote on failure + * + * Returns: Nonzero if the strings at @p0@ and @p1@ are equal, otherwise + * zero. + * + * Use: Check that strings at @p0@ and @p1@ are equal. As for + * @tvec_claim@ above, a test case is automatically begun and + * ended if none is already underway. If the values are + * unequal, then @tvec_fail@ is called, quoting @expr@, and the + * mismatched values are dumped: @p0@ is printed as the output + * value and @p1@ is printed as the input reference. + * + * The @TVEC_CLAIM_STRING@ 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 @ch0@ and @ch1@ arguments in the failure message. + */ + extern int tvec_claimeq_string(struct tvec_state */*tv*/, const char */*p0*/, size_t /*sz0*/, const char */*p1*/, size_t /*sz1*/, const char */*file*/, unsigned /*lno*/, const char */*expr*/); +#define TVEC_CLAIMEQ_STRING(tv, p0, sz0, p1, sz1) \ + (tvec_claimeq_string(tv, p0, sz0, p1, sz1, __FILE__, __LINE__, \ + #p0 "[" #sz0 "] /= " #p1 "[" #sz1 "]")) + +/* --- @tvec_claimeq_strz@, @TVEC_CLAIMEQ_STRZ@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const char *p0, *p1@ = two strings to compare + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *expr@ = the expression to quote on failure + * + * Returns: Nonzero if the strings at @p0@ and @p1@ are equal, otherwise + * zero. + * + * Use: Check that strings at @p0@ and @p1@ are equal, as for + * @tvec_claimeq_string@, except that the strings are assumed + * null-terminated, so their lengths don't need to be supplied + * explicitly. The macro is similarly like + * @TVEC_CLAIMEQ_STRING@. + */ + extern int tvec_claimeq_strz(struct tvec_state */*tv*/, const char */*p0*/, const char */*p1*/, const char */*file*/, unsigned /*lno*/, const char */*expr*/); +#define TVEC_CLAIMEQ_STRZ(tv, p0, p1) \ + (tvec_claimeq_strz(tv, p0, p1, __FILE__, __LINE__, #p0 " /= " #p1)) + +/* --- @tvec_claimeq_bytes@, @TVEC_CLAIMEQ_BYTES@ --- * + * + * Arguments: @struct tvec_state *tv@ = test-vector state + * @const void *p0@, @size_t sz0@ = first string with length + * @const void *p1@, @size_t sz1@ = second string with length + * @const char *file@, @unsigned @lno@ = calling file and line + * @const char *expr@ = the expression to quote on failure + * + * Returns: Nonzero if the strings at @p0@ and @p1@ are equal, otherwise + * zero. + * + * Use: Check that binary strings at @p0@ and @p1@ are equal. As for + * @tvec_claim@ above, a test case is automatically begun and + * ended if none is already underway. If the values are + * unequal, then @tvec_fail@ is called, quoting @expr@, and the + * mismatched values are dumped: @p0@ is printed as the output + * value and @p1@ is printed as the input reference. + * + * The @TVEC_CLAIM_STRING@ 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 @ch0@ and @ch1@ arguments in the failure message. + */ + extern int tvec_claimeq_bytes(struct tvec_state */*tv*/, const void */*p0*/, size_t /*sz0*/, const void */*p1*/, size_t /*sz1*/, const char */*file*/, unsigned /*lno*/, const char */*expr*/); -#define TVEC_CLAIMEQ_STRING(tv, p0, sz0, p1, sz1) \ - (tvec_claimeq_string(tv, p0, sz0, p1, sz1, __FILE__, __LINE__, \ - #p0 "[" #sz0 "] /= " #p1 "[" #sz1 "]")) -#define TVEC_CLAIMEQ_STRZ(tv, p0, p1) \ - (tvec_claimeq_strz(tv, p0, p1, __FILE__, __LINE__, #p0 " /= " #p1)) #define TVEC_CLAIMEQ_BYTES(tv, p0, sz0, p1, sz1) \ (tvec_claimeq(tv, p0, sz0, p1, sz1, __FILE__, __LINE__, \ #p0 "[" #sz0 "] /= " #p1 "[" #sz1 "]")) -extern const struct tvec_regty tvty_buffer; +/* --- @tvec_allocstring@, @tvec_allocbytes@ --- * + * + * Arguments: @union tvec_regval *rv@ = register value + * @size_t sz@ = required size + * + * Returns: --- + * + * Use: Allocated space in a text or binary string register. If the + * current register size is sufficient, its buffer is left + * alone; otherwise, the old buffer, if any, is freed and a + * fresh buffer allocated. These functions are not intended to + * be used to adjust a buffer repeatedly, e.g., while building + * output incrementally: (a) they will perform badly, and (b) + * the old buffer contents are simply discarded if reallocation + * is necessary. Instead, use a @dbuf@ or @dstr@. + * + * The @tvec_allocstring@ function sneakily allocates an extra + * byte for a terminating zero. The @tvec_allocbytes@ function + * doesn't do this. + */ extern void tvec_allocstring(union tvec_regval */*rv*/, size_t /*sz*/); extern void tvec_allocbytes(union tvec_regval */*rv*/, size_t /*sz*/); -/*----- Benchmarking ------------------------------------------------------*/ - -struct tvec_bench { - struct tvec_env _env; /* benchmarking is an environment */ - struct bench_state **bst; /* benchmark state anchor or null */ - unsigned long niter; /* iterations done per unit */ - int riter, rbuf; /* iterations and buffer registers */ - const struct tvec_env *env; /* environment (per test, not grp) */ -}; -#define TVEC_BENCHENV \ - { sizeof(struct tvec_benchctx), \ - tvec_benchsetup, \ - tvec_benchset, \ - tvec_benchbefore, \ - tvec_benchrun, \ - tvec_benchafter, \ - tvec_benchteardown } -#define TVEC_BENCHINIT TVEC_BENCHENV, &tvec_benchstate - -struct tvec_benchctx { - const struct tvec_bench *b; - struct bench_state *bst; - double dflt_target; - void *subctx; -}; - -extern struct bench_state *tvec_benchstate; - -extern int tvec_benchsetup(struct tvec_state */*tv*/, - const struct tvec_env */*env*/, - void */*pctx*/, void */*ctx*/); -extern int tvec_benchset(struct tvec_state */*tv*/, const char */*var*/, - const struct tvec_env */*env*/, void */*ctx*/); -extern int tvec_benchbefore(struct tvec_state */*tv*/, void */*ctx*/); -extern void tvec_benchrun(struct tvec_state */*tv*/, - tvec_testfn */*fn*/, void */*ctx*/); -extern void tvec_benchafter(struct tvec_state */*tv*/, void */*ctx*/); -extern void tvec_benchteardown(struct tvec_state */*tv*/, void */*ctx*/); - -extern void tvec_benchreport - (const struct gprintf_ops */*gops*/, void */*go*/, - unsigned /*unit*/, const struct bench_timing */*tm*/); - -/*----- Command-line interface --------------------------------------------*/ - -extern const struct tvec_config tvec_adhocconfig; - -extern void tvec_parseargs(int /*argc*/, char */*argv*/[], - struct tvec_state */*tv_out*/, - int */*argpos_out*/, - const struct tvec_config */*config*/); - -extern int tvec_readstdin(struct tvec_state */*tv*/); -extern int tvec_readfile(struct tvec_state */*tv*/, const char */*file*/); -extern int tvec_readdflt(struct tvec_state */*tv*/, const char */*file*/); -extern int tvec_readarg(struct tvec_state */*tv*/, const char */*arg*/); +/*----- Buffer type -------------------------------------------------------*/ -extern int tvec_readargs(int /*argc*/, char */*argv*/[], - struct tvec_state */*tv*/, - int */*argpos_inout*/, const char */*dflt*/); +/* Buffer registers are primarily used for benchmarking. Only a buffer's + * size is significant: its contents are ignored on comparison and output, + * and unspecified on input. + * + * The input is simply the buffer size, as an integer, optionally suffixed + * with a unit `kB', `MB', `GB', `TB', `PB', `EB', `ZB', `YB' (with or + * without the `B') denoting a power of 1024. Units are used on output only + * when the size would be expressed exactly. + */ -extern int tvec_main(int /*argc*/, char */*argv*/[], - const struct tvec_config */*config*/, - const char */*dflt*/); +extern const struct tvec_regty tvty_buffer; /*----- That's all, folks -------------------------------------------------*/ diff --git a/utils/Makefile.am b/utils/Makefile.am index 7807721..ffbe920 100644 --- a/utils/Makefile.am +++ b/utils/Makefile.am @@ -82,6 +82,10 @@ pkginclude_HEADERS += linreg.h libutils_la_SOURCES += linreg.c ##LIBMANS += linreg.3 +## Mathematics. +pkginclude_HEADERS += maths.h +##LIBMANS += maths.3 + ## String handling. pkginclude_HEADERS += str.h libutils_la_SOURCES += str.c diff --git a/utils/gprintf.c b/utils/gprintf.c index f527186..b575e1f 100644 --- a/utils/gprintf.c +++ b/utils/gprintf.c @@ -563,6 +563,54 @@ int gprintf(const struct gprintf_ops *ops, void *out, const char *p, ...) return (n); } +/*----- Utilities ---------------------------------------------------------*/ + +/* --- @gprintf_memputf@ --- * + * + * Arguments: @char **buf_inout@ = address of output buffer pointer + * @size_t *sz_inout@ = address of buffer size + * @size_t maxsz@ = buffer size needed for this operation + * @const char *p@ = pointer to format string + * @va_list *ap@ = captured format-arguments tail + * + * Returns: The formatted length. + * + * Use: Generic utility for mostly implementing the @nputf@ output + * function, if you don't have a better option. + * + * On entry, @*buf_inout@ should be null or a buffer pointer, + * with @*sz_inout@ either zero or the buffer's size, + * respectively. On exit, @*buf_input@ and @*sz_inout@ will be + * updated, if necessary, to describe a sufficiently large + * buffer, and the formatted string will have been written to + * the buffer. + * + * When the buffer is no longer required, free it using @xfree@. + */ + +size_t gprintf_memputf(char **buf_inout, size_t *sz_inout, + size_t maxsz, const char *p, va_list ap) +{ + char *buf = *buf_inout; + size_t sz = *sz_inout; + int n; + + if (sz <= maxsz) { + if (!sz) sz = 32; + while (sz <= maxsz) sz *= 2; + if (buf) xfree(buf); + buf = xmalloc(sz); *buf_inout = buf; *sz_inout = sz; + } + +#ifdef HAVE_SNPRINTF + n = vsnprintf(buf, maxsz + 1, p, ap); +#else + n = vsprintf(buf, p, ap); +#endif + assert(0 <= n && n <= maxsz); + return (n); +} + /*----- Standard printers -------------------------------------------------*/ static int file_putch(void *out, int ch) diff --git a/utils/gprintf.h b/utils/gprintf.h index 9e32338..0f06dd3 100644 --- a/utils/gprintf.h +++ b/utils/gprintf.h @@ -86,6 +86,33 @@ extern int PRINTF_LIKE(3, 4) gprintf(const struct gprintf_ops */*ops*/, void */*out*/, const char */*p*/, ...); +/* --- @gprintf_memputf@ --- * + * + * Arguments: @char **buf_inout@ = address of output buffer pointer + * @size_t *sz_inout@ = address of buffer size + * @size_t maxsz@ = buffer size needed for this operation + * @const char *p@ = pointer to format string + * @va_list *ap@ = captured format-arguments tail + * + * Returns: The formatted length. + * + * Use: Generic utility for mostly implementing the @nputf@ output + * function, if you don't have a better option. + * + * On entry, @*buf_inout@ should be null or a buffer pointer, + * with @*sz_inout@ either zero or the buffer's size, + * respectively. On exit, @*buf_input@ and @*sz_inout@ will be + * updated, if necessary, to describe a sufficiently large + * buffer, and the formatted string will have been written to + * the buffer. + * + * When the buffer is no longer required, free it using @xfree@. + */ + +extern size_t gprintf_memputf(char **/*buf_inout*/, size_t */*sz_inout*/, + size_t /*maxsz*/, + const char */*p*/, va_list /*ap*/); + /*----- That's all, folks -------------------------------------------------*/ #ifdef __cplusplus diff --git a/utils/maths.h b/utils/maths.h new file mode 100644 index 0000000..37227e9 --- /dev/null +++ b/utils/maths.h @@ -0,0 +1,91 @@ +/* -*-c-*- + * + * Mathematical utilities + * + * (c) 2023 Straylight/Edgeware + */ + +/*----- Licensing notice --------------------------------------------------* + * + * This file is part of the mLib utilities library. + * + * mLib is free software: you can redistribute it and/or modify it under + * the terms of the GNU Library General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * mLib is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public + * License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with mLib. If not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + * USA. + */ + +#ifndef MLIB_MATHS_H +#define MLIB_MATHS_H + +#ifdef __cplusplus + extern "C" { +#endif + +/*----- Header files ------------------------------------------------------*/ + +#include + +/*----- Macros provided ---------------------------------------------------*/ + +/* --- @NANP@ --- * + * + * Arguments: @floatish x@ = a floating-point number (evaluated multiple + * times) + * + * Returns: Nonzero if @x@ is not-a-number. + */ + +#ifdef isnan +# define NANP(x) isnan(x) +#else +# define NANP(x) (!((x) == (x))) +#endif + +/* --- @INFP@ --- * + * + * Arguments: @floatish x@ = a floating-point number (evaluated multiple + * times) + * + * Returns: Nonzero if @x@ is infinite. + */ + +#ifdef isinf +# define INFP(x) isinf(x) +#else +# define INFP(x) ((x) > LDBL_MAX || (x) < -LDBL_MAX) +#endif + +/* --- @NEGP@ --- * + * + * Arguments: @floatish x@ = a floating-point number (evaluated multiple + * times) + * + * Returns: Nonzero if @x@ is negative. This won't give the right answer + * for negative zero unless the system provides explicit + * support. + */ + +#ifdef signbit +# define NEGP(x) signbit(x) +#else +# define NEGP(x) ((x) < 0) +#endif + +/*----- That's all, folks -------------------------------------------------*/ + +#ifdef __cplusplus + } +#endif + +#endif