[mutter] tests: Add reference test framework



commit d7ce6a47f89884d9aa381a6ab7cd85002135f5cf
Author: Jonas Ådahl <jadahl gmail com>
Date:   Tue Jan 26 17:07:09 2021 +0100

    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>

 clutter/clutter/clutter-stage-view-private.h       |   2 +
 src/backends/meta-monitor-manager-private.h        |   1 +
 src/backends/meta-stage-private.h                  |   3 +
 src/backends/meta-virtual-monitor.h                |   2 +
 src/tests/meson.build                              |  26 +
 src/tests/meta-ref-test.c                          | 610 +++++++++++++++++++++
 src/tests/meta-ref-test.h                          |  39 ++
 src/tests/ref-test-sanity.c                        | 169 ++++++
 .../ref-tests/tests_ref-test_sanity_0.ref.png      | Bin 0 -> 6749 bytes
 .../ref-tests/tests_ref-test_sanity_1.ref.png      | Bin 0 -> 5402 bytes
 10 files changed, 852 insertions(+)
---
diff --git a/clutter/clutter/clutter-stage-view-private.h b/clutter/clutter/clutter-stage-view-private.h
index feee44e9e5..345b595644 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 fd30ebb58e..61bfead0f3 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 03214218fd..07534f6e39 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 f2d102f00e..d1fd43356f 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 fbdca617c4..816ae98109 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 0000000000..84ac8876d7
--- /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 <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;
+}
diff --git a/src/tests/meta-ref-test.h b/src/tests/meta-ref-test.h
new file mode 100644
index 0000000000..7a71e388f0
--- /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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef META_REF_TEST_H
+#define META_REF_TEST_H
+
+#include <glib.h>
+
+#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 0000000000..91710feeb8
--- /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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#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 0000000000..f434827f89
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 0000000000..29567348e6
Binary files /dev/null and b/src/tests/ref-tests/tests_ref-test_sanity_1.ref.png differ


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]