diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 24ce3f063..1829461f6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,9 +36,12 @@ jobs: env: DESTDIR: out run: | - meson build -Ddocumentation=true + meson build -Ddocumentation=true -Dtests=true ninja -C build ninja -C build install + - name: Run Tests + run: | + meson test -v -C build fedora: runs-on: ubuntu-latest @@ -93,3 +96,4 @@ jobs: io.elementary.vala-lint -d lib io.elementary.vala-lint -d plugins io.elementary.vala-lint -d src + io.elementary.vala-lint -d tests diff --git a/lib/Gestures/ActorTarget.vala b/lib/Gestures/ActorTarget.vala index 97a287a85..cba526a44 100644 --- a/lib/Gestures/ActorTarget.vala +++ b/lib/Gestures/ActorTarget.vala @@ -11,12 +11,6 @@ * If a new child (or target via {@link add_target}) is added, its progress will be synced. */ public class Gala.ActorTarget : Clutter.Actor, GestureTarget { - public Clutter.Actor? actor { - get { - return this; - } - } - public bool animating { get { return ongoing_animations > 0; } } private double[] current_progress; diff --git a/lib/Gestures/GestureTarget.vala b/lib/Gestures/GestureTarget.vala index ad39067de..638797ece 100644 --- a/lib/Gestures/GestureTarget.vala +++ b/lib/Gestures/GestureTarget.vala @@ -13,11 +13,5 @@ public interface Gala.GestureTarget : Object { END } - /** - * The actor manipulated by the gesture. The associated frame clock - * will be used for animation timelines. - */ - public abstract Clutter.Actor? actor { get; } - public virtual void propagate (UpdateType update_type, GestureAction action, double progress) { } } diff --git a/lib/Gestures/PropertyTarget.vala b/lib/Gestures/PropertyTarget.vala index 13a606035..d2a971624 100644 --- a/lib/Gestures/PropertyTarget.vala +++ b/lib/Gestures/PropertyTarget.vala @@ -7,27 +7,34 @@ public class Gala.PropertyTarget : Object, GestureTarget { public GestureAction action { get; construct; } - - //we don't want to hold a strong reference to the actor because we might've been added to it which would form a reference cycle - private weak Clutter.Actor? _actor; - public Clutter.Actor? actor { get { return _actor; } } - + // Don't take a reference since we are most of the time owned by the target + public weak Object? target { get; private set; } public string property { get; construct; } public Clutter.Interval interval { get; construct; } - public PropertyTarget (GestureAction action, Clutter.Actor actor, string property, Type value_type, Value from_value, Value to_value) { + public PropertyTarget (GestureAction action, Object target, string property, Type value_type, Value from_value, Value to_value) { Object (action: action, property: property, interval: new Clutter.Interval.with_values (value_type, from_value, to_value)); - _actor = actor; - _actor.destroy.connect (() => _actor = null); + this.target = target; + this.target.weak_ref (on_target_disposed); + } + + ~PropertyTarget () { + if (target != null) { + target.weak_unref (on_target_disposed); + } + } + + private void on_target_disposed () { + target = null; } public override void propagate (UpdateType update_type, GestureAction action, double progress) { - if (update_type != UPDATE || action != this.action) { + if (target == null || update_type != UPDATE || action != this.action) { return; } - actor.set_property (property, interval.compute (progress)); + target.set_property (property, interval.compute (progress)); } } diff --git a/lib/Gestures/RootTarget.vala b/lib/Gestures/RootTarget.vala index 214347b45..fac6afcaa 100644 --- a/lib/Gestures/RootTarget.vala +++ b/lib/Gestures/RootTarget.vala @@ -6,6 +6,12 @@ */ public interface Gala.RootTarget : Object, GestureTarget { + /** + * The actor manipulated by the gesture. The associated frame clock + * will be used for animation timelines. + */ + public abstract Clutter.Actor? actor { get; } + public void add_gesture_controller (GestureController controller) requires (controller.target == null) { controller.attached (this); weak_ref (controller.detached); diff --git a/meson.build b/meson.build index bff4da79c..054408e3f 100644 --- a/meson.build +++ b/meson.build @@ -168,6 +168,9 @@ subdir('plugins/template') if get_option('documentation') subdir('docs') endif +if get_option('tests') + subdir('tests') +endif subdir('po') vapigen = find_program('vapigen', required: false) diff --git a/meson_options.txt b/meson_options.txt index 54b3ac94c..2fbd384a6 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,3 +1,4 @@ option ('documentation', type : 'boolean', value : false) +option ('tests', type : 'boolean', value : false) option ('systemd', type : 'boolean', value : true) option ('systemduserunitdir', type : 'string', value : '') diff --git a/src/ShellClients/PanelWindow.vala b/src/ShellClients/PanelWindow.vala index 3008fe9a5..4a4547463 100644 --- a/src/ShellClients/PanelWindow.vala +++ b/src/ShellClients/PanelWindow.vala @@ -8,6 +8,7 @@ public class Gala.PanelWindow : ShellWindow, RootTarget { private static HashTable window_struts = new HashTable (null, null); + public Clutter.Actor? actor { get { return (Clutter.Actor) window.get_compositor_private (); } } public WindowManager wm { get; construct; } public Pantheon.Desktop.Anchor anchor { get; construct set; } diff --git a/src/ShellClients/ShellClientsManager.vala b/src/ShellClients/ShellClientsManager.vala index b18691cf0..c56ee07b7 100644 --- a/src/ShellClients/ShellClientsManager.vala +++ b/src/ShellClients/ShellClientsManager.vala @@ -20,8 +20,6 @@ public class Gala.ShellClientsManager : Object, GestureTarget { return instance; } - public Clutter.Actor? actor { get { return wm.stage; } } - public WindowManager wm { get; construct; } private NotificationsClient notifications_client; diff --git a/src/ShellClients/ShellWindow.vala b/src/ShellClients/ShellWindow.vala index 586627ade..d82459aab 100644 --- a/src/ShellClients/ShellWindow.vala +++ b/src/ShellClients/ShellWindow.vala @@ -6,7 +6,6 @@ */ public class Gala.ShellWindow : PositionedWindow, GestureTarget { - public Clutter.Actor? actor { get { return window_actor; } } public bool restore_previous_x11_region { private get; set; default = false; } public bool visible_in_multitasking_view { get; set; default = false; } diff --git a/src/Widgets/MultitaskingView/MultitaskingView.vala b/src/Widgets/MultitaskingView/MultitaskingView.vala index fefb94ea3..4ee643cc9 100644 --- a/src/Widgets/MultitaskingView/MultitaskingView.vala +++ b/src/Widgets/MultitaskingView/MultitaskingView.vala @@ -26,6 +26,7 @@ public class Gala.MultitaskingView : ActorTarget, RootTarget, ActivatableCompone private GestureController workspaces_gesture_controller; private GestureController multitasking_gesture_controller; + public Clutter.Actor? actor { get { return this; } } public WindowManagerGala wm { get; construct; } private Meta.Display display; diff --git a/src/Widgets/MultitaskingView/WindowClone.vala b/src/Widgets/MultitaskingView/WindowClone.vala index 44830a23a..dba31632c 100644 --- a/src/Widgets/MultitaskingView/WindowClone.vala +++ b/src/Widgets/MultitaskingView/WindowClone.vala @@ -27,6 +27,7 @@ public class Gala.WindowClone : ActorTarget, RootTarget { */ public signal void request_reposition (); + public Clutter.Actor? actor { get { return this; } } public WindowManager wm { get; construct; } public Meta.Window window { get; construct; } diff --git a/src/Widgets/WindowOverview.vala b/src/Widgets/WindowOverview.vala index 78ead9884..dc78017d7 100644 --- a/src/Widgets/WindowOverview.vala +++ b/src/Widgets/WindowOverview.vala @@ -10,6 +10,7 @@ public class Gala.WindowOverview : ActorTarget, RootTarget, ActivatableComponent private const int TOP_GAP = 30; private const int BOTTOM_GAP = 100; + public Clutter.Actor? actor { get { return this; } } public WindowManager wm { get; construct; } private GestureController gesture_controller; // Currently not used for actual touchpad gestures but only as controller diff --git a/tests/TestCase.vala b/tests/TestCase.vala new file mode 100644 index 000000000..c3e31d543 --- /dev/null +++ b/tests/TestCase.vala @@ -0,0 +1,82 @@ +/* + * Copyright 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/** + * A simple test case class. To use it inherit from it and add test methods + * in the constructor using {@link add_test}. Override {@link set_up} and {@link tear_down} + * to provide per-test-method setup and teardown. Then add a `main()` function + * and return the result of {@link run}. + */ +public abstract class Gala.TestCase : Object { + public delegate void TestMethod (); + + public string name { get; construct; } + + private GLib.TestSuite suite; + private Adaptor[] adaptors = new Adaptor[0]; + + construct { + suite = new GLib.TestSuite (name); + } + + public int run (string[] args) { + Test.init (ref args); + TestSuite.get_root ().add_suite ((owned) suite); + return Test.run (); + } + + protected void add_test (string name, owned TestMethod test) { + var adaptor = new Adaptor (name, (owned) test, this); + adaptors += adaptor; + + var test_case = new GLib.TestCase ( + adaptor.name, + adaptor.set_up, + adaptor.run, + adaptor.tear_down + ); + + suite.add ((owned) test_case); + } + + public virtual void set_up () { + } + + public virtual void tear_down () { + } + + public void assert_finalize_object (ref G data) { + unowned var weak_pointer = data; + ((Object) data).add_weak_pointer (&weak_pointer); + data = null; + assert_null (weak_pointer); + } + + private class Adaptor : Object { + public string name { get; construct; } + + private TestMethod test; + private TestCase test_case; + + public Adaptor (string name, owned TestMethod test, TestCase test_case) { + Object (name: name); + + this.test = (owned) test; + this.test_case = test_case; + } + + public void set_up (void* fixture) { + test_case.set_up (); + } + + public void run (void* fixture) { + test (); + } + + public void tear_down (void* fixture) { + test_case.tear_down (); + } + } +} diff --git a/tests/lib/PropertyTargetTest.vala b/tests/lib/PropertyTargetTest.vala new file mode 100644 index 000000000..055376a1b --- /dev/null +++ b/tests/lib/PropertyTargetTest.vala @@ -0,0 +1,154 @@ +/* + * Copyright 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +public class MockObject : Object { + public int int_value { get; set; } + public double double_value { get; set; } +} + +public class Gala.PropertyTargetTest : TestCase { + private MockObject? target; + private PropertyTarget? default_int_prop_target; + + public PropertyTargetTest () { + Object (name: "PropertyTarget"); + } + + construct { + add_test ("simple propagation", test_simple_propagation); + add_test ("double propagation", test_double_propagation); + add_test ("other actions", test_other_actions); + add_test ("finalize object first", test_finalize_object_first); + add_test ("finalize property target first", test_finalize_property_target_first); + } + + public override void set_up () { + target = new MockObject (); + default_int_prop_target = new PropertyTarget ( + MULTITASKING_VIEW, + target, + "int-value", + typeof(int), + 0, + 10 + ); + } + + public override void tear_down () { + target = null; + default_int_prop_target = null; + } + + private void test_simple_propagation () { + assert_nonnull (&default_int_prop_target); + assert_nonnull (&target); + assert_cmpint (target.int_value, EQ, 0); + + default_int_prop_target.propagate (UPDATE, MULTITASKING_VIEW, 0.0); + assert_cmpint (target.int_value, EQ, 0); + + default_int_prop_target.propagate (UPDATE, MULTITASKING_VIEW, 0.5); + assert_cmpint (target.int_value, EQ, 5); + + default_int_prop_target.propagate (UPDATE, MULTITASKING_VIEW, 1.0); + assert_cmpint (target.int_value, EQ, 10); + + default_int_prop_target.propagate (UPDATE, MULTITASKING_VIEW, 0.8); + assert_cmpint (target.int_value, EQ, 8); + + default_int_prop_target.propagate (UPDATE, MULTITASKING_VIEW, 0.3); + assert_cmpint (target.int_value, EQ, 3); + + default_int_prop_target.propagate (UPDATE, MULTITASKING_VIEW, 0.6); + assert_cmpint (target.int_value, EQ, 6); + + assert_finalize_object (ref target); + assert_finalize_object (ref default_int_prop_target); + } + + private void test_double_propagation () { + var double_prop_target = new PropertyTarget ( + MULTITASKING_VIEW, + target, + "double-value", + typeof(double), + 0.0, + 2.0 + ); + + assert_nonnull (&double_prop_target); + assert_nonnull (&target); + assert_cmpfloat (target.double_value, EQ, 0.0); + + double_prop_target.propagate (UPDATE, MULTITASKING_VIEW, 0.0); + assert_cmpfloat (target.double_value, EQ, 0.0); + + double_prop_target.propagate (UPDATE, MULTITASKING_VIEW, 0.5); + assert_cmpfloat (target.double_value, EQ, 1.0); + + double_prop_target.propagate (UPDATE, MULTITASKING_VIEW, 1.0); + assert_cmpfloat (target.double_value, EQ, 2.0); + + double_prop_target.propagate (UPDATE, MULTITASKING_VIEW, 0.8); + assert_cmpfloat (target.double_value, EQ, 1.6); + + double_prop_target.propagate (UPDATE, MULTITASKING_VIEW, 0.3); + assert_cmpfloat (target.double_value, EQ, 0.6); + + double_prop_target.propagate (UPDATE, MULTITASKING_VIEW, 0.6); + assert_cmpfloat (target.double_value, EQ, 1.2); + + assert_finalize_object (ref target); + assert_finalize_object (ref double_prop_target); + } + + private void test_other_actions () { + assert_nonnull (&default_int_prop_target); + + assert_nonnull (&target); + assert_cmpint (target.int_value, EQ, 0); + + default_int_prop_target.propagate (UPDATE, MULTITASKING_VIEW, 0.0); + assert_cmpint (target.int_value, EQ, 0); + + default_int_prop_target.propagate (UPDATE, MULTITASKING_VIEW, 0.5); + assert_cmpint (target.int_value, EQ, 5); + + default_int_prop_target.propagate (UPDATE, SWITCH_WORKSPACE, 1.0); + assert_cmpint (target.int_value, EQ, 5); + + default_int_prop_target.propagate (UPDATE, CUSTOM, 1.0); + assert_cmpint (target.int_value, EQ, 5); + + default_int_prop_target.propagate (UPDATE, MULTITASKING_VIEW, 1.0); + assert_cmpint (target.int_value, EQ, 10); + + assert_finalize_object (ref target); + assert_finalize_object (ref default_int_prop_target); + } + + private void test_finalize_object_first () { + assert_nonnull (&target); + assert_nonnull (&default_int_prop_target); + + // We can finalize the object before the property target because it doesn't hold a strong reference to it + assert_finalize_object (ref target); + assert_finalize_object (ref default_int_prop_target); + } + + private void test_finalize_property_target_first () { + assert_nonnull (&target); + assert_nonnull (&default_int_prop_target); + + // Finalize the property target before the object and make sure we don't have weak references + // to the object (i.e. we don't crash when finalizing the object) + assert_finalize_object (ref default_int_prop_target); + assert_finalize_object (ref target); + } +} + +public int main (string[] args) { + return new Gala.PropertyTargetTest ().run (args); +} diff --git a/tests/lib/meson.build b/tests/lib/meson.build new file mode 100644 index 000000000..18d01ab23 --- /dev/null +++ b/tests/lib/meson.build @@ -0,0 +1,22 @@ +lib_test_sources = files( + meson.project_source_root() / 'lib' / 'Gestures' / 'Gesture.vala', + meson.project_source_root() / 'lib' / 'Gestures' / 'GestureTarget.vala', + meson.project_source_root() / 'lib' / 'Gestures' / 'PropertyTarget.vala', +) + +tests = [ + 'PropertyTargetTest', +] + +foreach test : tests + test_executable = executable( + test, + '@0@.vala'.format(test), + common_test_sources, + lib_test_sources, + dependencies: gala_base_dep, + install: false, + ) + + test(test, test_executable, suite: ['Gala', 'Gala/lib']) +endforeach diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 000000000..a24aa529b --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,5 @@ +common_test_sources = files( + 'TestCase.vala', +) + +subdir('lib')