libui/test/lib/testing.c

315 lines
7.1 KiB
C

// 27 february 2018
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <string.h>
#include "timer.h"
#include "testing.h"
#include "testingpriv.h"
struct defer {
void (*f)(testingT *, void *);
void *data;
struct defer *next;
};
#ifdef _MSC_VER
// Microsoft defines jmp_buf with a __declspec(align()), and for whatever reason, they have a warning that triggers when you use that for any reason, and that warning is enabled with /W4
// Silence the warning; it's harmless.
#pragma warning(push)
#pragma warning(disable: 4324)
#endif
struct testingT {
// set at test creation time
const char *name;
void (*f)(testingT *, void *);
void *data;
// for sorting tests in a set; not used by subtests
const char *file;
long line;
// test status
bool failed;
bool skipped;
bool returned;
jmp_buf returnNowBuf;
// deferred functions
struct defer *defers;
bool defersRun;
// execution options
testingOptions opts;
// output
testingprivOutbuf *outbuf;
};
#ifdef _MSC_VER
#pragma warning(pop)
#endif
static void initTest(testingT *t, const char *name, void (*f)(testingT *, void *), void *data, const char *file, long line)
{
memset(t, 0, sizeof (testingT));
t->name = name;
t->f = f;
t->data = data;
t->file = file;
t->line = line;
}
struct testingSet {
testingprivArray tests;
};
static testingSet mainTests = { testingprivArrayStaticInit(testingT, 32, "testingT[]") };
void testingprivSetRegisterTest(testingSet **pset, const char *name, void (*f)(testingT *, void *), void *data, const char *file, long line)
{
testingSet *set;
testingT *t;
set = &mainTests;
if (pset != NULL) {
set = *pset;
if (set == NULL) {
set = testingprivNew(testingSet);
testingprivArrayInit(set->tests, testingT, 32, "testingT[]");
*pset = set;
}
}
t = (testingT *) testingprivArrayAppend(&(set->tests), 1);
initTest(t, name, f, data, file, line);
}
static int testcmp(const void *a, const void *b)
{
const testingT *ta = (const testingT *) a;
const testingT *tb = (const testingT *) b;
int ret;
ret = strcmp(ta->file, tb->file);
if (ret != 0)
return ret;
if (ta->line < tb->line)
return -1;
if (ta->line > tb->line)
return 1;
return 0;
}
static void runDefers(testingT *t)
{
struct defer *d;
if (t->defersRun)
return;
t->defersRun = true;
for (d = t->defers; d != NULL; d = d->next)
(*(d->f))(t, d->data);
// and now free the defers
// we could use t->defers == NULL instead of t->defersRun but then recursive calls to runDefers() (for instance, if a deferred function calls testingTFatalf()) would explode
while (t->defers != NULL) {
d = t->defers;
t->defers = t->defers->next;
testingprivFree(d);
}
}
static const testingOptions defaultOptions = {
.Verbose = 0,
};
static bool testingprivTRun(testingT *t, testingprivOutbuf *parentbuf)
{
const char *status;
timerTime start, end;
char timerstr[timerDurationStringLen];
bool printStatus;
if (t->opts.Verbose)
testingprivOutbufPrintf(parentbuf, "=== RUN %s\n", t->name);
t->outbuf = testingprivNewOutbuf();
start = timerMonotonicNow();
if (setjmp(t->returnNowBuf) == 0)
(*(t->f))(t, t->data);
end = timerMonotonicNow();
t->returned = true;
runDefers(t);
printStatus = t->opts.Verbose;
status = "PASS";
if (t->failed) {
status = "FAIL";
printStatus = true; // always print status on failure
} else if (t->skipped)
// note that failed overrides skipped
status = "SKIP";
timerDurationString(timerTimeSub(end, start), timerstr);
if (printStatus) {
testingprivOutbufPrintf(parentbuf, "--- %s: %s (%s)\n", status, t->name, timerstr);
testingprivOutbufAppendOutbuf(parentbuf, t->outbuf);
}
testingprivOutbufFree(t->outbuf);
t->outbuf = NULL;
return !t->failed;
}
// TODO make options and opts consistent throughout? what about format vs fmt in public-facing functions?
static void testingprivSetRun(testingSet *set, const testingOptions *opts, testingprivOutbuf *outbuf, bool *anyFailed)
{
size_t i;
testingT *t;
testingprivArrayQsort(&(set->tests), testcmp);
t = (testingT *) (set->tests.buf);
for (i = 0; i < set->tests.len; i++) {
t->opts = *opts;
if (!testingprivTRun(t, outbuf))
*anyFailed = true;
t++;
}
}
void testingSetRun(testingSet *set, const struct testingOptions *options, bool *anyRun, bool *anyFailed)
{
*anyRun = false;
*anyFailed = false;
if (set == NULL)
set = &mainTests;
if (options == NULL)
options = &defaultOptions;
if (set->tests.len == 0)
return;
testingprivSetRun(set, options, NULL, anyFailed);
*anyRun = true;
}
void testingprivTLogfFullThen(testingT *t, void (*then)(testingT *t), const char *file, long line, const char *format, ...)
{
va_list ap;
va_start(ap, format);
// run va_end() *before* then, just to be safe
testingprivTLogvfFullThen(t, NULL, file, line, format, ap);
va_end(ap);
if (then != NULL)
(*then)(t);
}
static const char *basename(const char *file)
{
const char *p;
for (;;) {
p = strpbrk(file, "/\\");
if (p == NULL)
break;
file = p + 1;
}
return file;
}
void testingprivTLogvfFullThen(testingT *t, void (*then)(testingT *t), const char *file, long line, const char *format, va_list ap)
{
testingprivOutbufPrintf(t->outbuf, "%s:%ld: ", basename(file), line);
testingprivOutbufVprintfIndented(t->outbuf, format, ap);
testingprivOutbufPrintf(t->outbuf, "\n");
if (then != NULL)
(*then)(t);
}
void testingTFail(testingT *t)
{
t->failed = true;
}
static void returnNow(testingT *t)
{
if (!t->returned) {
// set this now so a FailNow inside a Defer doesn't longjmp twice
t->returned = true;
// run defers before calling longjmp() just to be safe
runDefers(t);
longjmp(t->returnNowBuf, 1);
}
}
void testingTFailNow(testingT *t)
{
testingTFail(t);
returnNow(t);
}
void testingTSkipNow(testingT *t)
{
t->skipped = true;
returnNow(t);
}
void testingTDefer(testingT *t, void (*f)(testingT *t, void *data), void *data)
{
struct defer *d;
d = testingprivNew(struct defer);
d->f = f;
d->data = data;
// add to the head of the list so defers are run in reverse order of how they were added
d->next = t->defers;
t->defers = d;
}
void testingTRun(testingT *t, const char *subname, void (*subfunc)(testingT *t, void *data), void *data)
{
testingT *subt;
testingprivOutbuf *rewrittenName;
char *fullName;
rewrittenName = testingprivNewOutbuf();
while (*subname != '\0') {
const char *replaced;
replaced = NULL;
switch (*subname) {
case ' ':
case '\f':
case '\n':
case '\r':
case '\t':
case '\v':
replaced = "_";
break;
case '\a':
replaced = "\\a";
break;
case '\b':
replaced = "\\b";
break;
}
if (replaced != NULL)
testingprivOutbufPrintf(rewrittenName, "%s", replaced);
else if (isprint(*subname))
testingprivOutbufPrintf(rewrittenName, "%c", *subname);
else
testingprivOutbufPrintf(rewrittenName, "\\x%x", (unsigned int) (*subname));
subname++;
}
fullName = testingprivSmprintf("%s/%s", t->name, testingprivOutbufString(rewrittenName));
testingprivOutbufFree(rewrittenName);
subt = testingprivNew(testingT);
initTest(subt, fullName, subfunc, data, NULL, 0);
subt->opts = t->opts;
if (!testingprivTRun(subt, t->outbuf))
t->failed = true;
testingprivFree(subt);
testingprivFree(fullName);
}