From d7ce6a47f89884d9aa381a6ab7cd85002135f5cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20=C3=85dahl?= Date: Tue, 26 Jan 2021 17:07:09 +0100 Subject: [PATCH] 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: --- 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(+) create mode 100644 src/tests/meta-ref-test.c create mode 100644 src/tests/meta-ref-test.h create mode 100644 src/tests/ref-test-sanity.c create mode 100644 src/tests/ref-tests/tests_ref-test_sanity_0.ref.png create mode 100644 src/tests/ref-tests/tests_ref-test_sanity_1.ref.png 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 0000000000000000000000000000000000000000..f434827f89f9f708cbbf7c08873e72cbffb35938 GIT binary patch literal 6749 zcmV-j8lvTiP)`JN=Uip2e(ZlHZPs-d6&;!@*>a%KRIqir5giv5- zcKzRf{tEzAXjD~K6$%A3P!9m?-#``Sa1}8Bb@7~eqjh@k`PlQ(*irUf4+V7VEa zcN(bf2D(sE>EzJ#}Yn7(hC+y5F6q!=bgn*>oYyGGc2{^>4jb1(Z{)fJQx!Bi}{}0!H@K zytSh}IvuDrEr9g>ck+~(P_>tI??UHDEWa{4G{R))rssHYVp0IQaO2Z()CoTns%xC4 zx3#WCmOMp%IeA2cIZq?Z$DxE$qNwOlBH!-FwO7?}P_^JnC^zW41(A#zbL6`g>{|)u zxHW(8D)xM(JG#5ZQR^vBish{E#9q0GU~CbA_sY$`(9H_^A2`Bci^9MyeTWwhD!TbFv9lSJE${x06ev zublKbQyCo`dn7^RkV;g8f{xrStLThD&8mwK?kAg)*LR#M{FpjitNo3OwXS;p6hzrm zOP$Z3OjP1fz@Dn0$~&bK&MB_E7r#+isidaVKbZ0;HOC^UK(Kg%W{>lNzA7XJg@P^I z7<0SRxj>MWY^hmLEFWGFph(iOV$Ip3QFP*U3RP=)-R5pi$UR(M1d2}Vw823%o06Mr zVaATf7wu7IAXNc$zbj1uiBi)_xh4jGFP&3fL=f7h(uvsYyO_PZl&8=N^8HZ;i=?;- z25Y%?D{F`IZiPZ<)B_wvhMGe3*YW;??(SEwQC+(?v=@5R-AO=}|4sR4#R2ziAL5s9 zO))n9zGy=!Fa7S!Ko;vL7p3Hitd`o^>zp7W00?XC)px(U!Db>vt>AEt{>fas<^b(* zkl=RILvlP)kqwOqagsS-lzl4Lx1f=REju%1=rZjG6{plm7S>CVuee}{_(E)v;evoh zjdZ9O1%yvP>YxLX6y_%7udd0Gw@T_O<;0c8Lkj|$OH8@OvXFNniZ3&|#BGMpQc%I3 zW1~i{zeT}>>lfAavpEEXQJzQ;g=&k^j`8VWK+K)2WRA1`lt&ogVYJ7)wnV5j zK#WL5$bz`U^f7-IFC0v1VniM`7jcQ^+dFu9a5Px^iV9j z6JQ1J*zX=V#FxD=M=JBg+a4#{?Ld*?z$&SsddWo{zOG$rdB#01(c2VIIwkseV#4UK zw*DA!u>&K~x;$2qiAkp07{qV7nwEDG1X8@4(@`0Vt!r|2h5M`-vAxz*--q}WokTJF zf(Lg2TEeiUqQJ5hJz>=$F0Wfr2@!uP1`YAfabY;qbpNhabA9PCb<3)4V$fDJ6qKr5 zSy`;>I%x<~3eV5>ys~}&Osqt*t^wNFxl*$T3Ky@yvyQNhWVEVhl?36c@^~n=n1;be zPNK-nI@y;fYZ;*0AmBjF_}mVrXXD&CJ#Zi_<%=m&A<*0>K-W_%M%zItp(7N6TkKWc zZyaG{Q)7`WpS#%g6g?8Jlrcg2+E+InXPkB-qnJv{Zo7U9OFgJfVUD^#QI2P@M@QK7 zuX-v9)Fw4!T{GOC_m2=QnQ{`XVGQx@pC1FCIkE{@kL<+UXxSw;dFT|cbg=Ykte8IX zGQo7-jpVXJF%slfE-&{%RX|s}I)}t8s8_?|ywYeXL}+<`Lpa?&P5=UuN8ab7B4`Y6=s#h zC-k4(u=y)q8(y~u9j($o_VwB~+O3w+OGwuU#x*0y$z5W(Fl}`Z8jytkAspYZ5Oq+K zC?&U=$lwnG>i$uIbi3$?lJNfSANz~rP=x~yseJGbO^AS@SyBx7Dw|f;9DCDS$lo{F zmF`3H z)#*q1#hmT_@^)C|G@sdKxT&mzz|;J_xo)?#IAJpyaaBZf+#C%F(OlmlQBO3^Phu*4 z@KimgdTibmHI&kmi66zRJTKD`Qf10>Hv+sHi##g&a*s%q?kI_Zi6wYyg)xT1wgJA? z!1i!jipFX`KSJJl9+@Xl|AUG6ZqYU3ip^+Ml60|XmC|f$= z8m-9q-CebMs!io#LBIin={4XO<&KbwMpKlNm4l2G)YYwml0Dz)rl#PCyF?Jz0rMS7;q| zgnZW^D<;hQG?YxQ@lj7}lgSYBX>G4S$?cLCLjcv^Blgj`BuHA2nf8i&qzHxnk}Eeg zwkXA&M>7eRZN{7d`80%_I1)FOcXGV4IKFv$_d zQ6Nt6q5DWZl2<~|9DiI5oo>M)hce5)D~7TE+&qg7DP8QX_^u;kfE=LB|8)-T)qaS$ zbwh;F3qNu64UEG61zIbSMQ!z@pXp7NX^znA0MC2WKo$oOmp04k0}WvyXv`l~<;^eq zPwzCCFN&DTJZ+Ao^GtTz;r>1H@GcxkysYy<4Z@1=q^<(E_sa04Y4>pa1Y0M*{fG%f zXie|6#+jdIxj+Y~*#2fC$NQ71N&JUVgRY2$sJtKzJ#ooXPcbUNeYkbwvw}|x3X0UT z`9|zQ3)iS;o|ZcrWP=;`M3EIl&;Ba%k__jz_PlD&tG(`y*)ezkurU?W z6eg<2r6gK*OXN%v9s%on-%!F2s@PZK&v@;3FdJza{Ml7UImn!9BWP~|%jTtj_ad2Oq@*6t|LK<;GdaxM^J4$zwq9ClKG~%LAM=FH-`LvJhncLCFkYb=1 zD*XA8k5g# zi#;Bc7s^@Nfa%0!E-ABeJSHX^BQ0jHt)U6|FZZsqW}6afT~@LhR^h0UhT^+x9b!5a zPxGmMq&?5NTpLpOJWEIsxygHhESm zizsD=Rf#wo65rP~y>hM*k9BZ3ex3<)f0&V%FTAGa&6X7$Y{bx zxg`hent6(<;bj>cL;um)Mdz)hRoZUuTr9S&%T*$2a!CY504U13RzLD{GYk2B4$Vp4 z!sp%OnEgr<@d#+?iM^MvCRQYh;`Qyuq@TLKyPn|#)~{AzYy80}RB>nM4C`Q~i1 zNZdr#{Qlq*5JDGLE^fqK&@(!L?EQ_n9%L#EbJ3tB?oU~Nyol{FemwTiKm2J)vQ_~X zQkk|ikSsQTzH46hemlTs-rM~G-Tew;93f-yU6b7EwQX32*}C7P7bo4>Te$xr&-KNs8d zk@-}GHA>B#@KD_0*p?I)wmER|RGbKQaUb-2a*vj{&7xY?Z%B+-B2mdS|NG*29dw5# zvS%4n2d}LmBFti*PjNx*eM2oV-xP}(EFxrj7Jgq&m9$%W=GoE#PDZl+mdZ%Fj%v6s zS_YaQ%f66Hw4i_PA|oqV3Zr6#?%-2RR!jLX8@352_v?u_u`_`}t-l;kA;Gs47YUy# z-m`0~7kR(;vcBlpabEMIF!d!M*p)_{$xhI6Tt%!N^FI1V985^(VXh~{VHCITk5N#P zp?T`ATzLm^7Cr2xvou=m-woX6Z+%Sa*Tj_w*Yt;c+u6c!Q=KvEV*MQ%6~hSHyTYGs zEXoYJ-0$pcZ3U0(X?L{usT`g z)ZL+W^^~o>G4Af}-#D)VUevW6+pCP?F054HR}k>_pA917h)a53DEhbz+8Fq?|G>3> z-OBaIum3vjJx5SLSL0o$(?Ky%Dqg?-Ro)=kl!=F1n9&O#J~urkir~{&6+&X>l~9F7 zB$z1ZUi`%-y(GXpsrio}(@AaopDGFWF#Hm!!}{yKk1D9DeqXZNg6{{5g9aYx(H!lT z>5gExh3TYy5s!U&gmBP@O~g}^5miXtwm|mm0c%5_A_9SJL@m(%Jq@suAUG2artqtiDgU74j&Q9x?h%vQovzo(Ll0MB~ROIWdJc4 zjaE*~9G%}>#^haO_m<0oO_+zF7h=a~JLhFJ9oBc-2|-=-2E$jpLYebE5o>ju6ax1>m_FrFzR*3V4Lz*5_N z{P_2xfI}VuRoP>Jjt@pi-W%=I=y1Sbb`QYm{mN`&!=C?KU@RDmNVYtFo_>3-yR`7%6UvIGKvdBpZ zE+ek_y&KGMeoof5E+BE z)hbN(5$1=jm^WsnmpPr5TEKkIAodQO71D=x_iXAI-?rKX$yj!l=e8g#U5{b>c}wUJ zCd3~a`F&|gk})Y+m%MwE5!6eS3)8aRHd~5kg1oW$OUTdLdBb9KR>y~jrHLS_BXcRF zqZZD$I!nCqL+h&jtcg*u@1p|bw+}$};@SYTW-zgAMcS`Z+glr@txwX{;P2H+Bi)pY zt_)c!6i@Jrd%`AX+SgkCx*XRj|G&@6e6iEY3 z1#h&iwnU@xLSLq`#v}X%ey=#Y?{A3GR-ub85Q>})i{0K!O=s6_nf6gT{R`NoSvYU4QavewZIU=7~QY zCJkhEU@mC^p;%s1epuVg(&X~YvhLp2iJ5+mzbCR>!LN76XW}r0%@^aCg}v9ay>1gG z>(T1U&>FiS>~yLlTr7^X7I{{s91jt2vdl&U{7#ryK&XoKR7ih-eUuHGRz`w zqgRnJLnWai6TK{#D@yhb8d4SB@CB?QX9YI}ERwDkM|#ROdp)}!*3*>PWnh@vPS2ca zU=I`|x|<`hLkQj?)MD9~8xVWa!XPLqQ4>XTCo@s-_qfSVA)J})@kmux!^((en?N3E zQBv?Qm&h!M`xikL!8}aC)-;A~L_B2dUM^Nn0j6c!T0zs$vvb0a3n+UW#*~s#gfLh9 zhR3C+Nb9}I3+H^e)n~sn9lYzzevWt(VXgwgLEF^8NhVggH)7j>3X6ML5843(2}v$u1Cp>69*U-+c+BTv~AQALY9&hsB4BH=*?a02iFDlb82jZkWa^ zQUD^|@S5U37qd&n+in5SfJ|ux_Nn|d?-q^Uqi<|-0r0B|Nb1c+Q*)V;Qp%Sf=2!|b z$J5R7a9{3g^7pW7%(L&0!?yi6qCL>~VVRP9O3+dS=BI2o&pCZs3 z-j}sXLX$$F!R%`Shx=+?=m7rd`plZlr|agxB*`n11sE->EVBm;d&0w*(K)TwTi@%O zw5ROT3gE3ZNvUvOGZ_Gz@9ZW zw6nqGZoJoz9}q}nWhDSMr~dTUzXL!O8dcR*g+c)h)CBxT@6#)YfqhE?i~c7KMxDU{K>RomHqUuoA&KQK<4IYD*1NQCc@}0bNzogsN+r zR8`yVBv6G&%P_r1620JN4k8!n_-_Sm+l`m^X& z0p$=Ypi$T5o^LY+K_eqIhqkm!rvtT33nD##PnQ_dGN|*@U@EioE zPYOU6&i-k**GWGVs%t>gVbyDyC8Ve?M;wu1j??Jo15i>aSyTj+%r_ia_o^BVsuo>I zY~?3K$12FDz1!fApy+zXAm zcsWC(4m^ri1Zd>ficMW=y)S9Sjeu@z_0*`VEj8@1Z8r8?$>xGQXiMlMnN?MN-|86k z+Q)IU98r}Zof3R;Q|@_6n*qG)0?rJ_@F+V}@R-!+^$Ehg!jLkGOXtO->VZzd%-;@x@keb>)GM5@=OzC)p~< z0<17O$?-U9BP*!cXT)xj80D=0|I|BmC!Z`>Z!_>UyT+h0iI!Wxiz9;fNi? zm8U}M=qiBjd#wo|Q)*c$WbS+AHluxDnX;)*VJW{$qZh=9%VTsME(BNu(S7SPs;eoq zJ9)sXPqBaB_T`OHHDfFe7i#-{`39cD+)aK|U26sEB8V~*$85s-tmvhv2g`loX1+j_ z@p!Ok`qC%aSWnA$P~G5w>7*D(O&*jr3Rj4_Gk41TD>sdtO8vi_s%x6k%JQR1CgYrZ ziF#(~e9WOp`9q;D@ez`$O%K(0>sHke@rlt7y0)t35ECWMqvkiZggGs)Gsd9btT3g{ zoF?c`Y3XmUH8ar!nB7wjqcxJ}t{4zHNO713+x;qvzp^L?&lB+FKbN1sU;BU67Qq74 z%)6C0Wv$|b=#k$w$>Y+n`a?4+jn5NmZ?nu|pUkh<5{vA&U}Zo}M;kp3>2+pZ7Y9bA zHcRh(7X)PrC|t_891?IA@U5v3j6znkz){Hb?!gok8=CjBZ4i*yfcAG9*KZ0#nat(O zm1&Vw5iV7oY~s)6XwlY^_EkA_{=8DfEnYYn0)lno6er&)?UDK1zJqRv7mb{A!O;u8FA%x9D&bE2-d4X~EOk;q`zj09K{+u{kUI z!p*u~(ZY(nl)3Nl?y)La;u&}}KA!G>F1fbov20K!1pGU<6%q*(syK8De^_1+u)tA+ z&iUtJMt!!L!3Z&S4Ly&~(HDndWzapeb&M!2ScJ>bqOs^onm~$l`i{KI#rUn5)=+_O zjl&JEE6|F{hb>qV(KMJ8#3+_vy>{eDWrr{TID_~qdH&Zj3kP31a~V}C);H}iNC`2B z%?yFhNkCAY);jQ{pr}iKD_8~jsj2J=cFa92UY=l}W_vcJxcV2?0cSo$70{fAz7X-d>DAT(Zjo3fV$~JWw)4KNA_Q`&P`}!W$ z)Gh(N7qp_g+_CK!p?3CewWXEV=ICjrLi-uAR-=X&sS8(|O2^8)R@um25O(OM zJkFV|h3t^0IZo}51ZZ+8#&=G&aUH~xT)kMZ00F{L^ymrBe0MyzLikKEISGojR;YQC zj$}zw8m56cQB->=WK464mq#4&_v6VzJ@81);{?3sCm{I20g%f_U6iMaQH}?U@7y*i z#k`1>MCMCAah-Z+Dx6kV7}PN9wVZP~n@Fzc4`$!@Z&T^Z*|7ZBUyKLiXAXumnC^bg ztTZ2-00^<4w4CA9(Fb_%)C@NbXbi|^zv%134OsO-fsaAqrhvW7iLj0xfF5&-U}qNi zNlpE2HZ|U}3&VvyJiqdZK0m?6eKM^w>sZO&B=`W~n)AGmSG*jm`x+p#T2@r&zB(m7>{ZMYgno}m>uUvzMFP@$9VU0|1&P4cXB* zQ%u7hPIhzr7-X1hw$G*P$?wH_-h`>d6>pV+?Nd}|Zx8055@OUv%`H~c$d$XSvlQM2}| z%6l^)m+!xL)USoF#U|x1fpW<6IS%poMp_Rl6*R+p}ADp93m{ zy3Y^2!=58Bx6pHt#E<(#Ja%jdJf++WeC5^~FTti|$Ap-oGYmpk+5#>#3nN}=Iro_? z`l7kpzcq38+86{tht-c=AROz|6;cxZc-;YM#mJQU9myiuHtU9(tj~il&W39I4u3aH zv}iRa)gw5h_ge;2q35yEcbu=^nA_!Wr6rTS-~PFSEfl!S?Ik1nVRp>a);_En@;S(4q)q|5RLxe>c9;w=*bINxneCsxP~}Gr1|T-4bkoCp z3S>sFD#ZPZj>ZD>IbTf#52ND#8a*KVN8a=Kx5&E!zo?hPs`x%IE7pdLn66ipfQ*xMs_k8ccD&5cJRH(( zwg_KxroONmap^LP2;TYLAzja~x^J7?Rsn*^`;Xi?vIt5;_lMe0;uP5W!=E03*{JIv zpkJuQ!R=~BtDjR0HRh{nJ_&37A?dVqN50ig_sXj(0x z=8Yj+>Al;G{!~{j|0`MNOtovq)(3ig@D8Uk{QKe41*@^&i z&`o6*DCx~lR%7BWqI=V2!A+Qp1eOyHmyJjeLN#mx+0x`u@uhEJolw#6LNM-lQQ@|R z@3oo>ZrFL3J72`1jVMc4j4?nz(Ex5xzZ~jf&ECFEWM<7=4dHzowVDMgvRQ^B>zk-- zBpkMpO_K}dtg0Nk0?+A3dpog=h9I2n_Gv1k%0q*|3J5l-g z3>crR%ZT&ZWIa}?{5oWbnIdN~T6Pzt9C2iUV{@uu(+Y*ndh}KmeX&jA`*#O4h1%+Ac-( z`BLh_%%AnVA@4)ZS2RkG#5ovx1tS?J^u7iQ{Lq=-Z9olClQPI{$>;(eR5JQ)i55rf zH0-2m9VX`yX5&=M9d{+lyq&6Ad;oXE$`1Mv8+WaIj3(zOlWHs%5xVc@WbW!{Yr9MA z5GSM`D*3+Ik`!Y~vM#yjgd6lrl@(L9?zUP=XF|NO@k`3j;asp7pVh}Zi!J0%?iD@E z3CQEE&N6SC>2w$7Jw(Dj*V(9V4}$E)wH?s9gN0=?Qg0OWy$0|;=05@DwOcifa8ock zGi0hze`3H|r<*x!QyY4exGwd%2cK@M0*@j!^0yYual~YLLKaZYt;f>`SvAs9p4Td| zWp#({B+uNDWdgw|V#Jm}98`A>K+UAFefBXrsio-h=nrAgi-l6*C~ThTS>!5OMMAO) zrTc(+MJTH4U2l$r-P2mm=qCcH>1do&>2X`n?L?;>D;fdXUh}b7YLHQwxiK3k)l8!t z+l#%wka?UATpInwQLX=*4zbYr&Ha-#pKA=BG0O{<6#1hD08E+p%JwW;KZD;{s8Auw%b%Q3jED> zS!vg^EiXKdVNMjL%M?aRs+Xj3>A$n8IJDo4n0sRbJa>8v^a6rA;m}e3d5n0NBaggr ztZ3*TGT=d#wgSKIk;D1Wb%jF8sP|;35NSnGr&wjOQ*oM24K$MZ0nib-#w#0EoP3Cs zKP|iKICGyzwP_$xo~d)=cvbS_EVKRYvDCD1JYJ>tvlL)g5=fbqLj@*VaCa5fD{_MW z)+FB@=%@M32#!W!NLmMph+CL4ho{1|V-3yS5NA#Bdy57)ULxz-vxBG%t5woJ3IxK>&mPdNZHs^YFJgkR!a+ZPJ)CN6c zrUC3zG{s6cX05dCq_bl{Rce~d=oddZ)SB(eNR$mLSCo&b9D)<{VOzO+*4klGq+bo$ z?fXG?nX?exjgc53gl>^)QEkiu#GbS?2uVuTMAF=wSt$5(VDeK4$DNFLgetRPWsl}G zfe>j)Qs^+N$S8^Rix7)Y9)@7+Hl}U#c-YvzT&kP~Ox1R4MNL!B-~^6KC?gJYO2H^W zm@7V`rs=ePuNE-gCwkMb(Zq2i;(8_;?HfE7;Yg!2C8 zfN4;X1CZzjYU=;Fn6p$|b_;?AWlA+Lrow5C78}2N?-=9)m}=E7#RX%-UMCL`jh zlaG3!w|j5qDj7{0g$ARqNgR%=d7(S-Yu0D#WIbJ&gOe1m2o_+xtfI^anD(THIiqu0 zQ*V7<-;_N?pK1UPt=W%T_J>h$DgST}dtxc4s9Ym9Cuyif6jHF7D19$DDu{ON+c`H8 z5jEKHiVJybDtktpmam1`LJ+bk2@Lo{^`u`*$^wA@0T+ndqo&4BE&u=k07*qoM6N<$ Ef_*!JbN~PV literal 0 HcmV?d00001