diff --git a/clutter/clutter/clutter-stage-view-private.h b/clutter/clutter/clutter-stage-view-private.h
index feee44e9e..345b59564 100644
--- a/clutter/clutter/clutter-stage-view-private.h
+++ b/clutter/clutter/clutter-stage-view-private.h
@@ -44,6 +44,7 @@ void clutter_stage_view_invalidate_projection (ClutterStageView *view);
void clutter_stage_view_set_projection (ClutterStageView *view,
const graphene_matrix_t *matrix);
+CLUTTER_EXPORT
void clutter_stage_view_add_redraw_clip (ClutterStageView *view,
const cairo_rectangle_int_t *clip);
@@ -63,6 +64,7 @@ void clutter_stage_view_transform_rect_to_onscreen (ClutterStageView
int dst_height,
cairo_rectangle_int_t *dst_rect);
+CLUTTER_EXPORT
void clutter_stage_view_schedule_update (ClutterStageView *view);
void clutter_stage_view_notify_presented (ClutterStageView *view,
diff --git a/src/backends/meta-monitor-manager-private.h b/src/backends/meta-monitor-manager-private.h
index fd30ebb58..61bfead0f 100644
--- a/src/backends/meta-monitor-manager-private.h
+++ b/src/backends/meta-monitor-manager-private.h
@@ -392,6 +392,7 @@ gboolean meta_monitor_manager_get_max_screen_size (MetaMonitorManager
MetaLogicalMonitorLayoutMode
meta_monitor_manager_get_default_layout_mode (MetaMonitorManager *manager);
+META_EXPORT_TEST
MetaVirtualMonitor * meta_monitor_manager_create_virtual_monitor (MetaMonitorManager *manager,
const MetaVirtualMonitorInfo *info,
GError **error);
diff --git a/src/backends/meta-stage-private.h b/src/backends/meta-stage-private.h
index 03214218f..07534f6e3 100644
--- a/src/backends/meta-stage-private.h
+++ b/src/backends/meta-stage-private.h
@@ -21,6 +21,7 @@
#define META_STAGE_PRIVATE_H
#include "backends/meta-cursor.h"
+#include "core/util-private.h"
#include "meta/boxes.h"
#include "meta/meta-stage.h"
#include "meta/types.h"
@@ -62,12 +63,14 @@ gboolean meta_overlay_is_visible (MetaOverlay *overlay);
void meta_stage_set_active (MetaStage *stage,
gboolean is_active);
+META_EXPORT_TEST
MetaStageWatch * meta_stage_watch_view (MetaStage *stage,
ClutterStageView *view,
MetaStageWatchPhase watch_mode,
MetaStageWatchFunc callback,
gpointer user_data);
+META_EXPORT_TEST
void meta_stage_remove_watch (MetaStage *stage,
MetaStageWatch *watch);
diff --git a/src/backends/meta-virtual-monitor.h b/src/backends/meta-virtual-monitor.h
index f2d102f00..d1fd43356 100644
--- a/src/backends/meta-virtual-monitor.h
+++ b/src/backends/meta-virtual-monitor.h
@@ -47,6 +47,7 @@ struct _MetaVirtualMonitorClass
GObjectClass parent_class;
};
+META_EXPORT_TEST
MetaVirtualMonitorInfo * meta_virtual_monitor_info_new (int width,
int height,
float refresh_rate,
@@ -54,6 +55,7 @@ MetaVirtualMonitorInfo * meta_virtual_monitor_info_new (int width,
const char *product,
const char *serial);
+META_EXPORT_TEST
void meta_virtual_monitor_info_free (MetaVirtualMonitorInfo *info);
MetaCrtc * meta_virtual_monitor_get_crtc (MetaVirtualMonitor *virtual_monitor);
diff --git a/src/tests/meson.build b/src/tests/meson.build
index fbdca617c..816ae9810 100644
--- a/src/tests/meson.build
+++ b/src/tests/meson.build
@@ -157,6 +157,11 @@ anonymous_file_test = executable('anonymous-file-tests',
install_dir: mutter_installed_tests_libexecdir,
)
+ref_test_sources = [
+ 'meta-ref-test.c',
+ 'meta-ref-test.h',
+]
+
if have_native_tests
native_headless_tests = executable('mutter-native-headless-tests',
sources: [
@@ -170,6 +175,20 @@ if have_native_tests
install: have_installed_tests,
install_dir: mutter_installed_tests_libexecdir,
)
+
+ ref_test_sanity = executable('mutter-ref-test-sanity',
+ sources: [
+ 'ref-test-sanity.c',
+ 'test-utils.c',
+ 'test-utils.h',
+ ref_test_sources,
+ ],
+ include_directories: tests_includepath,
+ c_args: tests_c_args,
+ dependencies: [tests_deps],
+ install: have_installed_tests,
+ install_dir: mutter_installed_tests_libexecdir,
+ )
endif
stacking_tests = [
@@ -246,4 +265,11 @@ if have_native_tests
is_parallel: false,
timeout: 60,
)
+
+ test('ref-test-sanity', ref_test_sanity,
+ suite: ['core', 'mutter/ref-test/sanity'],
+ env: test_env,
+ is_parallel: false,
+ timeout: 60,
+ )
endif
diff --git a/src/tests/meta-ref-test.c b/src/tests/meta-ref-test.c
new file mode 100644
index 000000000..84ac8876d
--- /dev/null
+++ b/src/tests/meta-ref-test.c
@@ -0,0 +1,610 @@
+/*
+ * 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 .
+ */
+
+/*
+ * 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
+#include
+
+#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;
+}
diff --git a/src/tests/meta-ref-test.h b/src/tests/meta-ref-test.h
new file mode 100644
index 000000000..7a71e388f
--- /dev/null
+++ b/src/tests/meta-ref-test.h
@@ -0,0 +1,39 @@
+/*
+ * 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 .
+ */
+
+#ifndef META_REF_TEST_H
+#define META_REF_TEST_H
+
+#include
+
+#include "clutter/clutter/clutter.h"
+#include "meta/boxes.h"
+
+typedef enum _MetaReftestFlag
+{
+ META_REFTEST_FLAG_NONE = 0,
+ META_REFTEST_FLAG_UPDATE_REF = 1 << 0,
+} MetaReftestFlag;
+
+void meta_ref_test_verify_view (ClutterStageView *view,
+ const char *test_name,
+ int test_seq_no,
+ MetaReftestFlag flags);
+
+MetaReftestFlag meta_ref_test_determine_ref_test_flag (void);
+
+#endif /* META_REF_TEST_H */
diff --git a/src/tests/ref-test-sanity.c b/src/tests/ref-test-sanity.c
new file mode 100644
index 000000000..91710feeb
--- /dev/null
+++ b/src/tests/ref-test-sanity.c
@@ -0,0 +1,169 @@
+/*
+ * 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 .
+ *
+ */
+
+#include "config.h"
+
+#include "backends/meta-virtual-monitor.h"
+#include "backends/native/meta-renderer-native.h"
+#include "compositor/meta-plugin-manager.h"
+#include "core/main-private.h"
+#include "meta/main.h"
+#include "tests/meta-ref-test.h"
+#include "tests/test-utils.h"
+
+static MetaVirtualMonitor *virtual_monitor;
+
+static void
+setup_test_environment (void)
+{
+ MetaBackend *backend = meta_get_backend ();
+ MetaSettings *settings = meta_backend_get_settings (backend);
+ MetaMonitorManager *monitor_manager =
+ meta_backend_get_monitor_manager (backend);
+ MetaRenderer *renderer = meta_backend_get_renderer (backend);
+ g_autoptr (MetaVirtualMonitorInfo) monitor_info = NULL;
+ GError *error = NULL;
+ GList *views;
+
+ meta_settings_override_experimental_features (settings);
+ meta_settings_enable_experimental_feature (
+ settings,
+ META_EXPERIMENTAL_FEATURE_SCALE_MONITOR_FRAMEBUFFER);
+
+ monitor_info = meta_virtual_monitor_info_new (100, 100, 60.0,
+ "MetaTestVendor",
+ "MetaVirtualMonitor",
+ "0x1234");
+ virtual_monitor = meta_monitor_manager_create_virtual_monitor (monitor_manager,
+ monitor_info,
+ &error);
+ if (!virtual_monitor)
+ g_error ("Failed to create virtual monitor: %s", error->message);
+
+ meta_monitor_manager_reload (monitor_manager);
+
+ views = meta_renderer_get_views (renderer);
+ g_assert_cmpint (g_list_length (views), ==, 1);
+}
+
+static void
+tear_down_test_environment (void)
+{
+ MetaBackend *backend = meta_get_backend ();
+ MetaMonitorManager *monitor_manager =
+ meta_backend_get_monitor_manager (backend);
+
+ g_object_unref (virtual_monitor);
+ meta_monitor_manager_reload (monitor_manager);
+}
+
+static gboolean
+run_tests (gpointer data)
+{
+ int ret;
+
+ setup_test_environment ();
+
+ ret = g_test_run ();
+
+ tear_down_test_environment ();
+
+ meta_quit (ret != 0);
+
+ return ret;
+}
+
+static ClutterStageView *
+get_view (void)
+{
+ MetaBackend *backend = meta_get_backend ();
+ MetaRenderer *renderer = meta_backend_get_renderer (backend);
+
+ return CLUTTER_STAGE_VIEW (meta_renderer_get_views (renderer)->data);
+}
+
+static void
+meta_test_ref_test_sanity (void)
+{
+ MetaBackend *backend = meta_get_backend ();
+ ClutterActor *stage = meta_backend_get_stage (backend);
+ ClutterActor *actor1;
+ ClutterActor *actor2;
+
+ meta_ref_test_verify_view (get_view (),
+ g_test_get_path (), 0,
+ meta_ref_test_determine_ref_test_flag ());
+
+ actor1 = clutter_actor_new ();
+ clutter_actor_set_position (actor1, 10, 10);
+ clutter_actor_set_size (actor1, 50, 50);
+ clutter_actor_set_background_color (actor1, CLUTTER_COLOR_Orange);
+ clutter_actor_add_child (stage, actor1);
+
+ meta_ref_test_verify_view (get_view (),
+ g_test_get_path (), 1,
+ meta_ref_test_determine_ref_test_flag ());
+
+ actor2 = clutter_actor_new ();
+ clutter_actor_set_position (actor2, 20, 20);
+ clutter_actor_set_size (actor2, 50, 50);
+ clutter_actor_set_background_color (actor2, CLUTTER_COLOR_SkyBlue);
+ clutter_actor_add_child (stage, actor2);
+
+ g_test_expect_message (G_LOG_DOMAIN,
+ G_LOG_LEVEL_CRITICAL,
+ "Pixel difference exceeds limits*");
+
+ meta_ref_test_verify_view (get_view (),
+ g_test_get_path (), 1,
+ meta_ref_test_determine_ref_test_flag ());
+
+ g_test_assert_expected_messages ();
+
+ clutter_actor_destroy (actor2);
+ clutter_actor_destroy (actor1);
+}
+
+static void
+init_ref_test_sanity_tests (void)
+{
+ g_test_add_func ("/tests/ref-test/sanity",
+ meta_test_ref_test_sanity);
+}
+
+int
+main (int argc,
+ char **argv)
+{
+ test_init (&argc, &argv);
+ init_ref_test_sanity_tests ();
+
+ meta_plugin_manager_load (test_get_plugin_name ());
+
+ meta_override_compositor_configuration (META_COMPOSITOR_TYPE_WAYLAND,
+ META_TYPE_BACKEND_NATIVE,
+ "headless", TRUE,
+ NULL);
+
+ meta_init ();
+ meta_register_with_session ();
+
+ g_idle_add (run_tests, NULL);
+
+ return meta_run ();
+}
diff --git a/src/tests/ref-tests/tests_ref-test_sanity_0.ref.png b/src/tests/ref-tests/tests_ref-test_sanity_0.ref.png
new file mode 100644
index 000000000..f434827f8
Binary files /dev/null and b/src/tests/ref-tests/tests_ref-test_sanity_0.ref.png differ
diff --git a/src/tests/ref-tests/tests_ref-test_sanity_1.ref.png b/src/tests/ref-tests/tests_ref-test_sanity_1.ref.png
new file mode 100644
index 000000000..29567348e
Binary files /dev/null and b/src/tests/ref-tests/tests_ref-test_sanity_1.ref.png differ