diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8eb6ef19b..201ea5393 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,9 +39,15 @@ jobs: meson build -Ddocumentation=true -Dtests=true ninja -C build ninja -C build install + - name: Prepare Test Environment + run: | + mkdir -p -m 700 ${{ github.workspace }}/runtime-dir + mkdir -m 1777 /tmp/.X11-unix - name: Run Tests + env: + XDG_RUNTIME_DIR: ${{ github.workspace }}/runtime-dir run: | - meson test -v -C build + dbus-run-session -- meson test -v -C build fedora: runs-on: ubuntu-latest diff --git a/tests/MutterTestCase.vala b/tests/MutterTestCase.vala new file mode 100644 index 000000000..6b43abc91 --- /dev/null +++ b/tests/MutterTestCase.vala @@ -0,0 +1,81 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.MockPlugin : Meta.Plugin { + +} + +/** + * Base class for tests that need to interact with Mutter and Clutter. + * Sets up a Meta.Context, starts it, and provides access to the context + * and stage for derived test classes. Note that the context and therefore + * stage are shared between all tests and not recreated during set_up/tear_down, + * because Mutter doesn't allow that. + * If you need the main loop to run use {@link run_main_loop} and {@link quit_main_loop} + * instead of {@link Meta.Context.run_main_loop} and {@link Meta.Context.terminate}. + */ +public abstract class Gala.MutterTestCase : Gala.TestCase { + private const string[] MUTTER_ARGS = { + "--wayland", "--headless", "--no-x11", + "--wayland-display", "wayland-1", + "--virtual-monitor", "1280x720@60" + }; + + protected Meta.Context context { get; private set; } + protected Clutter.Stage stage { get { return (Clutter.Stage) context.get_backend ().get_stage (); } } + + private MainLoop? main_loop; + + construct { + context = new Meta.Context (""); + + unowned var unowned_args = MUTTER_ARGS; + try { + context.configure (ref unowned_args); + } catch (Error e) { + assert_no_error (e); + } + + context.set_plugin_gtype (typeof (MockPlugin)); + + try { + context.setup (); + } catch (Error e) { + assert_no_error (e); + } + + try { + context.start (); + } catch (Error e) { + assert_no_error (e); + } + } + + public override void set_up () { + Test.log_set_fatal_handler ((domain, level, message) => { + /* Mutter sends a fatal log when failing to connect to colord but that doesn't matter for us */ + Test.message ("Got fatal log, not aborting"); + return false; + }); + + main_loop = new MainLoop (null, false); + } + + public override void tear_down () { + main_loop = null; + } + + protected void run_main_loop () { + assert_true (main_loop != null); + main_loop.run (); + } + + protected void quit_main_loop () { + assert_true (main_loop != null); + main_loop.quit (); + } +} diff --git a/tests/lib/SetupTest.vala b/tests/lib/SetupTest.vala new file mode 100644 index 000000000..f90ac1455 --- /dev/null +++ b/tests/lib/SetupTest.vala @@ -0,0 +1,94 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +/** + * More of a test for our testing infrastructure and not really + * for testing any specific functionality of Gala. + * Check that the MutterTestCase base class successfully sets up meta and clutter + * and allows us to interact with it, e.g. by creating a Clutter actor. + */ +public class Gala.SetupTest : MutterTestCase { + public SetupTest () { + Object (name: "SetupTest"); + } + + construct { + add_test ("Test setup successful", test_setup_successful); + add_test ("Test main loop", test_main_loop); + add_test ("Test main loop can run twice", test_main_loop); + add_test ("Test actor animation", test_actor_animation); + } + + /** + * Check whether setup was successful, i.e. we have a + * backend, clutter backend, context, etc. + */ + private void test_setup_successful () { + assert_true (context != null); + + var display = context.get_display (); + assert_true (display != null); + + var backend = context.get_backend (); + assert_true (backend != null); + + var stage = backend.get_stage (); + assert_true (stage != null); + assert_true (stage is Clutter.Stage); + assert_true (this.stage == stage); + + // Creating an actor requires clutter machinery to be set up, so check this + var actor = new Clutter.Actor (); + assert_true (actor != null); + } + + private void test_main_loop () { + assert_true (context != null); + + var ran = false; + + Idle.add_once (() => ran = true); + Idle.add_once (quit_main_loop); + + run_main_loop (); + + assert_true (ran); + } + + private void test_actor_animation () { + assert_true (stage != null); + + var frames = 0; + + var timeline = new Clutter.Timeline.for_actor (stage, 100); + timeline.new_frame.connect (() => frames++); + + stage.show (); + + timeline.start (); + + Timeout.add (50, () => { + assert_true (timeline.is_playing ()); + return Source.REMOVE; + }); + + Timeout.add (150, () => { + assert_false (timeline.is_playing ()); + quit_main_loop (); + return Source.REMOVE; + }); + + run_main_loop (); + + Test.message ("Got %d frames", frames); + assert_cmpint (frames, GT, 0); + } +} + +public int main (string[] args) { + return new Gala.SetupTest ().run (args); +} diff --git a/tests/lib/meson.build b/tests/lib/meson.build index b7404d33a..91dd4f93a 100644 --- a/tests/lib/meson.build +++ b/tests/lib/meson.build @@ -8,6 +8,7 @@ lib_test_sources = files( tests = [ 'PropertyTargetTest', + 'SetupTest', ] foreach test : tests @@ -20,5 +21,5 @@ foreach test : tests install: false, ) - test(test, test_executable, suite: ['Gala', 'Gala/lib']) + test(test, test_executable, suite: ['Gala', 'Gala/lib'], is_parallel: false) endforeach diff --git a/tests/meson.build b/tests/meson.build index a24aa529b..5d7a42304 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,4 +1,5 @@ common_test_sources = files( + 'MutterTestCase.vala', 'TestCase.vala', )