blob: 5badc4377a0136605bbbb5eecc495d48ce38fdf2 [file] [log] [blame]
/*
* Copyright © 2009 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
* the authors not be used in advertising or publicity pertaining to
* distribution of the software without specific, written prior
* permission. The authors make no representations about the
* suitability of this software for any purpose. It is provided "as
* is" without express or implied warranty.
*
* THE AUTHORS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
* SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS, IN NO EVENT SHALL THE AUTHORS 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.
*
* Authors: Chris Wilson <chris@chris-wilson.co.uk>
*/
/*
* The basic idea is that we feed the trace to multiple backends in parallel
* and compare the output at the end of each context (based on the premise
* that contexts demarcate expose events, or their logical equivalents) with
* that of the image[1] backend. Each backend is executed in a separate
* process, for robustness and to isolate the global cairo state, with the
* image data residing in shared memory and synchronising over a socket.
*
* [1] Should be reference implementation, currently the image backend is
* considered to be the reference for all other backends.
*/
/* XXX Can't directly compare fills using spans versus trapezoidation,
* i.e. xlib vs image. Gah, kinda renders this whole scheme moot.
* How about reference platforms?
* E.g. accelerated xlib driver vs Xvfb?
*
* boilerplate->create_reference_surface()?
* boilerplate->reference->create_surface()?
* So for each backend spawn two processes, a reference and xlib
* (obviously minimising the number of reference processes when possible)
*/
/*
* XXX Handle show-page as well as cairo_destroy()? Though arguably that is
* only relevant for paginated backends which is currently outside the
* scope of this test.
*/
#define _GNU_SOURCE 1 /* getline() */
#include "cairo-test.h"
#include "buffer-diff.h"
#include "cairo-boilerplate-getopt.h"
#include <cairo-script-interpreter.h>
#include "cairo-missing.h"
#if CAIRO_HAS_SCRIPT_SURFACE
#include <cairo-script.h>
#endif
/* For basename */
#ifdef HAVE_LIBGEN_H
#include <libgen.h>
#endif
#include <ctype.h> /* isspace() */
#include <sys/types.h>
#include <dirent.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <sys/poll.h>
#include <sys/un.h>
#include <errno.h>
#include <assert.h>
#if CAIRO_HAS_REAL_PTHREAD
#include <pthread.h>
#endif
#if HAVE_FCFINI
#include <fontconfig/fontconfig.h>
#endif
#ifndef MAP_NORESERVE
#define MAP_NORESERVE 0
#endif
#define DEBUG 0
#define ignore_image_differences 0 /* XXX make me a cmdline option! */
#define write_results 1
#define write_traces 1
#define DATA_SIZE (256 << 20)
#define SHM_PATH_XXX "/.shmem-cairo-trace"
typedef struct _test_trace {
/* Options from command-line */
cairo_bool_t list_only;
char **names;
unsigned int num_names;
char **exclude_names;
unsigned int num_exclude_names;
/* Stuff used internally */
const cairo_boilerplate_target_t **targets;
int num_targets;
} test_trace_t;
typedef struct _test_runner {
const char *name;
cairo_surface_t *surface;
void *closure;
uint8_t *base;
const char *trace;
pid_t pid;
int sk;
cairo_bool_t is_recording;
cairo_script_interpreter_t *csi;
struct context_closure {
struct context_closure *next;
unsigned long id;
unsigned long start_line;
unsigned long end_line;
cairo_t *context;
cairo_surface_t *surface;
} *contexts;
unsigned long context_id;
} test_runner_t;
struct slave {
pid_t pid;
int fd;
unsigned long image_serial;
unsigned long image_ready;
unsigned long start_line;
unsigned long end_line;
cairo_surface_t *image;
long width, height;
cairo_surface_t *difference;
buffer_diff_result_t result;
const cairo_boilerplate_target_t *target;
const struct slave *reference;
cairo_bool_t is_recording;
};
struct request_image {
unsigned long id;
unsigned long start_line;
unsigned long end_line;
cairo_format_t format;
long width;
long height;
long stride;
};
struct surface_tag {
long width, height;
};
static const cairo_user_data_key_t surface_tag;
#define TARGET_NAME(T) ((T) ? (T)->name : "recording")
#if CAIRO_HAS_REAL_PTHREAD
#define tr_die(t) t->is_recording ? pthread_exit(NULL) : exit(1)
#else
#define tr_die(t) exit(1)
#endif
static cairo_bool_t
writen (int fd, const void *ptr, int len)
{
#if 0
const uint8_t *data = ptr;
while (len) {
int ret = write (fd, data, len);
if (ret < 0) {
switch (errno) {
case EAGAIN:
case EINTR:
continue;
default:
return FALSE;
}
} else if (ret == 0) {
return FALSE;
} else {
data += ret;
len -= ret;
}
}
return TRUE;
#else
int ret = send (fd, ptr, len, 0);
return ret == len;
#endif
}
static cairo_bool_t
readn (int fd, void *ptr, int len)
{
#if 0
uint8_t *data = ptr;
while (len) {
int ret = read (fd, data, len);
if (ret < 0) {
switch (errno) {
case EAGAIN:
case EINTR:
continue;
default:
return FALSE;
}
} else if (ret == 0) {
return FALSE;
} else {
data += ret;
len -= ret;
}
}
return TRUE;
#else
int ret = recv (fd, ptr, len, MSG_WAITALL);
return ret == len;
#endif
}
static cairo_format_t
format_for_content (cairo_content_t content)
{
switch (content) {
case CAIRO_CONTENT_ALPHA:
return CAIRO_FORMAT_A8;
case CAIRO_CONTENT_COLOR:
return CAIRO_FORMAT_RGB24;
default:
case CAIRO_CONTENT_COLOR_ALPHA:
return CAIRO_FORMAT_ARGB32;
}
}
static void
send_recording_surface (test_runner_t *tr,
int width, int height,
struct context_closure *closure)
{
#if CAIRO_HAS_REAL_PTHREAD
const struct request_image rq = {
closure->id,
closure->start_line,
closure->end_line,
-1,
width, height,
(long) closure->surface,
};
unsigned long offset;
unsigned long serial;
if (DEBUG > 1) {
printf ("send-recording-surface: %lu [%lu, %lu]\n",
closure->id,
closure->start_line,
closure->end_line);
}
writen (tr->sk, &rq, sizeof (rq));
readn (tr->sk, &offset, sizeof (offset));
/* signal completion */
writen (tr->sk, &closure->id, sizeof (closure->id));
/* wait for image check */
serial = 0;
readn (tr->sk, &serial, sizeof (serial));
if (DEBUG > 1) {
printf ("send-recording-surface: serial: %lu\n", serial);
}
if (serial != closure->id)
pthread_exit (NULL);
#else
exit (1);
#endif
}
static void *
request_image (test_runner_t *tr,
struct context_closure *closure,
cairo_format_t format,
int width, int height, int stride)
{
const struct request_image rq = {
closure->id,
closure->start_line,
closure->end_line,
format, width, height, stride
};
unsigned long offset = -1;
assert (format != (cairo_format_t) -1);
writen (tr->sk, &rq, sizeof (rq));
readn (tr->sk, &offset, sizeof (offset));
if (offset == (unsigned long) -1)
return NULL;
return tr->base + offset;
}
static void
send_surface (test_runner_t *tr,
struct context_closure *closure)
{
cairo_surface_t *source = closure->surface;
cairo_surface_t *image;
cairo_format_t format = (cairo_format_t) -1;
cairo_t *cr;
int width, height, stride;
void *data;
unsigned long serial;
if (DEBUG > 1) {
printf ("send-surface: '%s', is-recording? %d\n",
tr->name, tr->is_recording);
}
if (cairo_surface_get_type (source) == CAIRO_SURFACE_TYPE_IMAGE) {
width = cairo_image_surface_get_width (source);
height = cairo_image_surface_get_height (source);
format = cairo_image_surface_get_format (source);
} else {
struct surface_tag *tag;
tag = cairo_surface_get_user_data (source, &surface_tag);
if (tag != NULL) {
width = tag->width;
height = tag->height;
} else {
double x0, x1, y0, y1;
/* presumably created using cairo_surface_create_similar() */
cr = cairo_create (source);
cairo_clip_extents (cr, &x0, &y0, &x1, &y1);
cairo_destroy (cr);
tag = xmalloc (sizeof (*tag));
width = tag->width = x1 - x0;
height = tag->height = y1 - y0;
if (cairo_surface_set_user_data (source, &surface_tag, tag, free))
tr_die (tr);
}
}
if (tr->is_recording) {
send_recording_surface (tr, width, height, closure);
return;
}
if (format == (cairo_format_t) -1)
format = format_for_content (cairo_surface_get_content (source));
stride = cairo_format_stride_for_width (format, width);
data = request_image (tr, closure, format, width, height, stride);
if (data == NULL)
tr_die (tr);
image = cairo_image_surface_create_for_data (data,
format,
width, height,
stride);
cr = cairo_create (image);
cairo_surface_destroy (image);
cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);
cairo_set_source_surface (cr, source, 0, 0);
cairo_paint (cr);
cairo_destroy (cr);
/* signal completion */
writen (tr->sk, &closure->id, sizeof (closure->id));
/* wait for image check */
serial = 0;
readn (tr->sk, &serial, sizeof (serial));
if (serial != closure->id)
tr_die (tr);
}
static cairo_surface_t *
_surface_create (void *closure,
cairo_content_t content,
double width, double height,
long uid)
{
test_runner_t *tr = closure;
cairo_surface_t *surface;
surface = cairo_surface_create_similar (tr->surface,
content, width, height);
if (cairo_surface_get_type (surface) != CAIRO_SURFACE_TYPE_IMAGE) {
struct surface_tag *tag;
tag = xmalloc (sizeof (*tag));
tag->width = width;
tag->height = height;
if (cairo_surface_set_user_data (surface, &surface_tag, tag, free))
tr_die (tr);
}
return surface;
}
static cairo_t *
_context_create (void *closure, cairo_surface_t *surface)
{
test_runner_t *tr = closure;
struct context_closure *l;
if (DEBUG) {
fprintf (stderr, "%s: starting context %lu on line %d\n",
tr->name ? tr->name : "recording" ,
tr->context_id + 1,
cairo_script_interpreter_get_line_number (tr->csi));
}
l = xmalloc (sizeof (*l));
l->next = tr->contexts;
l->start_line = cairo_script_interpreter_get_line_number (tr->csi);
l->end_line = l->start_line;
l->context = cairo_create (surface);
l->surface = cairo_surface_reference (surface);
l->id = ++tr->context_id;
if (l->id == 0)
l->id = ++tr->context_id;
tr->contexts = l;
return l->context;
}
static void
_context_destroy (void *closure, void *ptr)
{
test_runner_t *tr = closure;
struct context_closure *l, **prev = &tr->contexts;
while ((l = *prev) != NULL) {
if (l->context == ptr) {
if (DEBUG) {
fprintf (stderr, "%s: context %lu complete on line %d\n",
tr->name ? tr->name : "recording" ,
tr->context_id,
cairo_script_interpreter_get_line_number (tr->csi));
}
l->end_line =
cairo_script_interpreter_get_line_number (tr->csi);
if (cairo_surface_status (l->surface) == CAIRO_STATUS_SUCCESS) {
send_surface (tr, l);
} else {
fprintf (stderr, "%s: error during replay, line %lu: %s!\n",
tr->name,
l->end_line,
cairo_status_to_string (cairo_surface_status (l->surface)));
tr_die (tr);
}
cairo_surface_destroy (l->surface);
*prev = l->next;
free (l);
return;
}
prev = &l->next;
}
}
static void
execute (test_runner_t *tr)
{
const cairo_script_interpreter_hooks_t hooks = {
.closure = tr,
.surface_create = _surface_create,
.context_create = _context_create,
.context_destroy = _context_destroy,
};
pid_t ack;
tr->csi = cairo_script_interpreter_create ();
cairo_script_interpreter_install_hooks (tr->csi, &hooks);
ack = -1;
readn (tr->sk, &ack, sizeof (ack));
if (ack != tr->pid)
tr_die (tr);
cairo_script_interpreter_run (tr->csi, tr->trace);
cairo_script_interpreter_finish (tr->csi);
if (cairo_script_interpreter_destroy (tr->csi))
tr_die (tr);
}
static int
spawn_socket (const char *socket_path, pid_t pid)
{
struct sockaddr_un addr;
int sk;
sk = socket (PF_UNIX, SOCK_STREAM, 0);
if (sk == -1)
return -1;
memset (&addr, 0, sizeof (addr));
addr.sun_family = AF_UNIX;
strcpy (addr.sun_path, socket_path);
if (connect (sk, (struct sockaddr *) &addr, sizeof (addr)) == -1)
return -1;
if (! writen (sk, &pid, sizeof (pid)))
return -1;
return sk;
}
static void *
spawn_shm (const char *shm_path)
{
void *base;
int fd;
fd = shm_open (shm_path, O_RDWR, 0);
if (fd == -1)
return MAP_FAILED;
base = mmap (NULL, DATA_SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_NORESERVE,
fd, 0);
close (fd);
return base;
}
static int
spawn_target (const char *socket_path,
const char *shm_path,
const cairo_boilerplate_target_t *target,
const char *trace)
{
test_runner_t tr;
pid_t pid;
if (DEBUG)
printf ("Spawning slave '%s' for %s\n", target->name, trace);
pid = fork ();
if (pid != 0)
return pid;
tr.is_recording = FALSE;
tr.pid = getpid ();
tr.sk = spawn_socket (socket_path, tr.pid);
if (tr.sk == -1) {
fprintf (stderr, "%s: Failed to open socket.\n",
target->name);
exit (-1);
}
tr.base = spawn_shm (shm_path);
if (tr.base == MAP_FAILED) {
fprintf (stderr, "%s: Failed to map shared memory segment.\n",
target->name);
exit (-1);
}
tr.name = target->name;
tr.contexts = NULL;
tr.context_id = 0;
tr.trace = trace;
tr.surface = target->create_surface (NULL,
target->content,
1, 1,
1, 1,
CAIRO_BOILERPLATE_MODE_TEST,
&tr.closure);
if (tr.surface == NULL) {
fprintf (stderr,
"%s: Failed to create target surface.\n",
target->name);
exit (-1);
}
execute (&tr);
cairo_surface_destroy (tr.surface);
if (target->cleanup)
target->cleanup (tr.closure);
close (tr.sk);
munmap (tr.base, DATA_SIZE);
exit (0);
}
#if CAIRO_HAS_REAL_PTHREAD
static void
cleanup_recorder (void *arg)
{
test_runner_t *tr = arg;
cairo_surface_finish (tr->surface);
cairo_surface_destroy (tr->surface);
close (tr->sk);
free (tr);
}
static void *
record (void *arg)
{
test_runner_t *tr = arg;
pthread_cleanup_push (cleanup_recorder, tr);
execute (tr);
pthread_cleanup_pop (TRUE);
return NULL;
}
/* The recorder is special:
* 1. It doesn't generate an image, but keeps an in-memory trace to
* reconstruct any surface.
* 2. Runs in the same process, but separate thread.
*/
static pid_t
spawn_recorder (const char *socket_path, const char *trace, test_runner_t **out)
{
test_runner_t *tr;
pthread_t id;
pthread_attr_t attr;
pid_t pid = getpid ();
if (DEBUG)
printf ("Spawning recorder for %s\n", trace);
tr = malloc (sizeof (*tr));
if (tr == NULL)
return -1;
tr->is_recording = TRUE;
tr->pid = pid;
tr->sk = spawn_socket (socket_path, tr->pid);
if (tr->sk == -1) {
free (tr);
return -1;
}
tr->base = NULL;
tr->name = NULL;
tr->contexts = NULL;
tr->context_id = 0;
tr->trace = trace;
tr->surface = cairo_recording_surface_create (CAIRO_CONTENT_COLOR_ALPHA,
NULL);
if (tr->surface == NULL) {
cleanup_recorder (tr);
return -1;
}
pthread_attr_init (&attr);
pthread_attr_setdetachstate (&attr, TRUE);
if (pthread_create (&id, &attr, record, tr) < 0) {
pthread_attr_destroy (&attr);
cleanup_recorder (tr);
return -1;
}
pthread_attr_destroy (&attr);
*out = tr;
return pid;
}
#endif
/* XXX imagediff - is the extra expense worth it? */
static cairo_bool_t
matches_reference (struct slave *slave)
{
cairo_surface_t *a, *b;
a = slave->image;
b = slave->reference->image;
if (a == b)
return TRUE;
if (a == NULL || b == NULL)
return FALSE;
if (cairo_surface_status (a) || cairo_surface_status (b))
return FALSE;
if (cairo_surface_get_type (a) != cairo_surface_get_type (b))
return FALSE;
if (cairo_image_surface_get_format (a) != cairo_image_surface_get_format (b))
return FALSE;
if (cairo_image_surface_get_width (a) != cairo_image_surface_get_width (b))
return FALSE;
if (cairo_image_surface_get_height (a) != cairo_image_surface_get_height (b))
return FALSE;
if (cairo_image_surface_get_stride (a) != cairo_image_surface_get_stride (b))
return FALSE;
if (FALSE && cairo_surface_get_content (a) & CAIRO_CONTENT_COLOR) {
cairo_surface_t *diff;
int width, height, stride, size;
unsigned char *data;
cairo_status_t status;
width = cairo_image_surface_get_width (a);
height = cairo_image_surface_get_height (a);
stride = cairo_image_surface_get_stride (a);
size = height * stride * 4;
data = malloc (size);
if (data == NULL)
return FALSE;
diff = cairo_image_surface_create_for_data (data,
cairo_image_surface_get_format (a),
width, height, stride);
cairo_surface_set_user_data (diff, (cairo_user_data_key_t *) diff,
data, free);
status = image_diff (NULL, a, b, diff, &slave->result);
if (status) {
cairo_surface_destroy (diff);
return FALSE;
}
if (image_diff_is_failure (&slave->result, slave->target->error_tolerance)) {
slave->difference = diff;
return FALSE;
} else {
cairo_surface_destroy (diff);
return TRUE;
}
} else {
int width, height, stride;
const uint8_t *aa, *bb;
int x, y;
width = cairo_image_surface_get_width (a);
height = cairo_image_surface_get_height (a);
stride = cairo_image_surface_get_stride (a);
aa = cairo_image_surface_get_data (a);
bb = cairo_image_surface_get_data (b);
switch (cairo_image_surface_get_format (a)) {
case CAIRO_FORMAT_ARGB32:
for (y = 0; y < height; y++) {
const uint32_t *ua = (uint32_t *) aa;
const uint32_t *ub = (uint32_t *) bb;
for (x = 0; x < width; x++) {
if (ua[x] != ub[x]) {
int channel;
for (channel = 0; channel < 4; channel++) {
unsigned va, vb, diff;
va = (ua[x] >> (channel*8)) & 0xff;
vb = (ub[x] >> (channel*8)) & 0xff;
diff = abs (va - vb);
if (diff > slave->target->error_tolerance)
return FALSE;
}
}
}
aa += stride;
bb += stride;
}
break;
case CAIRO_FORMAT_RGB24:
for (y = 0; y < height; y++) {
const uint32_t *ua = (uint32_t *) aa;
const uint32_t *ub = (uint32_t *) bb;
for (x = 0; x < width; x++) {
if ((ua[x] & 0x00ffffff) != (ub[x] & 0x00ffffff)) {
int channel;
for (channel = 0; channel < 3; channel++) {
unsigned va, vb, diff;
va = (ua[x] >> (channel*8)) & 0xff;
vb = (ub[x] >> (channel*8)) & 0xff;
diff = abs (va - vb);
if (diff > slave->target->error_tolerance)
return FALSE;
}
}
}
aa += stride;
bb += stride;
}
break;
case CAIRO_FORMAT_A8:
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++) {
if (aa[x] != bb[x]) {
unsigned diff = abs (aa[x] - bb[x]);
if (diff > slave->target->error_tolerance)
return FALSE;
}
}
aa += stride;
bb += stride;
}
break;
case CAIRO_FORMAT_A1:
width /= 8;
for (y = 0; y < height; y++) {
if (memcmp (aa, bb, width))
return FALSE;
aa += stride;
bb += stride;
}
break;
case CAIRO_FORMAT_RGB30:
case CAIRO_FORMAT_RGB16_565:
case CAIRO_FORMAT_INVALID:
assert (0);
}
return TRUE;
}
}
static cairo_bool_t
check_images (struct slave *slaves, int num_slaves)
{
int n;
if (ignore_image_differences)
return TRUE;
for (n = 0; n < num_slaves; n++) {
if (slaves[n].reference == NULL)
continue;
if (! matches_reference (&slaves[n]))
return FALSE;
}
return TRUE;
}
static void
write_images (const char *trace, struct slave *slave, int num_slaves)
{
while (num_slaves--) {
if (slave->image != NULL && ! slave->is_recording) {
char *filename;
xasprintf (&filename, "%s-%s-fail.png",
trace, slave->target->name);
cairo_surface_write_to_png (slave->image, filename);
free (filename);
if (slave->difference) {
xasprintf (&filename, "%s-%s-diff.png",
trace, slave->target->name);
cairo_surface_write_to_png (slave->difference, filename);
free (filename);
}
}
slave++;
}
}
static void
write_result (const char *trace, struct slave *slave)
{
static int index;
char *filename;
xasprintf (&filename, "%s-%s-pass-%d-%d-%d.png",
trace, slave->target->name, ++index,
slave->start_line, slave->end_line);
cairo_surface_write_to_png (slave->image, filename);
free (filename);
}
static void
write_trace (const char *trace, const char *id, struct slave *slave)
{
#if CAIRO_HAS_SCRIPT_SURFACE
cairo_device_t *script;
char *filename;
assert (slave->is_recording);
xasprintf (&filename, "%s-%s.trace", trace, id);
script = cairo_script_create (filename);
cairo_script_from_recording_surface (script, slave->image);
cairo_device_destroy (script);
free (filename);
#endif
}
static void
dump_traces (test_runner_t *tr,
const char *trace,
const char *target,
const char *fail)
{
#if CAIRO_HAS_SCRIPT_SURFACE
struct context_closure *c;
for (c = tr->contexts; c; c = c->next) {
cairo_device_t *script;
char *filename;
xasprintf (&filename, "%s-%s-%s.%lu.trace",
trace, target, fail, c->start_line);
script = cairo_script_create (filename);
cairo_script_from_recording_surface (script, c->surface);
cairo_device_destroy (script);
free (filename);
}
#endif
}
static unsigned long
allocate_image_for_slave (uint8_t *base,
unsigned long offset,
struct slave *slave)
{
struct request_image rq;
int size;
uint8_t *data;
assert (slave->image == NULL);
readn (slave->fd, &rq, sizeof (rq));
slave->image_serial = rq.id;
slave->start_line = rq.start_line;
slave->end_line = rq.end_line;
slave->width = rq.width;
slave->height = rq.height;
if (DEBUG > 1) {
printf ("allocate-image-for-slave: %s %lu [%lu, %lu] %ldx%ld stride=%lu => %lu, is-recording? %d\n",
TARGET_NAME (slave->target),
slave->image_serial,
slave->start_line,
slave->end_line,
slave->width,
slave->height,
rq.stride,
offset,
slave->is_recording);
}
if (slave->is_recording) {
/* special communication with recording-surface thread */
slave->image = cairo_surface_reference ((cairo_surface_t *) rq.stride);
} else {
size = rq.height * rq.stride;
size = (size + 4095) & -4096;
data = base + offset;
offset += size;
assert (offset <= DATA_SIZE);
slave->image = cairo_image_surface_create_for_data (data, rq.format,
rq.width, rq.height,
rq.stride);
}
return offset;
}
struct error_info {
unsigned long context_id;
unsigned long start_line;
unsigned long end_line;
};
static cairo_bool_t
test_run (void *base,
int sk,
const char *trace,
struct slave *slaves,
int num_slaves,
struct error_info *error)
{
struct pollfd *pfd;
int npfd, cnt, n, i;
int completion, err = 0;
cairo_bool_t ret = FALSE;
unsigned long image;
if (DEBUG) {
printf ("Running trace '%s' over %d slaves\n",
trace, num_slaves);
}
pfd = xcalloc (num_slaves+1, sizeof (*pfd));
pfd[0].fd = sk;
pfd[0].events = POLLIN;
npfd = 1;
completion = 0;
image = 0;
while ((cnt = poll (pfd, npfd, -1)) > 0) {
if (pfd[0].revents) {
int fd;
while ((fd = accept (sk, NULL, NULL)) != -1) {
pid_t pid;
readn (fd, &pid, sizeof (pid));
for (n = 0; n < num_slaves; n++) {
if (slaves[n].pid == pid) {
slaves[n].fd = fd;
break;
}
}
if (n == num_slaves) {
if (DEBUG)
printf ("unknown slave pid\n");
goto out;
}
pfd[npfd].fd = fd;
pfd[npfd].events = POLLIN;
npfd++;
if (! writen (fd, &pid, sizeof (pid)))
goto out;
}
cnt--;
}
for (n = 1; n < npfd && cnt; n++) {
if (! pfd[n].revents)
continue;
if (pfd[n].revents & POLLHUP) {
pfd[n].events = pfd[n].revents = 0;
completion++;
continue;
}
for (i = 0; i < num_slaves; i++) {
if (slaves[i].fd == pfd[n].fd) {
/* Communication with the slave is done in three phases,
* and we do each pass synchronously.
*
* 1. The slave requests an image buffer, which we
* allocate and then return to the slave the offset into
* the shared memory segment.
*
* 2. The slave indicates that it has finished writing
* into the shared image buffer. The slave now waits
* for the server to collate all the image data - thereby
* throttling the slaves.
*
* 3. After all slaves have finished writing their images,
* we compare them all against the reference image and,
* if satisfied, send an acknowledgement to all slaves.
*/
if (slaves[i].image_serial == 0) {
unsigned long offset;
image =
allocate_image_for_slave (base,
offset = image,
&slaves[i]);
if (! writen (pfd[n].fd, &offset, sizeof (offset))) {
pfd[n].events = pfd[n].revents = 0;
err = 1;
completion++;
continue;
}
} else {
readn (pfd[n].fd,
&slaves[i].image_ready,
sizeof (slaves[i].image_ready));
if (DEBUG) {
printf ("slave '%s' reports completion on %lu (expecting %lu)\n",
TARGET_NAME (slaves[i].target),
slaves[i].image_ready,
slaves[i].image_serial);
}
if (slaves[i].image_ready != slaves[i].image_serial) {
pfd[n].events = pfd[n].revents = 0;
err = 1;
completion++;
continue;
}
/* Can anyone spell 'P·E·D·A·N·T'? */
if (! slaves[i].is_recording)
cairo_surface_mark_dirty (slaves[i].image);
completion++;
}
break;
}
}
cnt--;
}
if (completion >= num_slaves) {
if (err) {
if (DEBUG > 1)
printf ("error detected\n");
goto out;
}
if (DEBUG > 1) {
printf ("all saves report completion\n");
}
if (slaves[0].end_line >= slaves[0].start_line &&
! check_images (slaves, num_slaves)) {
error->context_id = slaves[0].image_serial;
error->start_line = slaves[0].start_line;
error->end_line = slaves[0].end_line;
if (DEBUG) {
printf ("check_images failed: %lu, [%lu, %lu]\n",
slaves[0].image_serial,
slaves[0].start_line,
slaves[0].end_line);
}
write_images (trace, slaves, num_slaves);
if (slaves[0].is_recording)
write_trace (trace, "fail", &slaves[0]);
goto out;
}
if (write_results) write_result (trace, &slaves[1]);
if (write_traces && slaves[0].is_recording) {
char buf[80];
snprintf (buf, sizeof (buf), "%d", slaves[0].image_serial);
write_trace (trace, buf, &slaves[0]);
}
/* ack */
for (i = 0; i < num_slaves; i++) {
cairo_surface_destroy (slaves[i].image);
slaves[i].image = NULL;
if (DEBUG > 1) {
printf ("sending continuation to '%s'\n",
TARGET_NAME (slaves[i].target));
}
if (! writen (slaves[i].fd,
&slaves[i].image_serial,
sizeof (slaves[i].image_serial)))
{
goto out;
}
slaves[i].image_serial = 0;
slaves[i].image_ready = 0;
}
completion = 0;
image = 0;
}
}
done:
ret = TRUE;
out:
if (DEBUG) {
printf ("run complete: %d\n", ret);
}
for (n = 0; n < num_slaves; n++) {
if (slaves[n].fd != -1)
close (slaves[n].fd);
if (slaves[n].image == NULL)
continue;
cairo_surface_destroy (slaves[n].image);
slaves[n].image = NULL;
cairo_surface_destroy (slaves[n].difference);
slaves[n].difference = NULL;
slaves[n].image_serial = 0;
slaves[n].image_ready = 0;
}
free (pfd);
return ret;
}
static int
server_socket (const char *socket_path)
{
long flags;
struct sockaddr_un addr;
int sk;
sk = socket (PF_UNIX, SOCK_STREAM, 0);
if (sk == -1)
return -1;
memset (&addr, 0, sizeof (addr));
addr.sun_family = AF_UNIX;
strcpy (addr.sun_path, socket_path);
if (bind (sk, (struct sockaddr *) &addr, sizeof (addr)) == -1) {
close (sk);
return -1;
}
flags = fcntl (sk, F_GETFL);
if (flags == -1 || fcntl (sk, F_SETFL, flags | O_NONBLOCK) == -1) {
close (sk);
return -1;
}
if (listen (sk, 5) == -1) {
close (sk);
return -1;
}
return sk;
}
static int
server_shm (const char *shm_path)
{
int fd;
fd = shm_open (shm_path, O_RDWR | O_EXCL | O_CREAT, 0777);
if (fd == -1)
return -1;
if (ftruncate (fd, DATA_SIZE) == -1) {
close (fd);
return -1;
}
return fd;
}
static cairo_bool_t
_test_trace (test_trace_t *test,
const char *trace,
const char *name,
struct error_info *error)
{
const char *shm_path = SHM_PATH_XXX;
const cairo_boilerplate_target_t *target, *image;
struct slave *slaves, *s;
test_runner_t *recorder = NULL;
pid_t slave;
char socket_dir[] = "/tmp/cairo-test-trace.XXXXXX";
char *socket_path;
int sk, fd;
int i, num_slaves;
void *base;
cairo_bool_t ret = FALSE;
if (DEBUG)
printf ("setting up trace '%s'\n", trace);
/* create a socket to control the test runners */
if (mkdtemp (socket_dir) == NULL) {
fprintf (stderr, "Unable to create temporary name for socket\n");
return FALSE;
}
xasprintf (&socket_path, "%s/socket", socket_dir);
sk = server_socket (socket_path);
if (sk == -1) {
fprintf (stderr, "Unable to create socket for server\n");
goto cleanup_paths;
}
/* allocate some shared memory */
fd = server_shm (shm_path);
if (fd == -1) {
fprintf (stderr, "Unable to create shared memory '%s': %s\n",
shm_path, strerror (errno));
goto cleanup_sk;
}
image = cairo_boilerplate_get_image_target (CAIRO_CONTENT_COLOR_ALPHA);
assert (image != NULL);
s = slaves = xcalloc (2*test->num_targets + 1, sizeof (struct slave));
#if CAIRO_HAS_REAL_PTHREAD
/* set-up a recording-surface to reconstruct errors */
slave = spawn_recorder (socket_path, trace, &recorder);
if (slave < 0) {
fprintf (stderr, "Unable to create recording surface\n");
goto cleanup_sk;
}
s->pid = slave;
s->is_recording = TRUE;
s->target = NULL;
s->fd = -1;
s->reference = NULL;
s++;
#endif
/* spawn slave processes to run the trace */
for (i = 0; i < test->num_targets; i++) {
const cairo_boilerplate_target_t *reference;
struct slave *master;
target = test->targets[i];
if (DEBUG)
printf ("setting up target[%d]? '%s' (image? %d, measurable? %d)\n",
i, target->name, target == image, target->is_measurable);
if (target == image || ! target->is_measurable)
continue;
/* find a matching slave to use as a reference for this target */
if (target->reference_target != NULL) {
reference =
cairo_boilerplate_get_target_by_name (target->reference_target,
target->content);
assert (reference != NULL);
} else {
reference = image;
}
for (master = slaves; master < s; master++) {
if (master->target == reference)
break;
}
if (master == s) {
/* no match found, spawn a slave to render the reference image */
slave = spawn_target (socket_path, shm_path, reference, trace);
if (slave < 0)
continue;
s->pid = slave;
s->target = reference;
s->fd = -1;
s->reference = NULL;
s++;
}
slave = spawn_target (socket_path, shm_path, target, trace);
if (slave < 0)
continue;
s->pid = slave;
s->target = target;
s->fd = -1;
s->reference = master;
s++;
}
num_slaves = s - slaves;
if (num_slaves == 1) {
fprintf (stderr, "No targets to test\n");
goto cleanup;
}
base = mmap (NULL, DATA_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (base == MAP_FAILED) {
fprintf (stderr, "Unable to mmap shared memory\n");
goto cleanup;
}
ret = test_run (base, sk, name, slaves, num_slaves, error);
munmap (base, DATA_SIZE);
cleanup:
close (fd);
while (s-- > slaves) {
int status;
if (s->fd != -1)
close (s->fd);
cairo_surface_destroy (s->image);
cairo_surface_destroy (s->difference);
if (s->is_recording) /* in-process */
continue;
kill (s->pid, SIGKILL);
waitpid (s->pid, &status, 0);
if (WIFSIGNALED (status) && WTERMSIG(status) != SIGKILL) {
fprintf (stderr, "%s crashed\n", s->target->name);
if (recorder)
dump_traces (recorder, trace, s->target->name, "crash");
}
}
free (slaves);
shm_unlink (shm_path);
cleanup_sk:
close (sk);
cleanup_paths:
remove (socket_path);
remove (socket_dir);
free (socket_path);
return ret;
}
static void
test_trace (test_trace_t *test, const char *trace)
{
char *trace_cpy, *name, *dot;
trace_cpy = xstrdup (trace);
name = basename (trace_cpy);
dot = strchr (name, '.');
if (dot)
*dot = '\0';
if (test->list_only) {
printf ("%s\n", name);
} else {
struct error_info error = {0};
cairo_bool_t ret;
printf ("%s: ", name);
fflush (stdout);
ret = _test_trace (test, trace, name, &error);
if (ret) {
printf ("PASS\n");
} else {
if (error.context_id) {
printf ("FAIL (context %lu, lines [%lu, %lu])\n",
error.context_id,
error.start_line,
error.end_line);
} else {
printf ("FAIL\n");
}
}
}
free (trace_cpy);
}
static cairo_bool_t
read_excludes (test_trace_t *test, const char *filename)
{
FILE *file;
char *line = NULL;
size_t line_size = 0;
char *s, *t;
file = fopen (filename, "r");
if (file == NULL)
return FALSE;
while (getline (&line, &line_size, file) != -1) {
/* terminate the line at a comment marker '#' */
s = strchr (line, '#');
if (s)
*s = '\0';
/* whitespace delimits */
s = line;
while (*s != '\0' && isspace (*s))
s++;
t = s;
while (*t != '\0' && ! isspace (*t))
t++;
if (s != t) {
int i = test->num_exclude_names;
test->exclude_names = xrealloc (test->exclude_names,
sizeof (char *) * (i+1));
test->exclude_names[i] = strndup (s, t-s);
test->num_exclude_names++;
}
}
free (line);
fclose (file);
return TRUE;
}
static void
usage (const char *argv0)
{
fprintf (stderr,
"Usage: %s [-l] [-x exclude-file] [test-names ... | traces ...]\n"
"\n"
"Run the cairo test suite over the given traces (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"
" -x exclude; specify a file to read a list of traces to exclude\n"
"\n"
"If test names are given they are used as sub-string matches so a command\n"
"such as \"%s firefox\" can be used to run all firefox traces.\n"
"Alternatively, you can specify a list of filenames to execute.\n",
argv0, argv0);
}
static void
parse_options (test_trace_t *test, int argc, char *argv[])
{
int c;
test->list_only = FALSE;
test->names = NULL;
test->num_names = 0;
test->exclude_names = NULL;
test->num_exclude_names = 0;
while (1) {
c = _cairo_getopt (argc, argv, "lx:");
if (c == -1)
break;
switch (c) {
case 'l':
test->list_only = TRUE;
break;
case 'x':
if (! read_excludes (test, optarg)) {
fprintf (stderr, "Invalid argument for -x (not readable file): %s\n",
optarg);
exit (1);
}
break;
default:
fprintf (stderr, "Internal error: unhandled option: %c\n", c);
/* fall-through */
case '?':
usage (argv[0]);
exit (1);
}
}
if (optind < argc) {
test->names = &argv[optind];
test->num_names = argc - optind;
}
}
static void
test_reset (test_trace_t *test)
{
/* XXX leaking fonts again via recording-surface? */
#if 0
cairo_debug_reset_static_data ();
#if HAVE_FCFINI
FcFini ();
#endif
#endif
}
static void
test_fini (test_trace_t *test)
{
test_reset (test);
cairo_boilerplate_free_targets (test->targets);
free (test->exclude_names);
}
static cairo_bool_t
test_has_filenames (test_trace_t *test)
{
unsigned int i;
if (test->num_names == 0)
return FALSE;
for (i = 0; i < test->num_names; i++)
if (access (test->names[i], R_OK) == 0)
return TRUE;
return FALSE;
}
static cairo_bool_t
test_can_run (test_trace_t *test, const char *name)
{
unsigned int i;
char *copy, *dot;
cairo_bool_t ret;
if (test->num_names == 0 && test->num_exclude_names == 0)
return TRUE;
copy = xstrdup (name);
dot = strrchr (copy, '.');
if (dot != NULL)
*dot = '\0';
if (test->num_names) {
ret = TRUE;
for (i = 0; i < test->num_names; i++)
if (strstr (copy, test->names[i]))
goto check_exclude;
ret = FALSE;
goto done;
}
check_exclude:
if (test->num_exclude_names) {
ret = FALSE;
for (i = 0; i < test->num_exclude_names; i++)
if (strstr (copy, test->exclude_names[i]))
goto done;
ret = TRUE;
goto done;
}
done:
free (copy);
return ret;
}
static void
warn_no_traces (const char *message, const char *trace_dir)
{
fprintf (stderr,
"Error: %s '%s'.\n"
"Have you cloned the cairo-traces repository and uncompressed the traces?\n"
" git clone git://anongit.freedesktop.org/cairo-traces\n"
" cd cairo-traces && make\n"
"Or set the env.var CAIRO_TRACE_DIR to point to your traces?\n",
message, trace_dir);
}
static void
interrupt (int sig)
{
shm_unlink (SHM_PATH_XXX);
signal (sig, SIG_DFL);
raise (sig);
}
int
main (int argc, char *argv[])
{
test_trace_t test;
const char *trace_dir = "cairo-traces";
unsigned int n;
signal (SIGPIPE, SIG_IGN);
signal (SIGINT, interrupt);
parse_options (&test, argc, argv);
shm_unlink (SHM_PATH_XXX);
if (getenv ("CAIRO_TRACE_DIR") != NULL)
trace_dir = getenv ("CAIRO_TRACE_DIR");
test.targets = cairo_boilerplate_get_targets (&test.num_targets, NULL);
if (test_has_filenames (&test)) {
for (n = 0; n < test.num_names; n++) {
if (access (test.names[n], R_OK) == 0) {
test_trace (&test, test.names[n]);
test_reset (&test);
}
}
} else {
DIR *dir;
struct dirent *de;
int num_traces = 0;
dir = opendir (trace_dir);
if (dir == NULL) {
warn_no_traces ("Failed to open directory", trace_dir);
test_fini (&test);
return 1;
}
while ((de = readdir (dir)) != NULL) {
char *trace;
const char *dot;
dot = strrchr (de->d_name, '.');
if (dot == NULL)
continue;
if (strcmp (dot, ".trace"))
continue;
num_traces++;
if (! test_can_run (&test, de->d_name))
continue;
xasprintf (&trace, "%s/%s", trace_dir, de->d_name);
test_trace (&test, trace);
test_reset (&test);
free (trace);
}
closedir (dir);
if (num_traces == 0) {
warn_no_traces ("Found no traces in", trace_dir);
test_fini (&test);
return 1;
}
}
test_fini (&test);
return 0;
}
void
cairo_test_logv (const cairo_test_context_t *ctx,
const char *fmt, va_list va)
{
#if 0
vfprintf (stderr, fmt, va);
#endif
}
void
cairo_test_log (const cairo_test_context_t *ctx, const char *fmt, ...)
{
#if 0
va_list va;
va_start (va, fmt);
vfprintf (stderr, fmt, va);
va_end (va);
#endif
}