/* * 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 */ /* * 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 #include "cairo-missing.h" #if CAIRO_HAS_SCRIPT_SURFACE #include #endif /* For basename */ #ifdef HAVE_LIBGEN_H #include #endif #include /* isspace() */ #include #include #include #include #include #include #include #include #include #include #include #include #if CAIRO_HAS_REAL_PTHREAD #include #endif #if HAVE_FCFINI #include #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 }