1081 lines
25 KiB
C
1081 lines
25 KiB
C
/*
|
|
* Copyright 2018 Garrett D'Amore <garrett@damore.org>
|
|
*
|
|
* This software is supplied under the terms of the MIT License, a
|
|
* copy of which should be located in the distribution where this
|
|
* file was obtained (LICENSE.txt). A copy of the license may also be
|
|
* found online at https://opensource.org/licenses/MIT.
|
|
*/
|
|
|
|
/*
|
|
* This contains some of the guts of the testing framework. It is in a single
|
|
* file in order to simplify use and minimize external dependencies.
|
|
*
|
|
* If you use this with threads, you need to either have pthreads (and link
|
|
* your test program against the threading library), or you need Windows.
|
|
* Support for C11 threading is not implemented yet.
|
|
*
|
|
* For timing, this code needs a decent timer. It will use clock_gettime
|
|
* if it appears to be present, or the Win32 QueryPerformanceCounter, or
|
|
* gettimeofday() if neither of those are available.
|
|
*
|
|
* This code is unlikely to function at all on anything that isn't a UNIX
|
|
* or Windows system. As we think its unlikely that you'd want to use this
|
|
* to run testing inside an embedded device or something, we think this is a
|
|
* reasonable limitation.
|
|
*
|
|
* Note that we expect that on Windows, you have a reasonably current
|
|
* version of MSVC. (Specifically we need a few C99-isms that Microsoft
|
|
* only added late -- like in 2010. Specifically uint32_t and uint64_t).
|
|
*/
|
|
|
|
#include <setjmp.h>
|
|
#include <stdarg.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
|
|
#else
|
|
|
|
#if NNG_HAVE_LANGINFO
|
|
#include <langinfo.h>
|
|
#include <locale.h>
|
|
#endif
|
|
|
|
#include <sys/time.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#ifndef CONVEY_NO_THREADS
|
|
#include <pthread.h>
|
|
#endif
|
|
|
|
#endif
|
|
|
|
#include "convey.h"
|
|
|
|
/*
|
|
* About symbol naming. We use Go-like conventions to help set expectations,
|
|
* even though we cannot necessarily count on the linker to prevent
|
|
* access. We have to accept that we may be inlined directly into the
|
|
* user's program, so we try not to pollute their namespace. (Convenience
|
|
* pollution may be enabled in convey.h.)
|
|
*
|
|
* Symbols exposed to users directly are named ConveyXXX using CamelCase
|
|
* (just like Go).
|
|
*
|
|
* Symbols used internally, but which must be exposed for external linkage
|
|
* will be named using conveyXXX (camelCase with the leading "c" lower.)
|
|
*
|
|
* Symbols used internally, and kept entirely within the the .c file, are
|
|
* named convey_xxx (using underscores).
|
|
*
|
|
* When symbols can reasonably be expected not to collide and are local to
|
|
* a scope not expressed to user code, these rules are relaxed.
|
|
*/
|
|
|
|
static const char *convey_sym_pass = ".";
|
|
static const char *convey_sym_skip = "?";
|
|
static const char *convey_sym_fail = "X";
|
|
static const char *convey_nocolor = "";
|
|
static const char *convey_green = "";
|
|
static const char *convey_red = "";
|
|
static const char *convey_yellow = "";
|
|
|
|
static int convey_debug = 0;
|
|
static int convey_verbose = 0;
|
|
static int convey_nassert = 0;
|
|
static int convey_nskip = 0;
|
|
static const char *convey_assert_color = "";
|
|
|
|
#if defined(_WIN32)
|
|
static WORD convey_defattr;
|
|
static HANDLE convey_console;
|
|
#endif
|
|
|
|
#define CONVEY_EXIT_OK 0
|
|
#define CONVEY_EXIT_FAIL 2
|
|
#define CONVEY_EXIT_FATAL 3
|
|
#define CONVEY_EXIT_NOMEM 4
|
|
|
|
struct convey_timer {
|
|
uint64_t timer_base;
|
|
uint64_t timer_count;
|
|
uint64_t timer_rate;
|
|
int timer_running;
|
|
};
|
|
|
|
struct convey_log {
|
|
char * log_buf;
|
|
size_t log_size;
|
|
size_t log_length;
|
|
};
|
|
|
|
struct convey_ctx {
|
|
char ctx_name[256];
|
|
struct convey_ctx * ctx_parent;
|
|
struct convey_ctx * ctx_root; /* the root node on the list */
|
|
struct convey_ctx * ctx_next; /* root list only, cleanup */
|
|
int ctx_level;
|
|
int ctx_done;
|
|
int ctx_started;
|
|
jmp_buf * ctx_jmp;
|
|
int ctx_fatal;
|
|
int ctx_fail;
|
|
int ctx_skip;
|
|
int ctx_printed;
|
|
struct convey_timer ctx_timer;
|
|
struct convey_log * ctx_errlog;
|
|
struct convey_log * ctx_faillog;
|
|
struct convey_log * ctx_dbglog;
|
|
};
|
|
|
|
static void convey_print_result(struct convey_ctx *);
|
|
static void convey_init_timer(struct convey_timer *);
|
|
static void convey_start_timer(struct convey_timer *);
|
|
static void convey_stop_timer(struct convey_timer *);
|
|
static void convey_read_timer(struct convey_timer *, int *, int *);
|
|
static void convey_init_term(void);
|
|
static int convey_tls_init(void);
|
|
static void *convey_tls_get(void);
|
|
static int convey_tls_set(void *);
|
|
static struct convey_ctx *convey_get_ctx(void);
|
|
static void convey_vlogf(struct convey_log *, const char *, va_list, int);
|
|
static void convey_logf(struct convey_log *, const char *, ...);
|
|
static void convey_log_emit(struct convey_log *, const char *, const char *);
|
|
static void convey_log_free(struct convey_log *);
|
|
static struct convey_log *convey_log_alloc(void);
|
|
static char * convey_nextline(char **);
|
|
static void convey_emit_color(const char *);
|
|
|
|
/*
|
|
* convey_emit_color just changes the output text to the color
|
|
* requested. It is Windows console aware.
|
|
*/
|
|
static void
|
|
convey_emit_color(const char *color)
|
|
{
|
|
#if defined(_WIN32)
|
|
|
|
if (convey_console != INVALID_HANDLE_VALUE) {
|
|
WORD attr;
|
|
|
|
attr = convey_defattr &
|
|
~(FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED |
|
|
FOREGROUND_INTENSITY);
|
|
|
|
if (color == convey_nocolor) {
|
|
attr = convey_defattr;
|
|
} else if (color == convey_yellow) {
|
|
attr |= FOREGROUND_GREEN | FOREGROUND_RED |
|
|
FOREGROUND_INTENSITY;
|
|
} else if (color == convey_green) {
|
|
attr |= FOREGROUND_GREEN | FOREGROUND_INTENSITY;
|
|
} else if (color == convey_red) {
|
|
attr |= FOREGROUND_RED | FOREGROUND_INTENSITY;
|
|
} else {
|
|
return;
|
|
}
|
|
(void) fflush(stdout);
|
|
SetConsoleTextAttribute(convey_console, attr);
|
|
} else {
|
|
(void) fputs(color, stdout);
|
|
}
|
|
#else
|
|
(void) fputs(color, stdout);
|
|
#endif
|
|
}
|
|
|
|
/*
|
|
* convey_print_result prints the test results. It prints more information
|
|
* in convey_verbose mode. Note that its possible for assertion checks done at
|
|
* a given block to be recorded in a deeper block, since we can't easily
|
|
* go back up to the old line and print it.
|
|
*
|
|
* We also leverage this point to detect completion of a root context, and
|
|
* deallocate the child contexts. The root context should never be reentered
|
|
* here.
|
|
*/
|
|
static void
|
|
convey_print_result(struct convey_ctx *t)
|
|
{
|
|
int secs, usecs;
|
|
|
|
if (t->ctx_root == t) {
|
|
convey_stop_timer(&t->ctx_timer); /* This is idempotent */
|
|
|
|
convey_read_timer(&t->ctx_timer, &secs, &usecs);
|
|
|
|
(void) convey_logf(t->ctx_dbglog, "Test %s: %s (%d.%02ds)\n",
|
|
t->ctx_fatal ? "FATAL"
|
|
: t->ctx_fail
|
|
? "FAIL"
|
|
: t->ctx_skip ? "PASS (with SKIPs)" : "PASS",
|
|
t->ctx_name, secs, usecs / 10000);
|
|
|
|
if (convey_verbose) {
|
|
(void) puts("");
|
|
}
|
|
convey_log_emit(t->ctx_errlog, "Errors:", convey_red);
|
|
convey_log_emit(t->ctx_faillog, "Failures:", convey_yellow);
|
|
if (convey_debug) {
|
|
convey_log_emit(t->ctx_dbglog, "Log:", convey_nocolor);
|
|
}
|
|
if (convey_verbose) {
|
|
(void) puts("");
|
|
(void) puts("");
|
|
convey_emit_color(convey_assert_color);
|
|
(void) printf(
|
|
"%d assertions thus far", convey_nassert);
|
|
convey_emit_color(convey_nocolor);
|
|
|
|
if (convey_nskip) {
|
|
(void) fputs(" ", stdout);
|
|
convey_emit_color(convey_yellow);
|
|
(void) fputs(
|
|
"(one or more sections skipped)", stdout);
|
|
convey_emit_color(convey_nocolor);
|
|
}
|
|
(void) printf("\n\n--- %s: %s (%d.%02ds)\n",
|
|
t->ctx_fatal ? "FATAL"
|
|
: t->ctx_fail ? "FAIL" : "PASS",
|
|
t->ctx_name, secs, usecs / 10000);
|
|
}
|
|
|
|
/* Remove the context, because we cannot reenter here */
|
|
convey_tls_set(NULL);
|
|
|
|
while (t != NULL) {
|
|
struct convey_ctx *freeit = t;
|
|
if (t->ctx_root == t) {
|
|
convey_log_free(t->ctx_dbglog);
|
|
convey_log_free(t->ctx_faillog);
|
|
convey_log_free(t->ctx_errlog);
|
|
}
|
|
t = t->ctx_next;
|
|
memset(freeit, 0, sizeof(*freeit));
|
|
free(freeit);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* conveyStart is called when the context starts, before any call to
|
|
* setjmp is made. If the context isn't initialized already, that is
|
|
* done. Note that this code gets called multiple times when the
|
|
* context is reentered, which is why the context used must be statically
|
|
* allocated -- a record that it has already done is checked. If
|
|
* the return value is zero, then this block has already been executed,
|
|
* and it should be skipped. Otherwise, it needs to be done.
|
|
*/
|
|
int
|
|
conveyStart(conveyScope *scope, const char *name)
|
|
{
|
|
struct convey_ctx *t, *parent;
|
|
|
|
parent = convey_get_ctx();
|
|
|
|
if ((t = scope->cs_data) != NULL) {
|
|
if (t->ctx_done) {
|
|
convey_print_result(t);
|
|
return (1); /* all done, skip */
|
|
}
|
|
return (0); /* continue onward */
|
|
}
|
|
scope->cs_data = (t = calloc(1, sizeof(struct convey_ctx)));
|
|
if (t == NULL) {
|
|
goto allocfail;
|
|
}
|
|
t->ctx_jmp = &scope->cs_jmp;
|
|
|
|
(void) snprintf(t->ctx_name, sizeof(t->ctx_name) - 1, "%s", name);
|
|
|
|
if (parent != NULL) {
|
|
t->ctx_parent = parent;
|
|
t->ctx_root = t->ctx_parent->ctx_root;
|
|
t->ctx_level = t->ctx_parent->ctx_level + 1;
|
|
/* unified logging against the root context */
|
|
t->ctx_dbglog = t->ctx_root->ctx_dbglog;
|
|
t->ctx_faillog = t->ctx_root->ctx_faillog;
|
|
t->ctx_errlog = t->ctx_root->ctx_errlog;
|
|
t->ctx_next = t->ctx_root->ctx_next;
|
|
t->ctx_root->ctx_next = t;
|
|
} else {
|
|
t->ctx_parent = t;
|
|
t->ctx_root = t;
|
|
if (((t->ctx_errlog = convey_log_alloc()) == NULL) ||
|
|
((t->ctx_faillog = convey_log_alloc()) == NULL) ||
|
|
((t->ctx_dbglog = convey_log_alloc()) == NULL)) {
|
|
goto allocfail;
|
|
}
|
|
convey_logf(t->ctx_dbglog, "Test Started: %s\n", t->ctx_name);
|
|
}
|
|
return (0);
|
|
|
|
allocfail:
|
|
if (t != NULL) {
|
|
convey_log_free(t->ctx_errlog);
|
|
convey_log_free(t->ctx_dbglog);
|
|
convey_log_free(t->ctx_faillog);
|
|
free(t);
|
|
scope->cs_data = NULL;
|
|
}
|
|
if (parent != NULL) {
|
|
ConveyError("Unable to allocate context");
|
|
}
|
|
return (1);
|
|
}
|
|
|
|
/*
|
|
* conveyLoop is called right after setjmp. If unwind is true it indicates
|
|
* that setjmp returned true, and we are unwinding the stack. In that case
|
|
* we perform a local cleanup and keep popping back up the stack. We
|
|
* always come through this, even if the test finishes successfully, so
|
|
* that we can do this stack unwind. If we are unwinding, and we are
|
|
* at the root context, then we pritn the results and return non-zero
|
|
* so that our caller knows to stop further processing.
|
|
*/
|
|
int
|
|
conveyLoop(conveyScope *scope, int unwind)
|
|
{
|
|
struct convey_ctx *t;
|
|
int i;
|
|
|
|
if ((t = scope->cs_data) == NULL) {
|
|
return (1);
|
|
}
|
|
if (unwind) {
|
|
if ((t->ctx_parent != t) && (t->ctx_parent != NULL)) {
|
|
longjmp(*t->ctx_parent->ctx_jmp, 1);
|
|
}
|
|
if (t->ctx_done) {
|
|
convey_print_result(t);
|
|
return (1);
|
|
}
|
|
}
|
|
if (!t->ctx_started) {
|
|
t->ctx_started = 1;
|
|
|
|
if (convey_verbose) {
|
|
if (t->ctx_root == t) {
|
|
(void) printf("=== RUN: %s\n", t->ctx_name);
|
|
} else {
|
|
(void) puts("");
|
|
for (i = 0; i < t->ctx_level; i++) {
|
|
(void) fputs(" ", stdout);
|
|
}
|
|
(void) printf("%s ", t->ctx_name);
|
|
(void) fflush(stdout);
|
|
}
|
|
}
|
|
|
|
convey_init_timer(&t->ctx_timer);
|
|
convey_start_timer(&t->ctx_timer);
|
|
}
|
|
/* Reset TC for the following code. */
|
|
convey_tls_set(t);
|
|
return (0);
|
|
}
|
|
|
|
void
|
|
conveyFinish(conveyScope *scope, int *rvp)
|
|
{
|
|
struct convey_ctx *t;
|
|
|
|
if ((t = scope->cs_data) == NULL) {
|
|
/* allocation failure */
|
|
*rvp = CONVEY_EXIT_NOMEM;
|
|
return;
|
|
}
|
|
t->ctx_done = 1;
|
|
if (rvp != NULL) {
|
|
/* exit code 1 is reserved for usage errors */
|
|
if (t->ctx_fatal) {
|
|
*rvp = CONVEY_EXIT_FATAL;
|
|
} else if (t->ctx_fail) {
|
|
*rvp = CONVEY_EXIT_FAIL;
|
|
} else {
|
|
*rvp = CONVEY_EXIT_OK;
|
|
}
|
|
}
|
|
longjmp(*t->ctx_jmp, 1);
|
|
}
|
|
|
|
void
|
|
conveySkip(const char *file, int line, const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
struct convey_ctx *t = convey_get_ctx();
|
|
struct convey_log *dlog = t->ctx_dbglog;
|
|
|
|
if (convey_verbose) {
|
|
convey_emit_color(convey_yellow);
|
|
(void) fputs(convey_sym_skip, stdout);
|
|
convey_emit_color(convey_nocolor);
|
|
}
|
|
convey_logf(dlog, "* %s (%s:%d) (Skip): ", t->ctx_name, file, line);
|
|
va_start(ap, fmt);
|
|
convey_vlogf(dlog, fmt, ap, 1);
|
|
va_end(ap);
|
|
t->ctx_done = 1; /* This forces an end */
|
|
convey_nskip++;
|
|
longjmp(*t->ctx_jmp, 1);
|
|
}
|
|
|
|
void
|
|
conveyAssertFail(const char *cond, const char *file, int line)
|
|
{
|
|
struct convey_ctx *t = convey_get_ctx();
|
|
|
|
convey_nassert++;
|
|
if (convey_verbose) {
|
|
convey_emit_color(convey_yellow);
|
|
(void) fputs(convey_sym_fail, stdout);
|
|
convey_emit_color(convey_nocolor);
|
|
(void) fflush(stdout);
|
|
}
|
|
if (t->ctx_root != t) {
|
|
t->ctx_root->ctx_fail++;
|
|
}
|
|
convey_assert_color = convey_yellow;
|
|
t->ctx_fail++;
|
|
t->ctx_done = 1; /* This forces an end */
|
|
convey_logf(t->ctx_faillog, "* Assertion Failed (%s)\n", t->ctx_name);
|
|
convey_logf(t->ctx_faillog, "File: %s\n", file);
|
|
convey_logf(t->ctx_faillog, "Line: %d\n", line);
|
|
convey_logf(t->ctx_faillog, "Test: %s\n\n", cond);
|
|
convey_logf(t->ctx_dbglog, "* %s (%s:%d) (FAILED): %s\n", t->ctx_name,
|
|
file, line, cond);
|
|
longjmp(*t->ctx_jmp, 1);
|
|
}
|
|
|
|
void
|
|
conveyAssertPass(const char *cond, const char *file, int line)
|
|
{
|
|
struct convey_ctx *t = convey_get_ctx();
|
|
|
|
convey_nassert++;
|
|
if (convey_verbose) {
|
|
convey_emit_color(convey_green);
|
|
(void) fputs(convey_sym_pass, stdout);
|
|
convey_emit_color(convey_nocolor);
|
|
(void) fflush(stdout);
|
|
}
|
|
convey_logf(t->ctx_dbglog, "* %s (%s:%d) (Passed): %s\n", t->ctx_name,
|
|
file, line, cond);
|
|
}
|
|
|
|
void
|
|
conveyAssertSkip(const char *cond, const char *file, int line)
|
|
{
|
|
struct convey_ctx *t = convey_get_ctx();
|
|
|
|
convey_nskip++;
|
|
if (convey_verbose) {
|
|
convey_emit_color(convey_yellow);
|
|
(void) fputs(convey_sym_pass, stdout);
|
|
convey_emit_color(convey_nocolor);
|
|
(void) fflush(stdout);
|
|
}
|
|
convey_logf(t->ctx_dbglog, "* %s (%s:%d) (Skip): %s\n", t->ctx_name,
|
|
file, line, cond);
|
|
}
|
|
|
|
/*
|
|
* Performance counters. Really we just want to start and stop timers, to
|
|
* measure elapsed time in usec.
|
|
*/
|
|
static void
|
|
convey_init_timer(struct convey_timer *pc)
|
|
{
|
|
memset(pc, 0, sizeof(*pc));
|
|
}
|
|
|
|
static void
|
|
convey_start_timer(struct convey_timer *pc)
|
|
{
|
|
if (pc->timer_running) {
|
|
return;
|
|
}
|
|
#if defined(_WIN32)
|
|
LARGE_INTEGER pcnt, pfreq;
|
|
QueryPerformanceCounter(&pcnt);
|
|
QueryPerformanceFrequency(&pfreq);
|
|
pc->timer_base = pcnt.QuadPart;
|
|
pc->timer_rate = pfreq.QuadPart;
|
|
#elif defined(CLOCK_MONOTONIC) && !defined(CONVEY_USE_GETTIMEOFDAY)
|
|
struct timespec ts;
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
pc->timer_base = ts.tv_sec;
|
|
pc->timer_base *= 1000000000;
|
|
pc->timer_base += ts.tv_nsec;
|
|
pc->timer_rate = 1000000000;
|
|
#else
|
|
struct timeval tv;
|
|
|
|
gettimeofday(&tv, NULL);
|
|
pc->timer_base = tv.tv_sec;
|
|
pc->timer_base *= 1000000;
|
|
pc->timer_base += tv.tv_usec;
|
|
pc->timer_rate = 1000000;
|
|
#endif
|
|
pc->timer_running = 1;
|
|
}
|
|
|
|
static void
|
|
convey_stop_timer(struct convey_timer *pc)
|
|
{
|
|
if (!pc->timer_running) {
|
|
return;
|
|
}
|
|
do {
|
|
#if defined(_WIN32)
|
|
LARGE_INTEGER pcnt;
|
|
QueryPerformanceCounter(&pcnt);
|
|
pc->timer_count += (pcnt.QuadPart - pc->timer_base);
|
|
#elif defined(CLOCK_MONOTONIC) && !defined(CONVEY_USE_GETTIMEOFDAY)
|
|
uint64_t ns;
|
|
struct timespec ts;
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
ns = ts.tv_sec;
|
|
ns *= 1000000000;
|
|
ns += (uint64_t) ts.tv_nsec;
|
|
pc->timer_count += (ns - pc->timer_base);
|
|
#else
|
|
uint64_t us;
|
|
struct timeval tv;
|
|
|
|
gettimeofday(&tv, NULL);
|
|
us = tv.tv_sec;
|
|
us *= 1000000;
|
|
us += tv.tv_usec;
|
|
pc->timer_count += (us - pc->timer_base);
|
|
#endif
|
|
} while (0);
|
|
}
|
|
|
|
static void
|
|
convey_read_timer(struct convey_timer *pc, int *secp, int *usecp)
|
|
{
|
|
uint64_t delta, rate, sec, usec;
|
|
|
|
delta = pc->timer_count;
|
|
rate = pc->timer_rate;
|
|
|
|
sec = delta / rate;
|
|
delta -= (sec * rate);
|
|
|
|
/*
|
|
* done this way we avoid dividing rate by 1M -- and the above
|
|
* ensures we don't wrap.
|
|
*/
|
|
usec = (delta * 1000000) / rate;
|
|
|
|
if (secp) {
|
|
*secp = (int) sec;
|
|
}
|
|
if (usecp) {
|
|
*usecp = (int) usec;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Thread-specific data. Pthreads uses one way, Win32 another. If you
|
|
* lack threads, just #define CONVEY_NO_THREADS. C11 thread support is
|
|
* pending.
|
|
*/
|
|
|
|
#ifdef CONVEY_NO_THREADS
|
|
static void *convey_tls_key;
|
|
|
|
static int
|
|
convey_tls_init(void)
|
|
{
|
|
return (0);
|
|
}
|
|
|
|
static int
|
|
convey_tls_set(void *v)
|
|
{
|
|
convey_tls_key = v;
|
|
return (0);
|
|
}
|
|
|
|
static void *
|
|
convey_tls_get(void)
|
|
{
|
|
return (convey_tls_key);
|
|
}
|
|
|
|
#elif defined(_WIN32)
|
|
|
|
static DWORD convey_tls_key;
|
|
|
|
static int
|
|
convey_tls_init(void)
|
|
{
|
|
if ((convey_tls_key = TlsAlloc()) == TLS_OUT_OF_INDEXES) {
|
|
return (-1);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
static int
|
|
convey_tls_set(void *v)
|
|
{
|
|
if (!TlsSetValue(convey_tls_key, v)) {
|
|
return (-1);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
static void *
|
|
convey_tls_get(void)
|
|
{
|
|
return ((void *) TlsGetValue(convey_tls_key));
|
|
}
|
|
|
|
#else
|
|
|
|
static pthread_key_t convey_tls_key;
|
|
|
|
static int
|
|
convey_tls_init(void)
|
|
{
|
|
if (pthread_key_create(&convey_tls_key, NULL) != 0) {
|
|
return (-1);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
static int
|
|
convey_tls_set(void *v)
|
|
{
|
|
if (pthread_setspecific(convey_tls_key, v) != 0) {
|
|
return (-1);
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
static void *
|
|
convey_tls_get(void)
|
|
{
|
|
return (pthread_getspecific(convey_tls_key));
|
|
}
|
|
|
|
#endif
|
|
|
|
static struct convey_ctx *
|
|
convey_get_ctx(void)
|
|
{
|
|
return (convey_tls_get());
|
|
}
|
|
|
|
/*
|
|
* Log stuff.
|
|
*/
|
|
static void
|
|
convey_vlogf(struct convey_log *log, const char *fmt, va_list va, int addnl)
|
|
{
|
|
/* Grow the log buffer if we need to */
|
|
while ((log->log_size - log->log_length) < 256) {
|
|
size_t newsz = log->log_size + 2000;
|
|
char * ptr = malloc(newsz);
|
|
if (ptr == NULL) {
|
|
return;
|
|
}
|
|
memcpy(ptr, log->log_buf, log->log_length);
|
|
memset(ptr + log->log_length, 0, newsz - log->log_length);
|
|
free(log->log_buf);
|
|
log->log_buf = ptr;
|
|
log->log_size = newsz;
|
|
}
|
|
|
|
/* 2 allows space for NULL, and newline */
|
|
(void) vsnprintf(log->log_buf + log->log_length,
|
|
log->log_size - (log->log_length + 2), fmt, va);
|
|
log->log_length += strlen(log->log_buf + log->log_length);
|
|
if (addnl && (log->log_buf[log->log_length - 1] != '\n')) {
|
|
log->log_buf[log->log_length++] = '\n';
|
|
}
|
|
}
|
|
|
|
static void
|
|
convey_logf(struct convey_log *log, const char *fmt, ...)
|
|
{
|
|
va_list va;
|
|
|
|
va_start(va, fmt);
|
|
convey_vlogf(log, fmt, va, 0);
|
|
va_end(va);
|
|
}
|
|
|
|
static void
|
|
convey_log_emit(struct convey_log *log, const char *header, const char *color)
|
|
{
|
|
char *s;
|
|
char *last = log->log_buf;
|
|
|
|
if (log->log_length == 0) {
|
|
return;
|
|
}
|
|
|
|
(void) fputs("\n\n", stdout);
|
|
convey_emit_color(color);
|
|
(void) fputs(header, stdout);
|
|
convey_emit_color(convey_nocolor);
|
|
(void) fputs("\n\n", stdout);
|
|
while ((s = convey_nextline(&last)) != NULL) {
|
|
(void) fputs(" ", stdout);
|
|
convey_emit_color(color);
|
|
(void) fputs(s, stdout);
|
|
convey_emit_color(convey_nocolor);
|
|
(void) fputs("\n", stdout);
|
|
}
|
|
}
|
|
|
|
static void
|
|
convey_log_free(struct convey_log *log)
|
|
{
|
|
if (log != NULL) {
|
|
if (log->log_size != 0) {
|
|
free(log->log_buf);
|
|
}
|
|
free(log);
|
|
}
|
|
}
|
|
|
|
static struct convey_log *
|
|
convey_log_alloc(void)
|
|
{
|
|
return (calloc(1, sizeof(struct convey_log)));
|
|
}
|
|
|
|
/*
|
|
* ConveyInit initializes some common global stuff. Call it from main(),
|
|
* if you don't use the framework provided main.
|
|
*/
|
|
int
|
|
ConveyInit(void)
|
|
{
|
|
static int inited;
|
|
|
|
if (!inited) {
|
|
if (convey_tls_init() != 0) {
|
|
return (-1);
|
|
}
|
|
convey_init_term();
|
|
inited = 1;
|
|
}
|
|
return (0);
|
|
}
|
|
|
|
void
|
|
ConveySetVerbose(void)
|
|
{
|
|
convey_verbose = 1;
|
|
}
|
|
|
|
void
|
|
conveyFail(const char *file, int line, const char *fmt, ...)
|
|
{
|
|
struct convey_ctx *t = convey_get_ctx();
|
|
struct convey_log *flog = t->ctx_faillog;
|
|
struct convey_log *dlog = t->ctx_dbglog;
|
|
va_list ap;
|
|
|
|
convey_logf(dlog, "* %s (%s:%d) (Failed): ", t->ctx_name, file, line);
|
|
va_start(ap, fmt);
|
|
convey_vlogf(dlog, fmt, ap, 1);
|
|
va_end(ap);
|
|
|
|
convey_logf(flog, "* %s\n", t->ctx_root->ctx_name);
|
|
convey_logf(flog, "File: %s\n", file);
|
|
convey_logf(flog, "Line: %d\n", line);
|
|
convey_logf(flog, "Reason: ");
|
|
va_start(ap, fmt);
|
|
convey_vlogf(flog, fmt, ap, 1);
|
|
va_end(ap);
|
|
|
|
if (t->ctx_root != t) {
|
|
t->ctx_root->ctx_fail++;
|
|
}
|
|
convey_assert_color = convey_yellow;
|
|
t->ctx_fail++;
|
|
t->ctx_done = 1; /* This forces an end */
|
|
longjmp(*t->ctx_jmp, 1);
|
|
}
|
|
|
|
void
|
|
conveyError(const char *file, int line, const char *fmt, ...)
|
|
{
|
|
struct convey_ctx *t = convey_get_ctx();
|
|
struct convey_log *flog = t->ctx_errlog;
|
|
struct convey_log *dlog = t->ctx_dbglog;
|
|
va_list ap;
|
|
|
|
convey_logf(dlog, "* %s (%s:%d) (Error): ", t->ctx_name, file, line);
|
|
va_start(ap, fmt);
|
|
convey_vlogf(dlog, fmt, ap, 1);
|
|
va_end(ap);
|
|
|
|
convey_logf(flog, "* %s\n", t->ctx_root->ctx_name);
|
|
convey_logf(flog, "File: %s\n", file);
|
|
convey_logf(flog, "Line: %d\n", line);
|
|
convey_logf(flog, "Reason: ");
|
|
va_start(ap, fmt);
|
|
convey_vlogf(flog, fmt, ap, 1);
|
|
va_end(ap);
|
|
|
|
if (t->ctx_root != t) {
|
|
t->ctx_root->ctx_fail++;
|
|
}
|
|
convey_assert_color = convey_red;
|
|
t->ctx_fail++;
|
|
t->ctx_done = 1; /* This forces an end */
|
|
longjmp(*t->ctx_jmp, 1);
|
|
}
|
|
|
|
void
|
|
conveyPrintf(const char *file, int line, const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
struct convey_ctx *t = convey_get_ctx();
|
|
struct convey_log *dlog = t->ctx_dbglog;
|
|
|
|
convey_logf(dlog, "* %s (%s:%d) (Debug): ", t->ctx_name, file, line);
|
|
va_start(ap, fmt);
|
|
convey_vlogf(dlog, fmt, ap, 1);
|
|
va_end(ap);
|
|
}
|
|
|
|
static void
|
|
convey_init_term(void)
|
|
{
|
|
const char *term;
|
|
|
|
#ifndef _WIN32
|
|
/* Windows console doesn't do Unicode (consistently). */
|
|
|
|
#if NNG_HAVE_LANGINFO
|
|
const char *codeset;
|
|
|
|
(void) setlocale(LC_ALL, "");
|
|
codeset = nl_langinfo(CODESET);
|
|
if ((codeset != NULL) && (strcmp(codeset, "UTF-8") == 0)) {
|
|
convey_sym_pass = "✔";
|
|
convey_sym_fail = "✘";
|
|
convey_sym_skip = "⚠";
|
|
}
|
|
#endif
|
|
term = getenv("TERM");
|
|
if (!isatty(fileno(stdin))) {
|
|
term = NULL;
|
|
}
|
|
|
|
#else
|
|
CONSOLE_SCREEN_BUFFER_INFO info;
|
|
|
|
convey_console = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
if (!GetConsoleScreenBufferInfo(convey_console, &info)) {
|
|
convey_console = INVALID_HANDLE_VALUE;
|
|
} else {
|
|
convey_defattr = info.wAttributes;
|
|
// Values probably don't matter, just need to be
|
|
// different!
|
|
convey_nocolor = "\033[0m";
|
|
convey_green = "\033[32m";
|
|
convey_yellow = "\033[33m";
|
|
convey_red = "\033[31m";
|
|
}
|
|
term = getenv("TERM");
|
|
#endif
|
|
|
|
if (term != NULL) {
|
|
if ((strstr(term, "xterm") != NULL) ||
|
|
(strstr(term, "ansi") != NULL) ||
|
|
(strstr(term, "color") != NULL)) {
|
|
convey_nocolor = "\033[0m";
|
|
convey_green = "\033[32m";
|
|
convey_yellow = "\033[33m";
|
|
convey_red = "\033[31m";
|
|
}
|
|
}
|
|
convey_assert_color = convey_green;
|
|
}
|
|
|
|
/*
|
|
* This function exists because strtok isn't safe, and strtok_r and
|
|
* strsep are not universally available. Its like strsep, but only does
|
|
* newlines. Could be implemented using strpbrk, but this is probably
|
|
* faster since we are only looking for a single character.
|
|
*/
|
|
static char *
|
|
convey_nextline(char **next)
|
|
{
|
|
char *line = *next;
|
|
char *nl;
|
|
char c;
|
|
|
|
if (line == NULL) {
|
|
return (NULL);
|
|
}
|
|
for (nl = line; (c = (*nl)) != '\0'; nl++) {
|
|
if (c == '\n') {
|
|
*nl = '\0';
|
|
*next = nl + 1;
|
|
return (line);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* If the last character in the file is a newline, treat it as
|
|
* the end. (This will appear as a blank last line.)
|
|
*/
|
|
if (*line == '\0') {
|
|
line = NULL;
|
|
}
|
|
*next = NULL;
|
|
return (line);
|
|
}
|
|
|
|
static struct convey_env {
|
|
struct convey_env *next;
|
|
const char * name;
|
|
char * value;
|
|
} * convey_environment;
|
|
|
|
static struct convey_env *
|
|
conveyFindEnv(const char *name)
|
|
{
|
|
struct convey_env *ev;
|
|
for (ev = convey_environment; ev != NULL; ev = ev->next) {
|
|
if (strcmp(name, ev->name) == 0) {
|
|
return (ev);
|
|
}
|
|
}
|
|
return (NULL);
|
|
}
|
|
|
|
char *
|
|
conveyGetEnv(const char *name)
|
|
{
|
|
struct convey_env *ev;
|
|
|
|
if ((ev = conveyFindEnv(name)) != NULL) {
|
|
return (ev->value);
|
|
}
|
|
return (getenv(name));
|
|
}
|
|
|
|
int
|
|
conveyPutEnv(const char *name, char *value)
|
|
{
|
|
struct convey_env *env;
|
|
|
|
if ((env = conveyFindEnv(name)) == NULL) {
|
|
env = malloc(sizeof(*env));
|
|
if (env == NULL) {
|
|
return (-1);
|
|
}
|
|
env->next = convey_environment;
|
|
convey_environment = env;
|
|
}
|
|
env->name = name;
|
|
env->value = value;
|
|
return (0);
|
|
}
|
|
|
|
int
|
|
conveyMain(int argc, char **argv)
|
|
{
|
|
int i;
|
|
const char * status;
|
|
const char * prog = "<unknown>";
|
|
struct convey_timer pc;
|
|
int secs, usecs;
|
|
struct convey_env * env;
|
|
|
|
if ((argc > 0) && (argv[0] != NULL)) {
|
|
prog = argv[0];
|
|
}
|
|
|
|
/*
|
|
* Poor man's getopt. Very poor. We should add a way for tests
|
|
* to retrieve additional test specific options.
|
|
*/
|
|
for (i = 1; i < argc; i++) {
|
|
if (argv[i][0] != '-') {
|
|
break;
|
|
}
|
|
if (strcmp(argv[i], "-v") == 0) {
|
|
ConveySetVerbose();
|
|
continue;
|
|
}
|
|
if (strcmp(argv[i], "-d") == 0) {
|
|
convey_debug++;
|
|
continue;
|
|
}
|
|
if ((strcmp(argv[i], "-p") == 0) && ((i + 1) < argc)) {
|
|
char *delim;
|
|
if ((delim = strchr(argv[i + 1], '=')) != NULL) {
|
|
*delim = '\0';
|
|
conveyPutEnv(argv[i + 1], delim + 1);
|
|
} else {
|
|
conveyPutEnv(argv[i + 1], "");
|
|
}
|
|
i++;
|
|
continue;
|
|
}
|
|
}
|
|
if (ConveyInit() != 0) {
|
|
(void) fputs("Cannot initialize test framework\n", stderr);
|
|
exit(CONVEY_EXIT_NOMEM);
|
|
}
|
|
|
|
convey_init_timer(&pc);
|
|
convey_start_timer(&pc);
|
|
i = conveyMainImpl();
|
|
convey_stop_timer(&pc);
|
|
|
|
switch (i) {
|
|
case CONVEY_EXIT_NOMEM:
|
|
(void) fputs("Cannot initialize root test context\n", stderr);
|
|
exit(CONVEY_EXIT_NOMEM);
|
|
case CONVEY_EXIT_OK:
|
|
if (convey_verbose) {
|
|
(void) puts("PASS");
|
|
}
|
|
status = "ok";
|
|
break;
|
|
case CONVEY_EXIT_FAIL:
|
|
status = "FAIL";
|
|
if (convey_verbose) {
|
|
(void) puts("FAIL");
|
|
}
|
|
break;
|
|
default:
|
|
status = "FATAL";
|
|
if (convey_verbose) {
|
|
(void) puts("FATAL");
|
|
}
|
|
break;
|
|
}
|
|
|
|
convey_read_timer(&pc, &secs, &usecs);
|
|
(void) printf(
|
|
"%-8s%-52s%4d.%03ds\n", status, prog, secs, usecs / 1000);
|
|
while ((env = convey_environment) != NULL) {
|
|
convey_environment = env->next;
|
|
free(env);
|
|
}
|
|
convey_read_timer(&pc, &secs, &usecs);
|
|
exit(i);
|
|
}
|