mutter/src/tests/meta-ref-test.c
Jonas Ådahl d7ce6a47f8 tests: Add reference test framework
This adds a test framework that makes it possible to compare the result
of painting a view against a reference image. Test reference as PNG
images are stored in src/tests/ref-tests/.

Reference images needs to be created for testing to be able to succeed.
Adding a test reference image is done using the
`MUTTER_REF_TEST_UPDATE` environment variable. See meta-ref-test.c for
details.

The image comparison code is largely based on the reference image test
framework in weston; see meta-ref-test.c for details.

Part-of: <https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/1698>
2021-03-12 15:09:45 +00:00

611 lines
18 KiB
C

/*
* Copyright (C) 2021 Red Hat Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* The image difference code is originally a reformatted and simplified
* copy of weston-test-client-helper.c from the weston repository, with
* the following copyright and license note:
*
* Copyright © 2012 Intel Corporation
* Copyright © 2015 Samsung Electronics Co., Ltd
* Copyright 2016, 2017 Collabora, Ltd.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice (including the
* next paragraph) shall be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/*
* To update or initialize reference images for tests, set the
* MUTTER_REF_TEST_UPDATE environment variable.
*
* The MUTTER_REF_TEST_UPDATE is interpreted as a comma seperated list of
* regular expressions. If the test path matches any of the regular
* expressions, the test reference image will be updated, unless the
* existing reference image is pixel identical to the newly created one.
*
* Updating test reference images also requires using a software OpenGL
* renderer, which can be achieved using LIBGL_ALWAYS_SOFTWARE=1
*
* For example, for the test case '/path/to/test/case', run the test
* inside
*
* ```
* env LIBGL_ALWAYS_SOFTWARE=1 MUTTER_REF_TEST_UPDATE='/path/to/test/case`
* ```
*
*/
#include "config.h"
#include "tests/meta-ref-test.h"
#include <cairo.h>
#include <glib.h>
#include "backends/meta-backend-private.h"
#include "backends/meta-stage-private.h"
#include "clutter/clutter/clutter-stage-view-private.h"
typedef struct _Range
{
int a;
int b;
} Range;
typedef struct _ImageIterator
{
uint8_t *data;
int stride;
} ImageIterator;
typedef struct _PixelDiffStat
{
/* Pixel diff stat channel */
struct {
int min_diff;
int max_diff;
} ch[4];
} PixelDiffStat;
/**
* range_get:
* @range: Range to validate or NULL.
*
* Validate and get range.
*
* Returns the given range, or {0, 0} for NULL.
*
* Will abort if range is invalid, that is a > b.
*/
static Range
range_get (const Range *range)
{
if (!range)
return (Range) { 0, 0 };
g_assert_cmpint (range->a, <=, range->b);
return *range;
}
static void
image_iterator_init (ImageIterator *it,
cairo_surface_t *image)
{
it->stride = cairo_image_surface_get_stride (image);
it->data = cairo_image_surface_get_data (image);
g_assert_cmpint (cairo_image_surface_get_format (image), ==,
CAIRO_FORMAT_ARGB32);
}
static uint32_t *
image_iterator_get_row (ImageIterator *it,
int y)
{
return (uint32_t *) (it->data + y * it->stride);
}
static gboolean
fuzzy_match_pixels (uint32_t pix_a,
uint32_t pix_b,
const Range *fuzz,
PixelDiffStat *diff_stat)
{
gboolean ret = TRUE;
int shift;
int i;
for (shift = 0, i = 0; i < 4; shift += 8, i++)
{
int val_a = (pix_a >> shift) & 0xffu;
int val_b = (pix_b >> shift) & 0xffu;
int d = val_b - val_a;
if (diff_stat)
{
diff_stat->ch[i].min_diff = MIN (diff_stat->ch[i].min_diff, d);
diff_stat->ch[i].max_diff = MAX (diff_stat->ch[i].max_diff, d);
}
if (d < fuzz->a || d > fuzz->b)
ret = FALSE;
}
return ret;
}
static gboolean
compare_images (cairo_surface_t *ref_image,
cairo_surface_t *result_image,
const Range *precision,
PixelDiffStat *diff_stat)
{
Range fuzz = range_get (precision);
ImageIterator it_ref;
ImageIterator it_result;
int x, y;
uint32_t *pix_ref;
uint32_t *pix_result;
g_assert_cmpint (cairo_image_surface_get_width (ref_image), ==,
cairo_image_surface_get_width (result_image));
g_assert_cmpint (cairo_image_surface_get_height (ref_image), ==,
cairo_image_surface_get_height (result_image));
image_iterator_init (&it_ref, ref_image);
image_iterator_init (&it_result, result_image);
for (y = 0; y < cairo_image_surface_get_height (ref_image); y++)
{
pix_ref = image_iterator_get_row (&it_ref, y);
pix_result = image_iterator_get_row (&it_result, y);
for (x = 0; x < cairo_image_surface_get_width (ref_image); x++)
{
if (!fuzzy_match_pixels (*pix_ref, *pix_result,
&fuzz, diff_stat))
return FALSE;
pix_ref++;
pix_result++;
}
}
return TRUE;
}
static void
assert_software_rendered (void)
{
MetaBackend *backend = meta_get_backend ();
g_assert_false (meta_backend_is_rendering_hardware_accelerated (backend));
}
static void
capture_view_into (ClutterStageView *view,
MetaRectangle *rect,
uint8_t *buffer,
int stride)
{
CoglFramebuffer *framebuffer;
ClutterBackend *backend;
CoglContext *context;
CoglBitmap *bitmap;
cairo_rectangle_int_t view_layout;
float view_scale;
float texture_width;
float texture_height;
int x, y;
framebuffer = clutter_stage_view_get_framebuffer (view);
view_scale = clutter_stage_view_get_scale (view);
texture_width = roundf (rect->width * view_scale);
texture_height = roundf (rect->height * view_scale);
backend = clutter_get_default_backend ();
context = clutter_backend_get_cogl_context (backend);
bitmap = cogl_bitmap_new_for_data (context,
texture_width, texture_height,
CLUTTER_CAIRO_FORMAT_ARGB32,
stride,
buffer);
clutter_stage_view_get_layout (view, &view_layout);
x = roundf ((rect->x - view_layout.x) * view_scale);
y = roundf ((rect->y - view_layout.y) * view_scale);
cogl_framebuffer_read_pixels_into_bitmap (framebuffer,
x, y,
COGL_READ_PIXELS_COLOR_BUFFER,
bitmap);
cogl_object_unref (bitmap);
}
typedef struct
{
MetaStageWatch *watch;
GMainLoop *loop;
cairo_surface_t *out_image;
} CaptureViewData;
static void
on_after_paint (MetaStage *stage,
ClutterStageView *view,
ClutterPaintContext *paint_context,
gpointer user_data)
{
CaptureViewData *data = user_data;
MetaRectangle rect;
float view_scale;
int texture_width, texture_height;
cairo_surface_t *image;
uint8_t *buffer;
int stride;
meta_stage_remove_watch (stage, data->watch);
data->watch = NULL;
clutter_stage_view_get_layout (view, &rect);
view_scale = clutter_stage_view_get_scale (view);
texture_width = roundf (rect.width * view_scale);
texture_height = roundf (rect.height * view_scale);
image = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
texture_width, texture_height);
cairo_surface_set_device_scale (image, view_scale, view_scale);
buffer = cairo_image_surface_get_data (image);
stride = cairo_image_surface_get_stride (image);
capture_view_into (view, &rect, buffer, stride);
data->out_image = image;
cairo_surface_mark_dirty (data->out_image);
g_main_loop_quit (data->loop);
}
static cairo_surface_t *
capture_view (ClutterStageView *view)
{
MetaBackend *backend = meta_get_backend ();
MetaStage *stage = META_STAGE (meta_backend_get_stage (backend));
CaptureViewData data = { 0 };
data.loop = g_main_loop_new (NULL, FALSE);
data.watch = meta_stage_watch_view (stage, view,
META_STAGE_WATCH_AFTER_PAINT,
on_after_paint,
&data);
clutter_stage_view_add_redraw_clip (view, NULL);
clutter_stage_view_schedule_update (view);
g_main_loop_run (data.loop);
g_main_loop_unref (data.loop);
g_assert_null (data.watch);
g_assert_nonnull (data.out_image);
return data.out_image;
}
static void
depathify (char *path)
{
int len = strlen (path);
int i;
for (i = 0; i < len; i++)
{
if (path[i] == '/')
path[i] = '_';
}
}
static void
ensure_expected_format (cairo_surface_t **ref_image)
{
int width, height;
cairo_surface_t *target;
cairo_t *cr;
if (cairo_image_surface_get_format (*ref_image) ==
CAIRO_FORMAT_ARGB32)
return;
width = cairo_image_surface_get_width (*ref_image);
height = cairo_image_surface_get_height (*ref_image);
target = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
cr = cairo_create (target);
cairo_set_source_surface (cr, *ref_image, 0.0, 0.0);
cairo_paint (cr);
cairo_destroy (cr);
cairo_surface_destroy (*ref_image);
*ref_image = target;
}
/**
* Tint a color:
* @src Source pixel as x8r8g8b8.
* @add The tint as x8r8g8b8, x8 must be zero; r8, g8 and b8 must be
* no greater than 0xc0 to avoid overflow to another channel.
* Returns: The tinted pixel color as x8r8g8b8, x8 guaranteed to be 0xff.
*
* The source pixel RGB values are divided by 4, and then the tint is added.
* To achieve colors outside of the range of src, a tint color channel must be
* at least 0x40. (0xff / 4 = 0x3f, 0xff - 0x3f = 0xc0)
*/
static uint32_t
tint (uint32_t src,
uint32_t add)
{
uint32_t v;
v = ((src & 0xfcfcfcfc) >> 2) | 0xff000000;
return v + add;
}
static cairo_surface_t *
visualize_difference (cairo_surface_t *ref_image,
cairo_surface_t *result_image,
const Range *precision)
{
Range fuzz = range_get (precision);
int width, height;
cairo_surface_t *diff_image;
cairo_t *cr;
ImageIterator it_ref;
ImageIterator it_result;
ImageIterator it_diff;
int y;
width = cairo_image_surface_get_width (ref_image);
height = cairo_image_surface_get_height (ref_image);
diff_image = cairo_surface_create_similar_image (ref_image,
CAIRO_FORMAT_ARGB32,
width,
height);
cr = cairo_create (diff_image);
cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 1.0);
cairo_paint (cr);
cairo_set_source_surface (cr, ref_image, 0.0, 0.0);
cairo_set_operator (cr, CAIRO_OPERATOR_HSL_LUMINOSITY);
cairo_paint (cr);
cairo_destroy (cr);
image_iterator_init (&it_ref, ref_image);
image_iterator_init (&it_result, result_image);
image_iterator_init (&it_diff, diff_image);
for (y = 0; y < cairo_image_surface_get_height (ref_image); y++)
{
uint32_t *ref_pixel;
uint32_t *result_pixel;
uint32_t *diff_pixel;
int x;
ref_pixel = image_iterator_get_row (&it_ref, y);
result_pixel = image_iterator_get_row (&it_result, y);
diff_pixel = image_iterator_get_row (&it_diff, y);
for (x = 0; x < cairo_image_surface_get_width (ref_image); x++)
{
if (fuzzy_match_pixels (*ref_pixel, *result_pixel,
&fuzz, NULL))
*diff_pixel = tint (*diff_pixel, 0x00008000); /* green */
else
*diff_pixel = tint (*diff_pixel, 0x00c00000); /* red */
ref_pixel++;
result_pixel++;
diff_pixel++;
}
}
return diff_image;
}
void
meta_ref_test_verify_view (ClutterStageView *view,
const char *test_name_unescaped,
int test_seq_no,
MetaReftestFlag flags)
{
cairo_surface_t *view_image;
const char *dist_dir;
g_autofree char *test_name = NULL;
g_autofree char *ref_image_path = NULL;
cairo_surface_t *ref_image;
cairo_status_t ref_status;
if (flags & META_REFTEST_FLAG_UPDATE_REF)
assert_software_rendered ();
view_image = capture_view (view);
test_name = g_strdup (test_name_unescaped + 1);
depathify (test_name);
dist_dir = g_test_get_dir (G_TEST_DIST);
ref_image_path = g_strdup_printf ("%s/tests/ref-tests/%s_%d.ref.png",
dist_dir,
test_name, test_seq_no);
ref_image = cairo_image_surface_create_from_png (ref_image_path);
g_assert_nonnull (ref_image);
ref_status = cairo_surface_status (ref_image);
if (flags & META_REFTEST_FLAG_UPDATE_REF)
{
g_assert (ref_status == CAIRO_STATUS_FILE_NOT_FOUND ||
ref_status == CAIRO_STATUS_SUCCESS);
if (ref_status == CAIRO_STATUS_SUCCESS)
ensure_expected_format (&ref_image);
if (ref_status == CAIRO_STATUS_SUCCESS &&
cairo_image_surface_get_width (ref_image) ==
cairo_image_surface_get_width (view_image) &&
cairo_image_surface_get_height (ref_image) ==
cairo_image_surface_get_height (view_image) &&
compare_images (ref_image, view_image, NULL, NULL))
{
g_message ("Not updating '%s', it didn't change.", ref_image_path);
}
else
{
g_message ("Updating '%s'.", ref_image_path);
g_assert_cmpint (cairo_surface_write_to_png (view_image, ref_image_path),
==,
CAIRO_STATUS_SUCCESS);
}
}
else
{
const Range gl_fuzz = { -3, 4 };
PixelDiffStat diff_stat = {};
g_assert_cmpint (ref_status, ==, CAIRO_STATUS_SUCCESS);
ensure_expected_format (&ref_image);
if (!compare_images (ref_image, view_image, &gl_fuzz,
&diff_stat))
{
cairo_surface_t *diff_image;
const char *build_dir;
g_autofree char *ref_image_copy_path = NULL;
g_autofree char *result_image_path = NULL;
g_autofree char *diff_image_path = NULL;
diff_image = visualize_difference (ref_image, view_image,
&gl_fuzz);
build_dir = g_test_get_dir (G_TEST_BUILT);
ref_image_copy_path =
g_strdup_printf ("%s/meson-logs/tests/ref-tests/%s_%d.ref.png",
build_dir,
test_name, test_seq_no);
result_image_path =
g_strdup_printf ("%s/meson-logs/tests/ref-tests/%s_%d.result.png",
build_dir,
test_name, test_seq_no);
diff_image_path =
g_strdup_printf ("%s/meson-logs/tests/ref-tests/%s_%d.diff.png",
build_dir,
test_name, test_seq_no);
g_mkdir_with_parents (g_path_get_dirname (ref_image_copy_path),
0755);
g_assert_cmpint (cairo_surface_write_to_png (ref_image,
ref_image_copy_path),
==,
CAIRO_STATUS_SUCCESS);
g_assert_cmpint (cairo_surface_write_to_png (view_image,
result_image_path),
==,
CAIRO_STATUS_SUCCESS);
g_assert_cmpint (cairo_surface_write_to_png (diff_image,
diff_image_path),
==,
CAIRO_STATUS_SUCCESS);
g_critical ("Pixel difference exceeds limits "
"(min: [%d, %d, %d, %d], "
"max: [%d, %d, %d, %d])\n"
"See %s, %s, and %s for details.",
diff_stat.ch[0].min_diff,
diff_stat.ch[1].min_diff,
diff_stat.ch[2].min_diff,
diff_stat.ch[3].min_diff,
diff_stat.ch[0].max_diff,
diff_stat.ch[1].max_diff,
diff_stat.ch[2].max_diff,
diff_stat.ch[3].max_diff,
ref_image_copy_path,
result_image_path,
diff_image_path);
}
}
cairo_surface_destroy (view_image);
cairo_surface_destroy (ref_image);
}
MetaReftestFlag
meta_ref_test_determine_ref_test_flag (void)
{
const char *update_tests;
char **update_test_rules;
int n_update_test_rules;
MetaReftestFlag flags;
int i;
update_tests = g_getenv ("MUTTER_REF_TEST_UPDATE");
if (!update_tests)
return META_REFTEST_FLAG_NONE;
if (strcmp (update_tests, "all") == 0)
return META_REFTEST_FLAG_UPDATE_REF;
update_test_rules = g_strsplit (update_tests, ",", -1);
n_update_test_rules = g_strv_length (update_test_rules);
g_assert_cmpint (n_update_test_rules, >, 0);
flags = META_REFTEST_FLAG_NONE;
for (i = 0; i < n_update_test_rules; i++)
{
char *rule = update_test_rules[i];
if (g_regex_match_simple (rule, g_test_get_path (), 0, 0))
{
flags |= META_REFTEST_FLAG_UPDATE_REF;
break;
}
}
g_strfreev (update_test_rules);
return flags;
}