cairo/test/cairo-test-runner.c
Chris Wilson e90073f7dd [test] Build test suite into single binary.
Avoid calling libtool to link every single test case, by building just one
binary from all the sources.

This binary is then given the task of choosing tests to run (based on user
selection and individual test requirement), forking each test into its own
process and accumulating the results.
2008-10-31 12:30:11 +00:00

907 lines
21 KiB
C

/*
* Copyright © 2008 Chris Wilson
*
* Permission to use, copy, modify, distribute, and sell this software
* and its documentation for any purpose is hereby granted without
* fee, provided that the above copyright notice appear in all copies
* and that both that copyright notice and this permission notice
* appear in supporting documentation, and that the name of
* Chris Wilson not be used in advertising or publicity pertaining to
* distribution of the software without specific, written prior
* permission. Chris Wilson makes no representations about the
* suitability of this software for any purpose. It is provided "as
* is" without express or implied warranty.
*
* CHRIS WILSON DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
* SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS, IN NO EVENT SHALL CHRIS WILSON BE LIABLE FOR ANY SPECIAL,
* INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
* RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
* OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
* IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
* Author: Chris Wilson <chris@chris-wilson.co.uk>
*/
#include "cairo-test-private.h"
#include "cairo-boilerplate-getopt.h"
/* get the "real" version info instead of dummy cairo-version.h */
#undef CAIRO_VERSION_H
#undef CAIRO_VERSION_MAJOR
#undef CAIRO_VERSION_MINOR
#undef CAIRO_VERSION_MICRO
#include "../cairo-version.h"
#include <pixman.h> /* for version information */
#if HAVE_FORK && HAVE_WAITPID
#if HAVE_UNISTD_H
#include <unistd.h>
#endif
#if HAVE_SIGNAL_H
#include <signal.h>
#endif
#include <sys/types.h>
#include <sys/wait.h>
#endif
typedef struct _cairo_test_list {
const cairo_test_t *test;
struct _cairo_test_list *next;
} cairo_test_list_t;
typedef enum {
GE,
GT
} cairo_test_compare_op_t;
static cairo_test_list_t *tests;
static void CAIRO_BOILERPLATE_PRINTF_FORMAT(2,3)
_log (cairo_test_context_t *ctx,
const char *fmt,
...)
{
va_list ap;
va_start (ap, fmt);
vprintf (fmt, ap);
va_end (ap);
va_start (ap, fmt);
cairo_test_logv (ctx, fmt, ap);
va_end (ap);
}
static cairo_test_list_t *
_list_prepend (cairo_test_list_t *head, const cairo_test_t *test)
{
cairo_test_list_t *list;
list = xmalloc (sizeof (cairo_test_list_t));
list->test = test;
list->next = head;
head = list;
return head;
}
static cairo_test_list_t *
_list_reverse (cairo_test_list_t *head)
{
cairo_test_list_t *list, *next;
for (list = head, head = NULL; list != NULL; list = next) {
next = list->next;
list->next = head;
head = list;
}
return head;
}
static void
_list_free (cairo_test_list_t *list)
{
while (list != NULL) {
cairo_test_list_t *next = list->next;
free (list);
list = next;
}
}
#if HAVE_FORK && HAVE_WAITPID
static cairo_test_status_t
_cairo_test_wait (pid_t pid)
{
int exitcode;
if (waitpid (pid, &exitcode, 0) != pid)
return CAIRO_TEST_CRASHED;
if (WIFSIGNALED (exitcode)) {
switch (WTERMSIG (exitcode)) {
case SIGINT:
#if HAVE_RAISE
raise (SIGINT);
#endif
return CAIRO_TEST_UNTESTED;
default:
return CAIRO_TEST_CRASHED;
}
}
return WEXITSTATUS (exitcode);
}
#endif
static cairo_test_status_t
_cairo_test_runner_preamble (cairo_test_context_t *ctx)
{
#if HAVE_FORK && HAVE_WAITPID
pid_t pid;
switch ((pid = fork ())) {
case -1: /* error */
return CAIRO_TEST_UNTESTED;
case 0: /* child */
exit (ctx->test->preamble (ctx));
default:
return _cairo_test_wait (pid);
}
#else
return ctx->test->preamble (ctx);
#endif
}
static cairo_test_status_t
_cairo_test_runner_draw (cairo_test_context_t *ctx,
const cairo_boilerplate_target_t *target,
cairo_bool_t similar,
int device_offset)
{
#if HAVE_FORK && HAVE_WAITPID
pid_t pid;
switch ((pid = fork ())) {
case -1: /* error */
return CAIRO_TEST_UNTESTED;
case 0: /* child */
exit (_cairo_test_context_run_for_target (ctx, target,
similar, device_offset));
default:
return _cairo_test_wait (pid);
}
#else
return _cairo_test_context_run_for_target (ctx, target,
similar, device_offset);
#endif
}
static void
append_argv (int *argc, char ***argv, const char *str)
{
int old_argc;
char **old_argv;
cairo_bool_t doit;
const char *s, *t;
int olen;
int len;
int i;
if (str == NULL)
return;
old_argc = *argc;
old_argv = *argv;
doit = FALSE;
do {
if (doit)
*argv = xmalloc (sizeof (char *) * (1 + *argc) + olen);
olen = sizeof (char *) * (1 + *argc);
for (i = 0; i < old_argc; i++) {
len = strlen (old_argv[i]) + 1;
if (doit) {
(*argv)[i] = (char *) *argv + olen;
memcpy ((*argv)[i], old_argv[i], len);
}
olen += len;
}
s = str;
while ((t = strpbrk (s, " \t,:;")) != NULL) {
if (t - s) {
len = t - s;
if (doit) {
(*argv)[i] = (char *) *argv + olen;
memcpy ((*argv)[i], s, len);
(*argv)[i][len] = '\0';
}
olen += len + 1;
i++;
}
s = t + 1;
}
if (*s != '\0') {
len = strlen (s) + 1;
if (doit) {
(*argv)[i] = (char *) *argv + olen;
memcpy ((*argv)[i], s, len);
}
olen += len;
i++;
}
} while (doit++ == FALSE);
(*argv)[i] = NULL;
*argc += i;
}
typedef struct _cairo_test_runner {
cairo_test_context_t base;
unsigned int num_device_offsets;
int num_passed;
int num_xpassed;
int num_skipped;
int num_failed;
int num_xfailed;
int num_crashed;
cairo_test_list_t **crashes_per_target;
cairo_test_list_t **fails_per_target;
cairo_test_list_t **xpasses_per_target;
int *num_failed_per_target;
int *num_crashed_per_target;
int *num_xpassed_per_target;
cairo_bool_t list_only;
cairo_bool_t full_test;
} cairo_test_runner_t;
static void
usage (const char *argv0)
{
fprintf (stderr,
"Usage: %s [-f] [test-names|keywords ...]\n"
" %s -l\n"
"\n"
"Run the cairo conformance test suite over the given tests (all by default)\n"
"The command-line arguments are interpreted as follows:\n"
"\n"
" -l list only; just list selected test case names without executing\n"
" -f full; run the full set of tests. By default the test suite\n"
" skips similar surface and device offset testing.\n"
"\n"
"If test names are given they are used as exact matches either to a specific\n"
"test case or to a keyword, so a command such as\n"
"\"cairo-test-suite text\" can be used to run all text test cases.\n",
argv0, argv0);
}
static void
_parse_cmdline (cairo_test_runner_t *runner, int *argc, char **argv[])
{
int c;
while (1) {
c = _cairo_getopt (*argc, *argv, ":l");
if (c == -1)
break;
switch (c) {
case 'l':
runner->list_only = TRUE;
break;
case 'f':
runner->full_test = TRUE;
break;
default:
fprintf (stderr, "Internal error: unhandled option: %c\n", c);
/* fall-through */
case '?':
usage ((*argv)[0]);
exit (1);
}
}
*argc -= optind;
*argv += optind;
}
static void
_runner_init (cairo_test_runner_t *runner)
{
cairo_test_init (&runner->base, "cairo-test-suite");
runner->xpasses_per_target = xcalloc (sizeof (cairo_test_list_t *),
runner->base.num_targets);
runner->fails_per_target = xcalloc (sizeof (cairo_test_list_t *),
runner->base.num_targets);
runner->crashes_per_target = xcalloc (sizeof (cairo_test_list_t *),
runner->base.num_targets);
runner->num_xpassed_per_target = xcalloc (sizeof (int),
runner->base.num_targets);
runner->num_failed_per_target = xcalloc (sizeof (int),
runner->base.num_targets);
runner->num_crashed_per_target = xcalloc (sizeof (int),
runner->base.num_targets);
}
static void
_runner_print_versions (cairo_test_runner_t *runner)
{
_log (&runner->base,
"Compiled against cairo %s, running on %s.\n",
CAIRO_VERSION_STRING, cairo_version_string ());
_log (&runner->base,
"Compiled against pixman %s, running on %s.\n",
PIXMAN_VERSION_STRING, pixman_version_string ());
fflush (runner->base.log_file);
}
static void
_runner_print_summary (cairo_test_runner_t *runner)
{
_log (&runner->base,
"%d Passed [%d unexpectedly], %d Failed [%d crashed, %d expected], %d Skipped\n",
runner->num_passed + runner->num_xpassed,
runner->num_xpassed,
runner->num_failed + runner->num_crashed + runner->num_xfailed,
runner->num_crashed,
runner->num_xfailed,
runner->num_skipped);
}
static void
_runner_print_details (cairo_test_runner_t *runner)
{
unsigned int n;
for (n = 0; n < runner->base.num_targets; n++) {
const cairo_boilerplate_target_t *target;
cairo_test_list_t *list;
target = runner->base.targets_to_test[n];
if (runner->num_crashed_per_target[n]) {
_log (&runner->base, "%s (%s): %d crashed! -",
target->name,
cairo_boilerplate_content_name (target->content),
runner->num_crashed_per_target[n]);
for (list = runner->crashes_per_target[n];
list != NULL;
list = list->next)
{
char *name = cairo_test_get_name (list->test);
_log (&runner->base, " %s", name);
free (name);
}
_log (&runner->base, "\n");
}
if (runner->num_failed_per_target[n]) {
_log (&runner->base, "%s (%s): %d failed -",
target->name,
cairo_boilerplate_content_name (target->content),
runner->num_failed_per_target[n]);
for (list = runner->fails_per_target[n];
list != NULL;
list = list->next)
{
char *name = cairo_test_get_name (list->test);
_log (&runner->base, " %s", name);
free (name);
}
_log (&runner->base, "\n");
}
if (runner->num_xpassed_per_target[n]) {
_log (&runner->base, "%s (%s): %d unexpectedly passed -",
target->name,
cairo_boilerplate_content_name (target->content),
runner->num_xpassed_per_target[n]);
for (list = runner->xpasses_per_target[n];
list != NULL;
list = list->next)
{
char *name = cairo_test_get_name (list->test);
_log (&runner->base, " %s", name);
free (name);
}
_log (&runner->base, "\n");
}
}
}
static void
_runner_print_results (cairo_test_runner_t *runner)
{
_runner_print_summary (runner);
_runner_print_details (runner);
}
static cairo_test_status_t
_runner_fini (cairo_test_runner_t *runner)
{
unsigned int n;
for (n = 0; n < runner->base.num_targets; n++) {
_list_free (runner->crashes_per_target[n]);
_list_free (runner->fails_per_target[n]);
_list_free (runner->xpasses_per_target[n]);
}
free (runner->crashes_per_target);
free (runner->fails_per_target);
free (runner->xpasses_per_target);
free (runner->num_crashed_per_target);
free (runner->num_failed_per_target);
free (runner->num_xpassed_per_target);
cairo_test_fini (&runner->base);
return runner->num_failed + runner->num_crashed ?
CAIRO_TEST_FAILURE :
runner->num_passed + runner->num_xpassed + runner->num_xfailed ?
CAIRO_TEST_SUCCESS : CAIRO_TEST_UNTESTED;
}
static cairo_bool_t
_version_compare (int a, cairo_test_compare_op_t op, int b)
{
switch (op) {
case GT: return a > b;
case GE: return a >= b;
default: return FALSE;
}
}
static cairo_bool_t
_get_required_version (const char *str,
cairo_test_compare_op_t *op,
int *major,
int *minor,
int *micro)
{
while (*str == ' ')
str++;
if (strncmp (str, ">=", 2) == 0) {
*op = GE;
str += 2;
} else if (strncmp (str, ">", 1) == 0) {
*op = GT;
str += 1;
} else
return FALSE;
while (*str == ' ')
str++;
if (sscanf (str, "%d.%d.%d", major, minor, micro) != 3) {
*micro = 0;
if (sscanf (str, "%d.%d", major, minor) != 2)
return FALSE;
}
return TRUE;
}
static cairo_bool_t
_has_required_cairo_version (const char *str)
{
cairo_test_compare_op_t op;
int major, minor, micro;
if (! _get_required_version (str + 5 /* advance over "cairo" */,
&op, &major, &minor, &micro))
{
fprintf (stderr, "unrecognised cairo version requirement '%s'\n", str);
return FALSE;
}
return _version_compare (cairo_version (),
op,
CAIRO_VERSION_ENCODE (major, minor, micro));
}
static cairo_bool_t
_has_required_ghostscript_version (const char *str)
{
#if ! CAIRO_CAN_TEST_PS_SURFACE
return TRUE;
#endif
str += 2; /* advance over "gs" */
return TRUE;
}
static cairo_bool_t
_has_required_poppler_version (const char *str)
{
#if ! CAIRO_CAN_TEST_PDF_SURFACE
return TRUE;
#endif
str += 7; /* advance over "poppler" */
return TRUE;
}
static cairo_bool_t
_has_required_rsvg_version (const char *str)
{
#if ! CAIRO_CAN_TEST_SVG_SURFACE
return TRUE;
#endif
str += 4; /* advance over "rsvg" */
return TRUE;
}
int
main (int argc, char **argv)
{
cairo_test_runner_t runner;
cairo_test_list_t *list;
cairo_test_status_t *target_status;
unsigned int n, m;
char targets[4096];
int len;
#ifdef _MSC_VER
/* We don't want an assert dialog, we want stderr */
_CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE);
_CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR);
#endif
#ifndef CAIRO_HAS_CONSTRUCTOR_ATTRIBUTE
_cairo_test_runner_register_tests ();
#endif
memset (&runner, 0, sizeof (runner));
runner.num_device_offsets = 1;
if (getenv ("CAIRO_TEST_MODE")) {
const char *env = getenv ("CAIRO_TEST_MODE");
if (strstr (env, "full")) {
runner.full_test = TRUE;
}
}
_parse_cmdline (&runner, &argc, &argv);
append_argv (&argc, &argv, getenv ("CAIRO_TESTS"));
if (runner.full_test) {
runner.num_device_offsets = 2;
}
target_status = NULL; /* silence the compiler */
if (! runner.list_only) {
_runner_init (&runner);
_runner_print_versions (&runner);
target_status = xmalloc (sizeof (cairo_test_status_t) *
runner.base.num_targets);
}
for (list = tests; list != NULL; list = list->next) {
cairo_test_context_t ctx;
cairo_test_status_t status, expectation;
cairo_bool_t failed = FALSE, crashed = FALSE, skipped = TRUE;
const char *XFAIL = NULL;
char *name = cairo_test_get_name (list->test);
int i;
/* check for restricted runs */
if (argc) {
cairo_bool_t found = FALSE;
const char *keywords = list->test->keywords;
for (i = 0; i < argc; i++) {
const char *match = argv[i];
cairo_bool_t invert = match[0] == '!';
if (invert)
match++;
/* exact match on test name */
if (strcmp (name, match) == 0) {
found = ! invert;
break;
} else if (invert) {
found = TRUE;
}
/* keyword match */
if (keywords != NULL && strstr (keywords, match) != NULL) {
found = ! invert;
break;
} else if (invert) {
found = TRUE;
}
}
if (! found) {
free (name);
continue;
}
}
/* check to see if external requirements match */
if (list->test->requirements != NULL) {
const char *requirements = list->test->requirements;
const char *str;
str = strstr (requirements, "cairo");
if (str != NULL && ! _has_required_cairo_version (str)) {
if (runner.list_only)
goto TEST_NEXT;
else
goto TEST_SKIPPED;
}
str = strstr (requirements, "gs");
if (str != NULL && ! _has_required_ghostscript_version (str)) {
if (runner.list_only)
goto TEST_NEXT;
else
goto TEST_SKIPPED;
}
str = strstr (requirements, "poppler");
if (str != NULL && ! _has_required_poppler_version (str)) {
if (runner.list_only)
goto TEST_NEXT;
else
goto TEST_SKIPPED;
}
str = strstr (requirements, "rsvg");
if (str != NULL && ! _has_required_rsvg_version (str)) {
if (runner.list_only)
goto TEST_NEXT;
else
goto TEST_SKIPPED;
}
}
if (runner.list_only) {
printf ("%s ", name);
goto TEST_NEXT;
}
/* check for a failing test */
expectation = CAIRO_TEST_SUCCESS;
if (list->test->keywords != NULL &&
(XFAIL = strstr (list->test->keywords, "XFAIL")) != NULL)
{
if (XFAIL[5] == '=') {
/* backend specific expected failure */
XFAIL += 5;
} else {
XFAIL = NULL;
expectation = CAIRO_TEST_FAILURE;
}
}
_cairo_test_context_init_for_test (&ctx, &runner.base, list->test);
memset (target_status, 0,
sizeof (cairo_test_status_t) * ctx.num_targets);
if (ctx.test->preamble != NULL) {
status = _cairo_test_runner_preamble (&ctx);
switch (status) {
case CAIRO_TEST_SUCCESS:
skipped = FALSE;
break;
case CAIRO_TEST_NO_MEMORY:
case CAIRO_TEST_FAILURE:
failed = TRUE;
goto TEST_DONE;
case CAIRO_TEST_CRASHED:
failed = TRUE;
goto TEST_DONE;
case CAIRO_TEST_UNTESTED:
goto TEST_DONE;
}
}
if (ctx.test->draw == NULL)
goto TEST_DONE;
for (n = 0; n < ctx.num_targets; n++) {
const cairo_boilerplate_target_t *target;
cairo_bool_t target_failed = FALSE,
target_crashed = FALSE,
target_skipped = TRUE;
int has_similar;
target = ctx.targets_to_test[n];
if (XFAIL != NULL) {
const char *target_XFAIL, *base_XFAIL = NULL;
if (((target_XFAIL = strstr (XFAIL, target->name)) != NULL ||
(base_XFAIL = target_XFAIL = strstr (XFAIL, target->basename)) != NULL) &&
target_XFAIL < strpbrk (XFAIL, " \t;:") &&
target_XFAIL[-1] != '!')
{
ctx.expectation = CAIRO_TEST_FAILURE;
if (base_XFAIL != NULL) {
unsigned slen;
slen = strpbrk (base_XFAIL, " \t;:,") - base_XFAIL;
if (slen != strlen (target->basename))
ctx.expectation = CAIRO_TEST_SUCCESS;
}
}
else
ctx.expectation = CAIRO_TEST_SUCCESS;
} else
ctx.expectation = expectation;
has_similar = runner.full_test ?
cairo_test_target_has_similar (&ctx, target) :
0;
for (m = 0; m < runner.num_device_offsets; m++) {
int dev_offset = m * 25;
int similar;
for (similar = 0; similar <= has_similar; similar++) {
status = _cairo_test_runner_draw (&ctx, target,
similar, dev_offset);
switch (status) {
case CAIRO_TEST_SUCCESS:
target_skipped = FALSE;
break;
case CAIRO_TEST_NO_MEMORY:
case CAIRO_TEST_FAILURE:
target_failed = TRUE;
break;
case CAIRO_TEST_CRASHED:
target_crashed = TRUE;
break;
case CAIRO_TEST_UNTESTED:
break;
}
}
}
if (target_crashed) {
target_status[n] = CAIRO_TEST_CRASHED;
runner.num_crashed_per_target[n]++;
runner.crashes_per_target[n] = _list_prepend (runner.crashes_per_target[n],
list->test);
crashed = TRUE;
} else if (target_failed) {
if (ctx.expectation == CAIRO_TEST_SUCCESS) {
target_status[n] = CAIRO_TEST_FAILURE;
runner.num_failed_per_target[n]++;
runner.fails_per_target[n] = _list_prepend (runner.fails_per_target[n],
list->test);
} else
target_status[n] = CAIRO_TEST_SUCCESS;
failed = TRUE;
} else if (target_skipped) {
target_status[n] = CAIRO_TEST_UNTESTED;
} else {
target_status[n] = CAIRO_TEST_SUCCESS;
/* An unexpected pass should also be flagged */
if (ctx.expectation != CAIRO_TEST_SUCCESS) {
target_status[n] = CAIRO_TEST_FAILURE;
runner.num_xpassed_per_target[n]++;
runner.xpasses_per_target[n] = _list_prepend (runner.xpasses_per_target[n],
list->test);
}
skipped = FALSE;
}
}
TEST_DONE:
cairo_test_fini (&ctx);
TEST_SKIPPED:
targets[0] = '\0';
if (crashed) {
len = 0;
for (n = 0 ; n < runner.base.num_targets; n++) {
if (target_status[n] == CAIRO_TEST_CRASHED) {
if (strstr (targets,
runner.base.targets_to_test[n]->name) == NULL)
{
len += snprintf (targets + len, sizeof (targets) - len,
"%s, ",
runner.base.targets_to_test[n]->name);
}
}
}
targets[len-2] = '\0';
_log (&runner.base, "\n%s: CRASH! (%s)\n", name, targets);
runner.num_crashed++;
} else if (failed) {
if (expectation == CAIRO_TEST_SUCCESS) {
len = 0;
for (n = 0 ; n < runner.base.num_targets; n++) {
if (target_status[n] == CAIRO_TEST_FAILURE) {
if (strstr (targets,
runner.base.targets_to_test[n]->name) == NULL)
{
len += snprintf (targets + len,
sizeof (targets) - len,
"%s, ",
runner.base.targets_to_test[n]->name);
}
}
}
if (len == 0) {
_log (&runner.base, "%s: XFAIL\n", name);
runner.num_xfailed++;
} else {
targets[len-2] = '\0';
_log (&runner.base, "%s: FAIL (%s)\n", name, targets);
runner.num_failed++;
}
} else {
_log (&runner.base, "%s: XFAIL\n", name);
runner.num_xfailed++;
}
} else if (skipped) {
_log (&runner.base, "%s: UNTESTED\n", name);
runner.num_skipped++;
} else {
if (expectation == CAIRO_TEST_SUCCESS) {
_log (&runner.base, "%s: PASS\n", name);
runner.num_passed++;
} else {
_log (&runner.base, "%s: XPASS\n", name);
runner.num_xpassed++;
}
}
fflush (runner.base.log_file);
TEST_NEXT:
free (name);
}
_list_free (tests);
if (runner.list_only) {
printf ("\n");
return CAIRO_TEST_SUCCESS;
}
for (n = 0 ; n < runner.base.num_targets; n++) {
runner.crashes_per_target[n] = _list_reverse (runner.crashes_per_target[n]);
runner.fails_per_target[n] = _list_reverse (runner.fails_per_target[n]);
runner.xpasses_per_target[n] = _list_reverse (runner.xpasses_per_target[n]);
}
_runner_print_results (&runner);
free (target_status);
return _runner_fini (&runner);
}
void
cairo_test_register (const cairo_test_t *test)
{
tests = _list_prepend (tests, test);
}