diff options
Diffstat (limited to 'libs/cairo-1.16.0/test/cairo-test-trace.c')
-rw-r--r-- | libs/cairo-1.16.0/test/cairo-test-trace.c | 1780 |
1 files changed, 1780 insertions, 0 deletions
diff --git a/libs/cairo-1.16.0/test/cairo-test-trace.c b/libs/cairo-1.16.0/test/cairo-test-trace.c new file mode 100644 index 0000000..5badc43 --- /dev/null +++ b/libs/cairo-1.16.0/test/cairo-test-trace.c @@ -0,0 +1,1780 @@ +/* + * 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 +} |