From 614bc77b83cccdd3f58ee749e7259e965bbb1a21 Mon Sep 17 00:00:00 2001 From: Sophie Winter Date: Sun, 26 Jan 2025 00:10:57 -0800 Subject: [PATCH 01/15] Refactor integration test meson to support lock tests --- test/check-all-tests-are-in-meson.py | 3 +- .../integration-test-common.h | 1 + .../meson.build | 2 +- .../test-adapts-to-screen-size.c | 0 .../test-auto-exclusive-zone-no-margin.c | 0 ...st-auto-exclusive-zone-weird-bool-values.c | 0 .../test-auto-exclusive-zone-with-margin.c | 0 .../test-close-layer-surface.c | 0 .../test-create-xdg-toplevel.c | 0 .../test-creation-properties.c | 0 .../test-exclusive-zone-below-negative-1.c | 0 .../test-get-auto-exclusive-zone.c | 0 .../test-get-explicit-exclusive-zone.c | 0 .../test-get-keyboard-mode.c | 0 .../test-get-layer.c | 0 .../test-get-margin.c | 0 .../test-get-monitor.c | 0 .../test-get-namespace-custom-namespace.c | 0 .../test-get-namespace-default.c | 0 .../test-get-namespace-on-non-layer-window.c | 0 .../test-hide-and-show.c | 0 .../test-init-after-window-created.c | 0 .../test-is-layer-window.c | 0 .../test-is-supported-true.c | 0 .../test-layer-surface-not-created.c | 0 .../test-menu-popup.c | 0 .../test-multi-anchors.c | 0 .../test-set-anchor-normalizes-booleans.c | 0 .../test-set-default-size.c | 0 .../test-set-keyboard-mode.c | 0 .../test-set-layer.c | 0 .../test-set-margin.c | 0 .../test-set-monitor.c | 0 .../test-single-anchors.c | 0 .../test-uses-widget-size.c | 0 test/lock-tests/meson.build | 3 ++ ...test-lock-can-be-created-without-locking.c | 12 ++++++ test/meson.build | 37 +++++++++++-------- test/mock-server/mock-server.h | 1 + 39 files changed, 41 insertions(+), 18 deletions(-) rename test/{integration-tests => layer-tests}/meson.build (97%) rename test/{integration-tests => layer-tests}/test-adapts-to-screen-size.c (100%) rename test/{integration-tests => layer-tests}/test-auto-exclusive-zone-no-margin.c (100%) rename test/{integration-tests => layer-tests}/test-auto-exclusive-zone-weird-bool-values.c (100%) rename test/{integration-tests => layer-tests}/test-auto-exclusive-zone-with-margin.c (100%) rename test/{integration-tests => layer-tests}/test-close-layer-surface.c (100%) rename test/{integration-tests => layer-tests}/test-create-xdg-toplevel.c (100%) rename test/{integration-tests => layer-tests}/test-creation-properties.c (100%) rename test/{integration-tests => layer-tests}/test-exclusive-zone-below-negative-1.c (100%) rename test/{integration-tests => layer-tests}/test-get-auto-exclusive-zone.c (100%) rename test/{integration-tests => layer-tests}/test-get-explicit-exclusive-zone.c (100%) rename test/{integration-tests => layer-tests}/test-get-keyboard-mode.c (100%) rename test/{integration-tests => layer-tests}/test-get-layer.c (100%) rename test/{integration-tests => layer-tests}/test-get-margin.c (100%) rename test/{integration-tests => layer-tests}/test-get-monitor.c (100%) rename test/{integration-tests => layer-tests}/test-get-namespace-custom-namespace.c (100%) rename test/{integration-tests => layer-tests}/test-get-namespace-default.c (100%) rename test/{integration-tests => layer-tests}/test-get-namespace-on-non-layer-window.c (100%) rename test/{integration-tests => layer-tests}/test-hide-and-show.c (100%) rename test/{integration-tests => layer-tests}/test-init-after-window-created.c (100%) rename test/{integration-tests => layer-tests}/test-is-layer-window.c (100%) rename test/{integration-tests => layer-tests}/test-is-supported-true.c (100%) rename test/{integration-tests => layer-tests}/test-layer-surface-not-created.c (100%) rename test/{integration-tests => layer-tests}/test-menu-popup.c (100%) rename test/{integration-tests => layer-tests}/test-multi-anchors.c (100%) rename test/{integration-tests => layer-tests}/test-set-anchor-normalizes-booleans.c (100%) rename test/{integration-tests => layer-tests}/test-set-default-size.c (100%) rename test/{integration-tests => layer-tests}/test-set-keyboard-mode.c (100%) rename test/{integration-tests => layer-tests}/test-set-layer.c (100%) rename test/{integration-tests => layer-tests}/test-set-margin.c (100%) rename test/{integration-tests => layer-tests}/test-set-monitor.c (100%) rename test/{integration-tests => layer-tests}/test-single-anchors.c (100%) rename test/{integration-tests => layer-tests}/test-uses-widget-size.c (100%) create mode 100644 test/lock-tests/meson.build create mode 100644 test/lock-tests/test-lock-can-be-created-without-locking.c diff --git a/test/check-all-tests-are-in-meson.py b/test/check-all-tests-are-in-meson.py index 6c59c88..e32a7bd 100755 --- a/test/check-all-tests-are-in-meson.py +++ b/test/check-all-tests-are-in-meson.py @@ -31,7 +31,8 @@ def check_dir(dir_path): if __name__ == '__main__': test_dir = path.dirname(path.realpath(__file__)) - check_dir(path.join(test_dir, 'integration-tests')) + check_dir(path.join(test_dir, 'layer-tests')) + check_dir(path.join(test_dir, 'lock-tests')) check_dir(path.join(test_dir, 'smoke-tests')) check_dir(path.join(test_dir, 'unit-tests')) if dead_tests: diff --git a/test/integration-test-common/integration-test-common.h b/test/integration-test-common/integration-test-common.h index 0b813b8..e96d908 100644 --- a/test/integration-test-common/integration-test-common.h +++ b/test/integration-test-common/integration-test-common.h @@ -2,6 +2,7 @@ #define TEST_CLIENT_COMMON_H #include "gtk4-layer-shell.h" +#include "gtk4-session-lock.h" #include "test-common.h" #include #include diff --git a/test/integration-tests/meson.build b/test/layer-tests/meson.build similarity index 97% rename from test/integration-tests/meson.build rename to test/layer-tests/meson.build index 6a2f97a..f2856c8 100644 --- a/test/integration-tests/meson.build +++ b/test/layer-tests/meson.build @@ -1,4 +1,4 @@ -integration_tests = [ +layer_tests = [ 'test-is-supported-true', 'test-layer-surface-not-created', 'test-creation-properties', diff --git a/test/integration-tests/test-adapts-to-screen-size.c b/test/layer-tests/test-adapts-to-screen-size.c similarity index 100% rename from test/integration-tests/test-adapts-to-screen-size.c rename to test/layer-tests/test-adapts-to-screen-size.c diff --git a/test/integration-tests/test-auto-exclusive-zone-no-margin.c b/test/layer-tests/test-auto-exclusive-zone-no-margin.c similarity index 100% rename from test/integration-tests/test-auto-exclusive-zone-no-margin.c rename to test/layer-tests/test-auto-exclusive-zone-no-margin.c diff --git a/test/integration-tests/test-auto-exclusive-zone-weird-bool-values.c b/test/layer-tests/test-auto-exclusive-zone-weird-bool-values.c similarity index 100% rename from test/integration-tests/test-auto-exclusive-zone-weird-bool-values.c rename to test/layer-tests/test-auto-exclusive-zone-weird-bool-values.c diff --git a/test/integration-tests/test-auto-exclusive-zone-with-margin.c b/test/layer-tests/test-auto-exclusive-zone-with-margin.c similarity index 100% rename from test/integration-tests/test-auto-exclusive-zone-with-margin.c rename to test/layer-tests/test-auto-exclusive-zone-with-margin.c diff --git a/test/integration-tests/test-close-layer-surface.c b/test/layer-tests/test-close-layer-surface.c similarity index 100% rename from test/integration-tests/test-close-layer-surface.c rename to test/layer-tests/test-close-layer-surface.c diff --git a/test/integration-tests/test-create-xdg-toplevel.c b/test/layer-tests/test-create-xdg-toplevel.c similarity index 100% rename from test/integration-tests/test-create-xdg-toplevel.c rename to test/layer-tests/test-create-xdg-toplevel.c diff --git a/test/integration-tests/test-creation-properties.c b/test/layer-tests/test-creation-properties.c similarity index 100% rename from test/integration-tests/test-creation-properties.c rename to test/layer-tests/test-creation-properties.c diff --git a/test/integration-tests/test-exclusive-zone-below-negative-1.c b/test/layer-tests/test-exclusive-zone-below-negative-1.c similarity index 100% rename from test/integration-tests/test-exclusive-zone-below-negative-1.c rename to test/layer-tests/test-exclusive-zone-below-negative-1.c diff --git a/test/integration-tests/test-get-auto-exclusive-zone.c b/test/layer-tests/test-get-auto-exclusive-zone.c similarity index 100% rename from test/integration-tests/test-get-auto-exclusive-zone.c rename to test/layer-tests/test-get-auto-exclusive-zone.c diff --git a/test/integration-tests/test-get-explicit-exclusive-zone.c b/test/layer-tests/test-get-explicit-exclusive-zone.c similarity index 100% rename from test/integration-tests/test-get-explicit-exclusive-zone.c rename to test/layer-tests/test-get-explicit-exclusive-zone.c diff --git a/test/integration-tests/test-get-keyboard-mode.c b/test/layer-tests/test-get-keyboard-mode.c similarity index 100% rename from test/integration-tests/test-get-keyboard-mode.c rename to test/layer-tests/test-get-keyboard-mode.c diff --git a/test/integration-tests/test-get-layer.c b/test/layer-tests/test-get-layer.c similarity index 100% rename from test/integration-tests/test-get-layer.c rename to test/layer-tests/test-get-layer.c diff --git a/test/integration-tests/test-get-margin.c b/test/layer-tests/test-get-margin.c similarity index 100% rename from test/integration-tests/test-get-margin.c rename to test/layer-tests/test-get-margin.c diff --git a/test/integration-tests/test-get-monitor.c b/test/layer-tests/test-get-monitor.c similarity index 100% rename from test/integration-tests/test-get-monitor.c rename to test/layer-tests/test-get-monitor.c diff --git a/test/integration-tests/test-get-namespace-custom-namespace.c b/test/layer-tests/test-get-namespace-custom-namespace.c similarity index 100% rename from test/integration-tests/test-get-namespace-custom-namespace.c rename to test/layer-tests/test-get-namespace-custom-namespace.c diff --git a/test/integration-tests/test-get-namespace-default.c b/test/layer-tests/test-get-namespace-default.c similarity index 100% rename from test/integration-tests/test-get-namespace-default.c rename to test/layer-tests/test-get-namespace-default.c diff --git a/test/integration-tests/test-get-namespace-on-non-layer-window.c b/test/layer-tests/test-get-namespace-on-non-layer-window.c similarity index 100% rename from test/integration-tests/test-get-namespace-on-non-layer-window.c rename to test/layer-tests/test-get-namespace-on-non-layer-window.c diff --git a/test/integration-tests/test-hide-and-show.c b/test/layer-tests/test-hide-and-show.c similarity index 100% rename from test/integration-tests/test-hide-and-show.c rename to test/layer-tests/test-hide-and-show.c diff --git a/test/integration-tests/test-init-after-window-created.c b/test/layer-tests/test-init-after-window-created.c similarity index 100% rename from test/integration-tests/test-init-after-window-created.c rename to test/layer-tests/test-init-after-window-created.c diff --git a/test/integration-tests/test-is-layer-window.c b/test/layer-tests/test-is-layer-window.c similarity index 100% rename from test/integration-tests/test-is-layer-window.c rename to test/layer-tests/test-is-layer-window.c diff --git a/test/integration-tests/test-is-supported-true.c b/test/layer-tests/test-is-supported-true.c similarity index 100% rename from test/integration-tests/test-is-supported-true.c rename to test/layer-tests/test-is-supported-true.c diff --git a/test/integration-tests/test-layer-surface-not-created.c b/test/layer-tests/test-layer-surface-not-created.c similarity index 100% rename from test/integration-tests/test-layer-surface-not-created.c rename to test/layer-tests/test-layer-surface-not-created.c diff --git a/test/integration-tests/test-menu-popup.c b/test/layer-tests/test-menu-popup.c similarity index 100% rename from test/integration-tests/test-menu-popup.c rename to test/layer-tests/test-menu-popup.c diff --git a/test/integration-tests/test-multi-anchors.c b/test/layer-tests/test-multi-anchors.c similarity index 100% rename from test/integration-tests/test-multi-anchors.c rename to test/layer-tests/test-multi-anchors.c diff --git a/test/integration-tests/test-set-anchor-normalizes-booleans.c b/test/layer-tests/test-set-anchor-normalizes-booleans.c similarity index 100% rename from test/integration-tests/test-set-anchor-normalizes-booleans.c rename to test/layer-tests/test-set-anchor-normalizes-booleans.c diff --git a/test/integration-tests/test-set-default-size.c b/test/layer-tests/test-set-default-size.c similarity index 100% rename from test/integration-tests/test-set-default-size.c rename to test/layer-tests/test-set-default-size.c diff --git a/test/integration-tests/test-set-keyboard-mode.c b/test/layer-tests/test-set-keyboard-mode.c similarity index 100% rename from test/integration-tests/test-set-keyboard-mode.c rename to test/layer-tests/test-set-keyboard-mode.c diff --git a/test/integration-tests/test-set-layer.c b/test/layer-tests/test-set-layer.c similarity index 100% rename from test/integration-tests/test-set-layer.c rename to test/layer-tests/test-set-layer.c diff --git a/test/integration-tests/test-set-margin.c b/test/layer-tests/test-set-margin.c similarity index 100% rename from test/integration-tests/test-set-margin.c rename to test/layer-tests/test-set-margin.c diff --git a/test/integration-tests/test-set-monitor.c b/test/layer-tests/test-set-monitor.c similarity index 100% rename from test/integration-tests/test-set-monitor.c rename to test/layer-tests/test-set-monitor.c diff --git a/test/integration-tests/test-single-anchors.c b/test/layer-tests/test-single-anchors.c similarity index 100% rename from test/integration-tests/test-single-anchors.c rename to test/layer-tests/test-single-anchors.c diff --git a/test/integration-tests/test-uses-widget-size.c b/test/layer-tests/test-uses-widget-size.c similarity index 100% rename from test/integration-tests/test-uses-widget-size.c rename to test/layer-tests/test-uses-widget-size.c diff --git a/test/lock-tests/meson.build b/test/lock-tests/meson.build new file mode 100644 index 0000000..1796630 --- /dev/null +++ b/test/lock-tests/meson.build @@ -0,0 +1,3 @@ +lock_tests = [ + 'test-lock-can-be-created-without-locking', +] diff --git a/test/lock-tests/test-lock-can-be-created-without-locking.c b/test/lock-tests/test-lock-can-be-created-without-locking.c new file mode 100644 index 0000000..1b1123c --- /dev/null +++ b/test/lock-tests/test-lock-can-be-created-without-locking.c @@ -0,0 +1,12 @@ +#include "integration-test-common.h" + +static void callback_0() { + EXPECT_MESSAGE(ext_session_lock_v1 .lock); + + GtkSessionLockInstance* lock = gtk_session_lock_instance_new(); + g_object_unref(lock); +} + +TEST_CALLBACKS( + callback_0, +) diff --git a/test/meson.build b/test/meson.build index 77b7807..af64f70 100644 --- a/test/meson.build +++ b/test/meson.build @@ -1,7 +1,8 @@ subdir('test-common') subdir('mock-server') subdir('integration-test-common') -subdir('integration-tests') +subdir('layer-tests') +subdir('lock-tests') subdir('unit-tests') py = find_program('python3') @@ -10,21 +11,25 @@ run_test_script = files(meson.current_source_dir() + '/run-integration-test.py') env = environment() env.set('GTK4_LAYER_SHELL_BUILD', meson.build_root()) -foreach integration_test : integration_tests - integration_test_srcs = files('integration-tests/' + integration_test + '.c') - exe = executable( - integration_test, - integration_test_srcs, - dependencies: [gtk, wayland_client, gtk_layer_shell, integration_test_common]) - test( - 'integration-' + integration_test, - py, - workdir: meson.current_source_dir(), - env: env, - args: [ - run_test_script, - meson.current_build_dir() + '/' + integration_test, - ]) +foreach test_list : [['layer', layer_tests], ['lock', lock_tests]] + test_prefix = test_list[0] + test_dir = test_prefix + '-tests' + foreach test_name : test_list[1] + test_full_name = test_prefix + '-' + test_name + exe = executable( + test_full_name, + files(join_paths(test_dir, test_name + '.c')), + dependencies: [gtk, wayland_client, gtk_layer_shell, integration_test_common]) + test( + test_full_name, + py, + workdir: meson.current_source_dir(), + env: env, + args: [ + run_test_script, + meson.current_build_dir() + '/' + test_full_name, + ]) + endforeach endforeach if get_option('smoke-tests') diff --git a/test/mock-server/mock-server.h b/test/mock-server/mock-server.h index 765e95d..81c0d9a 100644 --- a/test/mock-server/mock-server.h +++ b/test/mock-server/mock-server.h @@ -12,6 +12,7 @@ #include #include "xdg-shell-server.h" #include "xdg-dialog-v1-server.h" +#include "ext-session-lock-v1-server.h" #include "wlr-layer-shell-unstable-v1-server.h" extern struct wl_display* display; From ef4be886fcd49686fb08e59433bd38082146399e Mon Sep 17 00:00:00 2001 From: Sophie Winter Date: Sun, 26 Jan 2025 00:28:44 -0800 Subject: [PATCH 02/15] run-integration-test.py: validate expectations at end of run (fix logic error) --- test/run-integration-test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/run-integration-test.py b/test/run-integration-test.py index b392aad..e5a3076 100755 --- a/test/run-integration-test.py +++ b/test/run-integration-test.py @@ -217,6 +217,7 @@ def verify_result(lines: List[str]): section_start = 0 set_expectation = False checked_expectation = False + for i, line in enumerate(lines): if line.startswith('EXPECT: '): assertions.append(line.split()[1:]) @@ -231,12 +232,14 @@ def verify_result(lines: List[str]): if line_contains(line, negative_assertion): section = format_stream('relevant section', '\n'.join(lines[section_start:i + 1])) raise TestError(section + '\n\nunexpected message matching "' + ' '.join(negative_assertion) + '"') - elif line == 'CHECK EXPECTATIONS COMPLETED' or i == len(lines) - 1: + + if line == 'CHECK EXPECTATIONS COMPLETED' or i == len(lines) - 1: checked_expectation = True if assertions: section = format_stream('relevant section', '\n'.join(lines[section_start:i])) raise TestError(section + '\n\ndid not find "' + ' '.join(assertions[0]) + '"') section_start = i + 1 + if not set_expectation or not checked_expectation: # If the test didn't use the right expectation format or something we don't want to silently pass raise TestError('test did not correctly set and check an expectation') From cff79f15588496a71a87c230a98961360ad6158e Mon Sep 17 00:00:00 2001 From: Sophie Winter Date: Sun, 26 Jan 2025 01:31:08 -0800 Subject: [PATCH 03/15] GH actions: ninja -C build test -> meson test -C build --verbose --- .github/workflows/build_and_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index fc3b85c..1207907 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -22,6 +22,6 @@ jobs: - name: Build run: ninja -C build - name: Test - run: ninja -C build test + run: meson test -C build --verbose - name: Install run: sudo ninja -C build install From 17b875c967fd7a4fe099752887431a0a336a9284 Mon Sep 17 00:00:00 2001 From: Sophie Winter Date: Sun, 26 Jan 2025 01:31:38 -0800 Subject: [PATCH 04/15] GH actions: don't install, no need --- .github/workflows/build_and_test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 1207907..48965bd 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -23,5 +23,3 @@ jobs: run: ninja -C build - name: Test run: meson test -C build --verbose - - name: Install - run: sudo ninja -C build install From 4b1e9e283cd71043fba98dc26d0f89237e26e5fe Mon Sep 17 00:00:00 2001 From: Sophie Winter Date: Sun, 26 Jan 2025 02:13:43 -0800 Subject: [PATCH 05/15] run-integration-test.py: don't enforece negative assertions from previous section --- test/run-integration-test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/run-integration-test.py b/test/run-integration-test.py index e5a3076..b5ea405 100755 --- a/test/run-integration-test.py +++ b/test/run-integration-test.py @@ -239,6 +239,7 @@ def verify_result(lines: List[str]): section = format_stream('relevant section', '\n'.join(lines[section_start:i])) raise TestError(section + '\n\ndid not find "' + ' '.join(assertions[0]) + '"') section_start = i + 1 + negative_assertions = [] if not set_expectation or not checked_expectation: # If the test didn't use the right expectation format or something we don't want to silently pass From cfa72fa177fa126e2a31cecbadb8e43e76a3cf6a Mon Sep 17 00:00:00 2001 From: Sophie Winter Date: Sun, 26 Jan 2025 02:17:13 -0800 Subject: [PATCH 06/15] Add and improve popup tests --- test/layer-tests/meson.build | 4 +- test/layer-tests/test-layer-surface-popup.c | 44 ++++++++++++++++ test/layer-tests/test-menu-popup.c | 31 ----------- ...st-xdg-toplevel-popup-with-layer-surface.c | 52 +++++++++++++++++++ ...xdg-toplevel-popup-without-layer-surface.c | 46 ++++++++++++++++ test/mock-server/overrides.c | 5 +- 6 files changed, 148 insertions(+), 34 deletions(-) create mode 100644 test/layer-tests/test-layer-surface-popup.c delete mode 100644 test/layer-tests/test-menu-popup.c create mode 100644 test/layer-tests/test-xdg-toplevel-popup-with-layer-surface.c create mode 100644 test/layer-tests/test-xdg-toplevel-popup-without-layer-surface.c diff --git a/test/layer-tests/meson.build b/test/layer-tests/meson.build index f2856c8..ecba9b0 100644 --- a/test/layer-tests/meson.build +++ b/test/layer-tests/meson.build @@ -19,7 +19,9 @@ layer_tests = [ 'test-get-explicit-exclusive-zone', 'test-get-auto-exclusive-zone', 'test-exclusive-zone-below-negative-1', - 'test-menu-popup', + 'test-layer-surface-popup', + 'test-xdg-toplevel-popup-with-layer-surface', + 'test-xdg-toplevel-popup-without-layer-surface', 'test-close-layer-surface', 'test-get-namespace-default', 'test-get-namespace-on-non-layer-window', diff --git a/test/layer-tests/test-layer-surface-popup.c b/test/layer-tests/test-layer-surface-popup.c new file mode 100644 index 0000000..9c89957 --- /dev/null +++ b/test/layer-tests/test-layer-surface-popup.c @@ -0,0 +1,44 @@ +#include "integration-test-common.h" + +static GtkWindow *layer_window; +static GtkWidget *layer_dropdown; +static const char *options[] = {"Foo", "Bar", "Baz", NULL}; + +static void callback_0() { + EXPECT_MESSAGE(zwlr_layer_shell_v1 .get_layer_surface); + + // The popup is weirdly slow to open, so slow the tests down + step_time = 600; + + layer_window = GTK_WINDOW(gtk_window_new()); + layer_dropdown = gtk_drop_down_new_from_strings(options); + gtk_window_set_child(layer_window, layer_dropdown); + gtk_layer_init_for_window(layer_window); + gtk_window_present(layer_window); +} + +static void callback_1() { + EXPECT_MESSAGE(xdg_wm_base .get_xdg_surface); + EXPECT_MESSAGE(xdg_surface .get_popup nil); + EXPECT_MESSAGE(zwlr_layer_surface_v1 .get_popup); + EXPECT_MESSAGE(xdg_popup .grab); + + DONT_EXPECT_MESSAGE(xdg_popup .destroy); + + g_signal_emit_by_name(layer_dropdown, "activate", NULL); +} + +static void callback_2() { + EXPECT_MESSAGE(xdg_popup .destroy); + EXPECT_MESSAGE(xdg_surface .destroy); + EXPECT_MESSAGE(zwlr_layer_surface_v1 .destroy); + + gtk_window_close(layer_window); + gtk_window_close(layer_window); +} + +TEST_CALLBACKS( + callback_0, + callback_1, + callback_2, +) diff --git a/test/layer-tests/test-menu-popup.c b/test/layer-tests/test-menu-popup.c deleted file mode 100644 index 37cd7d3..0000000 --- a/test/layer-tests/test-menu-popup.c +++ /dev/null @@ -1,31 +0,0 @@ -#include "integration-test-common.h" - -static GtkWindow *window; -static GtkWidget *dropdown; -static const char *options[] = {"Foo", "Bar", "Baz", NULL}; - -static void callback_0() { - EXPECT_MESSAGE(zwlr_layer_shell_v1 .get_layer_surface); - - // The popup is weirdly slow to open, so slow the tests down - step_time = 600; - - window = GTK_WINDOW(gtk_window_new()); - dropdown = gtk_drop_down_new_from_strings(options); - gtk_window_set_child(window, dropdown); - gtk_layer_init_for_window(window); - gtk_window_present(window); -} - -static void callback_1() { - EXPECT_MESSAGE(xdg_wm_base .get_xdg_surface); - EXPECT_MESSAGE(xdg_surface .get_popup); - EXPECT_MESSAGE(xdg_popup .grab); - - g_signal_emit_by_name (dropdown, "activate", NULL); -} - -TEST_CALLBACKS( - callback_0, - callback_1, -) diff --git a/test/layer-tests/test-xdg-toplevel-popup-with-layer-surface.c b/test/layer-tests/test-xdg-toplevel-popup-with-layer-surface.c new file mode 100644 index 0000000..89f64fb --- /dev/null +++ b/test/layer-tests/test-xdg-toplevel-popup-with-layer-surface.c @@ -0,0 +1,52 @@ +#include "integration-test-common.h" + +static GtkWindow *layer_window; +static GtkWindow *normal_window; +static GtkWidget *normal_dropdown; +static const char *options[] = {"Foo", "Bar", "Baz", NULL}; + +static void callback_0() { + EXPECT_MESSAGE(zwlr_layer_shell_v1 .get_layer_surface); + EXPECT_MESSAGE(xdg_wm_base .get_xdg_surface); + EXPECT_MESSAGE(xdg_surface .get_toplevel); + + // The popup is weirdly slow to open, so slow the tests down + step_time = 600; + + layer_window = create_default_window(); + gtk_layer_init_for_window(layer_window); + gtk_window_present(layer_window); + + normal_window = GTK_WINDOW(gtk_window_new()); + normal_dropdown = gtk_drop_down_new_from_strings(options); + gtk_window_set_child(normal_window, normal_dropdown); + gtk_window_present(normal_window); +} + +static void callback_1() { + EXPECT_MESSAGE(xdg_wm_base .get_xdg_surface); + EXPECT_MESSAGE(xdg_surface .get_popup xdg_surface); + EXPECT_MESSAGE(xdg_popup .grab); + + DONT_EXPECT_MESSAGE(zwlr_layer_surface_v1 .get_popup); + DONT_EXPECT_MESSAGE(xdg_popup .destroy); + + g_signal_emit_by_name(normal_dropdown, "activate", NULL); +} + +static void callback_2() { + EXPECT_MESSAGE(xdg_popup .destroy); + EXPECT_MESSAGE(xdg_surface .destroy); + EXPECT_MESSAGE(xdg_toplevel .destroy); + EXPECT_MESSAGE(xdg_surface .destroy); + EXPECT_MESSAGE(zwlr_layer_surface_v1 .destroy); + + gtk_window_close(normal_window); + gtk_window_close(layer_window); +} + +TEST_CALLBACKS( + callback_0, + callback_1, + callback_2, +) diff --git a/test/layer-tests/test-xdg-toplevel-popup-without-layer-surface.c b/test/layer-tests/test-xdg-toplevel-popup-without-layer-surface.c new file mode 100644 index 0000000..2179e98 --- /dev/null +++ b/test/layer-tests/test-xdg-toplevel-popup-without-layer-surface.c @@ -0,0 +1,46 @@ +#include "integration-test-common.h" + +static GtkWindow *normal_window; +static GtkWidget *normal_dropdown; +static const char *options[] = {"Foo", "Bar", "Baz", NULL}; + +static void callback_0() { + EXPECT_MESSAGE(xdg_wm_base .get_xdg_surface); + EXPECT_MESSAGE(xdg_surface .get_toplevel); + + DONT_EXPECT_MESSAGE(zwlr_layer_shell_v1 .get_layer_surface); + + // The popup is weirdly slow to open, so slow the tests down + step_time = 1200; + + normal_window = GTK_WINDOW(gtk_window_new()); + normal_dropdown = gtk_drop_down_new_from_strings(options); + gtk_window_set_child(normal_window, normal_dropdown); + gtk_window_present(normal_window); +} + +static void callback_1() { + EXPECT_MESSAGE(xdg_wm_base .get_xdg_surface); + EXPECT_MESSAGE(xdg_surface .get_popup xdg_surface); + EXPECT_MESSAGE(xdg_popup .grab); + + DONT_EXPECT_MESSAGE(zwlr_layer_surface_v1 .get_popup); + DONT_EXPECT_MESSAGE(xdg_popup .destroy); + + g_signal_emit_by_name(normal_dropdown, "activate", NULL); +} + +static void callback_2() { + EXPECT_MESSAGE(xdg_popup .destroy); + EXPECT_MESSAGE(xdg_surface .destroy); + EXPECT_MESSAGE(xdg_toplevel .destroy); + EXPECT_MESSAGE(xdg_surface .destroy); + + gtk_window_close(normal_window); +} + +TEST_CALLBACKS( + callback_0, + callback_1, + callback_2, +) diff --git a/test/mock-server/overrides.c b/test/mock-server/overrides.c index c535491..ec4aa10 100644 --- a/test/mock-server/overrides.c +++ b/test/mock-server/overrides.c @@ -54,7 +54,6 @@ static void surface_data_unmap(SurfaceData* data) { SurfaceData* popup = data->most_recent_popup; while (popup) { // Popups must be unmapped before their parents - ASSERT(!popup->surface); ASSERT(!popup->layer_surface); ASSERT(!popup->xdg_popup); ASSERT(!popup->xdg_toplevel); @@ -241,7 +240,9 @@ static void xdg_surface_get_popup(struct wl_resource *resource, const struct wl_ wl_resource_get_version(resource), id); use_default_impl(popup); - xdg_popup_send_configure(popup, 0, 0, 100, 100); + // If the configure size is too small GTK gets upset and unmaps its popup in protest + // https://gitlab.gnome.org/GNOME/gtk/-/blob/4.16.12/gtk/gtkpopover.c?ref_type=tags#L719 + xdg_popup_send_configure(popup, 0, 0, 500, 500); xdg_surface_send_configure(resource, wl_display_next_serial(display)); SurfaceData* data = wl_resource_get_user_data(resource); surface_data_set_role(data, SURFACE_ROLE_XDG_POPUP); From 061ac5c8a2e639ca08329d40b7212c81a66515f6 Mon Sep 17 00:00:00 2001 From: Sophie Winter Date: Sun, 26 Jan 2025 02:17:43 -0800 Subject: [PATCH 07/15] Fix lock-test-lock-can-be-created-without-locking --- test/lock-tests/test-lock-can-be-created-without-locking.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lock-tests/test-lock-can-be-created-without-locking.c b/test/lock-tests/test-lock-can-be-created-without-locking.c index 1b1123c..5f8adf5 100644 --- a/test/lock-tests/test-lock-can-be-created-without-locking.c +++ b/test/lock-tests/test-lock-can-be-created-without-locking.c @@ -1,7 +1,7 @@ #include "integration-test-common.h" static void callback_0() { - EXPECT_MESSAGE(ext_session_lock_v1 .lock); + DONT_EXPECT_MESSAGE(ext_session_lock_v1 .lock); GtkSessionLockInstance* lock = gtk_session_lock_instance_new(); g_object_unref(lock); From 5a30d9cc503511360220b564eee78ee0857a50dc Mon Sep 17 00:00:00 2001 From: Sophie Winter Date: Sun, 26 Jan 2025 02:18:06 -0800 Subject: [PATCH 08/15] Update readme with more details on running tests --- README.md | 5 ++++- test/README.md | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 46995d5..1219892 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,10 @@ pacman -S --needed meson ninja gtk4 wayland gobject-introspection libgirepositor ### Running the Tests * `ninja -C build test` -* Or, to run a specific test and print the complete output `meson test --verbose -C build` +* Or, to run a specific test and print the complete output `meson test -C build --verbose ` +* To watch a specific test run against the currently active Wayland compositor `ninja -C build && ./build/test/ --auto` +* To run the test in interactive mode it's same as above, but without the `--auto` flag +* If you have [wayland-debug](https://github.com/wmww/wayland-debug), `wayland-debug -f 'zwlr_*, xdg_*' -r ./build/test/ --auto` can be helpful for debugging ## Licensing 100% MIT (unlike the GTK3 version of this library which contained GPL code copied from GTK) diff --git a/test/README.md b/test/README.md index cc5d102..53e0063 100644 --- a/test/README.md +++ b/test/README.md @@ -18,7 +18,7 @@ This directory is home to the gtk4-layer-shell test suite. Most of the potential bugs in GTK Layer Shell arise from interactions between the library, GTK and the Wayland compositor, so unit tests aren't particularly useful. Instead, most of our tests are integration tests. ### Integration test app -Each integration test is a single unique GTK app that uses GTK Layer Shell. All test clients are located in `integration-tests`. Anything common to multiple tests gets pulled into `integration-test-common` or `test-common`. Tests consist of a sequence of callbacks. At the start of each callback the app can state that specific Wayland messages should be sent during or after the callback is run (see expectations format below). Each meson test runs a single integration test. +Each integration test is a single unique GTK app that uses GTK Layer Shell. All test clients are located in an integration test subdirectory (eg `layer-tests/`). Anything common to multiple tests gets pulled into `integration-test-common` or `test-common`. Tests consist of a sequence of callbacks. At the start of each callback the app can state that specific Wayland messages should be sent during or after the callback is run (see expectations format below). Each meson test runs a single integration test. Integration tests can be run directly on a normal Wayland compositor (this may be useful for debugging). When run without arguments, they open an additional layer shell window with a `Continue ->` button to manually advance the test. Pass `--auto` to run each test callback with a timeout the way they are run when automated. From ad58e73c18aa0aac612d371ef0b3e8ca3691f558 Mon Sep 17 00:00:00 2001 From: Sophie Winter Date: Sun, 26 Jan 2025 02:24:33 -0800 Subject: [PATCH 09/15] tests: change DONT_EXPECT_MESSAGE -> UNEXPECT_MESSAGE --- test/README.md | 4 +++- test/integration-test-common/integration-test-common.h | 2 +- test/layer-tests/test-layer-surface-not-created.c | 2 +- test/layer-tests/test-layer-surface-popup.c | 2 +- .../test-xdg-toplevel-popup-with-layer-surface.c | 4 ++-- .../test-xdg-toplevel-popup-without-layer-surface.c | 6 +++--- test/lock-tests/test-lock-can-be-created-without-locking.c | 2 +- test/run-integration-test.py | 2 +- 8 files changed, 13 insertions(+), 11 deletions(-) diff --git a/test/README.md b/test/README.md index 53e0063..93fc0b1 100644 --- a/test/README.md +++ b/test/README.md @@ -25,7 +25,9 @@ Integration tests can be run directly on a normal Wayland compositor (this may b ### Expectations format Integration tests emit protocol expectations by using the `EXPECT_MESSAGE` macro. Each expectation is a white-space-separated sequence of tokens written to a line of stdout. The first element must be `EXPECT:` (this is automatically inserted by `EXPECT_MESSAGE`). For an expectation to match a message, each following token must appear in order in the message line. The list of expected messages must match in the correct order. Messages are matched against the output of the app run with `WAYLAND_DEBUG=1`. Events and requests are not distinguished. -When the script encounters `CHECK EXPECTATIONS COMPLETED` (emitted by the `CHECK_EXPECTATIONS()` macro), it will assert that all previous expectations have been met. This is emitted automatically at the start of each test callback. +Tests can also use the `UNEXPECT_MESSAGE()` macro to emit `UNEXPECT:` lines. They're the same, except if a matching message is encountered the test fails. + +When the script encounters `CHECK EXPECTATIONS COMPLETED` (emitted by the `CHECK_EXPECTATIONS()` macro), it will assert that all previous expectations have been met. This is emitted automatically at the start of each test callback, and implicitly exists at the end of the test. ### Test runner `ninja -C build test` will run `run-integration-test.py` for each test defined in `test/meson.build`. This script: diff --git a/test/integration-test-common/integration-test-common.h b/test/integration-test-common/integration-test-common.h index e96d908..ecd87ce 100644 --- a/test/integration-test-common/integration-test-common.h +++ b/test/integration-test-common/integration-test-common.h @@ -15,7 +15,7 @@ extern int step_time; // Tell the test script that a request containing the given space-separated components is expected #define EXPECT_MESSAGE(message) fprintf(stderr, "EXPECT: %s\n", #message) // Tell the test script this request is not expected -#define DONT_EXPECT_MESSAGE(message) fprintf(stderr, "DONT_EXPECT: %s\n", #message) +#define UNEXPECT_MESSAGE(message) fprintf(stderr, "UNEXPECT: %s\n", #message) // Tell the test script that all expected messages should now be fulfilled // (called automatically before each callback and at the end of the test) #define CHECK_EXPECTATIONS() fprintf(stderr, "CHECK EXPECTATIONS COMPLETED\n") diff --git a/test/layer-tests/test-layer-surface-not-created.c b/test/layer-tests/test-layer-surface-not-created.c index 4bf494a..69d5528 100644 --- a/test/layer-tests/test-layer-surface-not-created.c +++ b/test/layer-tests/test-layer-surface-not-created.c @@ -3,7 +3,7 @@ static GtkWindow *window; static void callback_0() { - DONT_EXPECT_MESSAGE(.get_layer_surface zwlr_layer_shell_v1); + UNEXPECT_MESSAGE(.get_layer_surface zwlr_layer_shell_v1); window = create_default_window(); gtk_layer_init_for_window(window); diff --git a/test/layer-tests/test-layer-surface-popup.c b/test/layer-tests/test-layer-surface-popup.c index 9c89957..7bc472b 100644 --- a/test/layer-tests/test-layer-surface-popup.c +++ b/test/layer-tests/test-layer-surface-popup.c @@ -23,7 +23,7 @@ static void callback_1() { EXPECT_MESSAGE(zwlr_layer_surface_v1 .get_popup); EXPECT_MESSAGE(xdg_popup .grab); - DONT_EXPECT_MESSAGE(xdg_popup .destroy); + UNEXPECT_MESSAGE(xdg_popup .destroy); g_signal_emit_by_name(layer_dropdown, "activate", NULL); } diff --git a/test/layer-tests/test-xdg-toplevel-popup-with-layer-surface.c b/test/layer-tests/test-xdg-toplevel-popup-with-layer-surface.c index 89f64fb..9746b66 100644 --- a/test/layer-tests/test-xdg-toplevel-popup-with-layer-surface.c +++ b/test/layer-tests/test-xdg-toplevel-popup-with-layer-surface.c @@ -28,8 +28,8 @@ static void callback_1() { EXPECT_MESSAGE(xdg_surface .get_popup xdg_surface); EXPECT_MESSAGE(xdg_popup .grab); - DONT_EXPECT_MESSAGE(zwlr_layer_surface_v1 .get_popup); - DONT_EXPECT_MESSAGE(xdg_popup .destroy); + UNEXPECT_MESSAGE(zwlr_layer_surface_v1 .get_popup); + UNEXPECT_MESSAGE(xdg_popup .destroy); g_signal_emit_by_name(normal_dropdown, "activate", NULL); } diff --git a/test/layer-tests/test-xdg-toplevel-popup-without-layer-surface.c b/test/layer-tests/test-xdg-toplevel-popup-without-layer-surface.c index 2179e98..a8df360 100644 --- a/test/layer-tests/test-xdg-toplevel-popup-without-layer-surface.c +++ b/test/layer-tests/test-xdg-toplevel-popup-without-layer-surface.c @@ -8,7 +8,7 @@ static void callback_0() { EXPECT_MESSAGE(xdg_wm_base .get_xdg_surface); EXPECT_MESSAGE(xdg_surface .get_toplevel); - DONT_EXPECT_MESSAGE(zwlr_layer_shell_v1 .get_layer_surface); + UNEXPECT_MESSAGE(zwlr_layer_shell_v1 .get_layer_surface); // The popup is weirdly slow to open, so slow the tests down step_time = 1200; @@ -24,8 +24,8 @@ static void callback_1() { EXPECT_MESSAGE(xdg_surface .get_popup xdg_surface); EXPECT_MESSAGE(xdg_popup .grab); - DONT_EXPECT_MESSAGE(zwlr_layer_surface_v1 .get_popup); - DONT_EXPECT_MESSAGE(xdg_popup .destroy); + UNEXPECT_MESSAGE(zwlr_layer_surface_v1 .get_popup); + UNEXPECT_MESSAGE(xdg_popup .destroy); g_signal_emit_by_name(normal_dropdown, "activate", NULL); } diff --git a/test/lock-tests/test-lock-can-be-created-without-locking.c b/test/lock-tests/test-lock-can-be-created-without-locking.c index 5f8adf5..1cd801d 100644 --- a/test/lock-tests/test-lock-can-be-created-without-locking.c +++ b/test/lock-tests/test-lock-can-be-created-without-locking.c @@ -1,7 +1,7 @@ #include "integration-test-common.h" static void callback_0() { - DONT_EXPECT_MESSAGE(ext_session_lock_v1 .lock); + UNEXPECT_MESSAGE(ext_session_lock_v1 .lock); GtkSessionLockInstance* lock = gtk_session_lock_instance_new(); g_object_unref(lock); diff --git a/test/run-integration-test.py b/test/run-integration-test.py index b5ea405..5808285 100755 --- a/test/run-integration-test.py +++ b/test/run-integration-test.py @@ -222,7 +222,7 @@ def verify_result(lines: List[str]): if line.startswith('EXPECT: '): assertions.append(line.split()[1:]) set_expectation = True - elif line.startswith('DONT_EXPECT: '): + elif line.startswith('UNEXPECT: '): negative_assertions.append(line.split()[1:]) set_expectation = True elif line.startswith('[') and line.endswith(')') and ('@' in line or '#' in line): From 517484abe51ca59405af5ba3b0e1e12d271a6595 Mon Sep 17 00:00:00 2001 From: Sophie Winter Date: Sun, 26 Jan 2025 06:10:50 -0800 Subject: [PATCH 10/15] Add basic session lock support to mock server --- test/mock-server/overrides.c | 122 ++++++++++++++++++++++++++++++----- 1 file changed, 106 insertions(+), 16 deletions(-) diff --git a/test/mock-server/overrides.c b/test/mock-server/overrides.c index ec4aa10..a1446cd 100644 --- a/test/mock-server/overrides.c +++ b/test/mock-server/overrides.c @@ -6,6 +6,7 @@ typedef enum { SURFACE_ROLE_XDG_TOPLEVEL, SURFACE_ROLE_XDG_POPUP, SURFACE_ROLE_LAYER, + SURFACE_ROLE_SESSION_LOCK, } SurfaceRole; typedef struct SurfaceData SurfaceData; @@ -20,12 +21,15 @@ struct SurfaceData { struct wl_resource* xdg_popup; struct wl_resource* xdg_surface; struct wl_resource* layer_surface; + struct wl_resource* lock_surface; char has_committed_buffer; // This surface has a non-null committed buffer char initial_commit_for_role; // Set to 1 when a role is created for a surface, and cleared after the first commit char layer_send_configure; // If to send a layer surface configure on the next commit int layer_set_w; // The width to configure the layer surface with int layer_set_h; // The height to configure the layer surface with uint32_t layer_anchor; // The layer surface's anchor + uint32_t lock_surface_pending_serial; + char lock_surface_initial_configure_acked; SurfaceData* most_recent_popup; // Start of the popup linked list SurfaceData* previous_popup_sibling; // Forms a linked list of popups SurfaceData* popup_parent; @@ -34,17 +38,33 @@ struct SurfaceData { static struct wl_resource* seat_global = NULL; static struct wl_resource* pointer_global = NULL; static struct wl_resource* output_global = NULL; +static struct wl_resource* current_session_lock = NULL; + +static void surface_data_assert_no_role(SurfaceData* data) { + ASSERT(!data->xdg_popup); + ASSERT(!data->xdg_toplevel); + ASSERT(!data->xdg_surface); + ASSERT(!data->layer_surface); + ASSERT(!data->lock_surface); +} // Needs to be called before any role objects are assigned static void surface_data_set_role(SurfaceData* data, SurfaceRole role) { if (data->role != SURFACE_ROLE_NONE) { ASSERT_EQ(data->role, role, "%u"); } - char is_xdg_role = (role == SURFACE_ROLE_XDG_TOPLEVEL || role == SURFACE_ROLE_XDG_POPUP); - ASSERT_EQ(data->xdg_surface != NULL, is_xdg_role, "%d"); - ASSERT(!data->xdg_toplevel); - ASSERT(!data->xdg_popup); - ASSERT(!data->layer_surface); + + if (role == SURFACE_ROLE_XDG_TOPLEVEL || role == SURFACE_ROLE_XDG_POPUP) { + ASSERT(data->xdg_surface != NULL); + } else { + ASSERT(data->xdg_surface == NULL); + } + + struct wl_resource* xdg_surface = data->xdg_surface; + data->xdg_surface = NULL; // XDG surfaces are allowed, so hide it from surface_data_assert_no_role() + surface_data_assert_no_role(data); + data->xdg_surface = xdg_surface; + ASSERT(!data->has_committed_buffer); data->role = role; data->initial_commit_for_role = 1; @@ -54,10 +74,7 @@ static void surface_data_unmap(SurfaceData* data) { SurfaceData* popup = data->most_recent_popup; while (popup) { // Popups must be unmapped before their parents - ASSERT(!popup->layer_surface); - ASSERT(!popup->xdg_popup); - ASSERT(!popup->xdg_toplevel); - ASSERT(!popup->xdg_surface); + surface_data_assert_no_role(data); popup = popup->previous_popup_sibling; } } @@ -89,11 +106,21 @@ static void wl_surface_attach(struct wl_resource *resource, const struct wl_mess static void wl_surface_commit(struct wl_resource *resource, const struct wl_message* message, union wl_argument* args) { SurfaceData* data = wl_resource_get_user_data(resource); + + if (data->role == SURFACE_ROLE_SESSION_LOCK) { + if (data->buffer_cleared) { + FATAL("null buffer committed to session lock surface"); + } else if (!data->pending_buffer && !data->has_committed_buffer) { + FATAL("no buffer has been attached to committed session lock surface"); + } else if (!data->lock_surface_initial_configure_acked) { + FATAL("session lock surface committed before initial configure acked"); + } + } + if (data->buffer_cleared) { data->has_committed_buffer = 0; data->buffer_cleared = 0; - } - else if (data->pending_buffer) { + } else if (data->pending_buffer) { data->has_committed_buffer = 1; } @@ -108,7 +135,7 @@ static void wl_surface_commit(struct wl_resource *resource, const struct wl_mess data->pending_frame = NULL; } - if (data->initial_commit_for_role) { + if (data->initial_commit_for_role && data->role != SURFACE_ROLE_SESSION_LOCK) { ASSERT(!data->has_committed_buffer); data->initial_commit_for_role = 0; } @@ -137,10 +164,7 @@ static void wl_surface_commit(struct wl_resource *resource, const struct wl_mess static void wl_surface_destroy(struct wl_resource* resource, const struct wl_message* message, union wl_argument* args) { SurfaceData* data = wl_resource_get_user_data(resource); - ASSERT(!data->xdg_popup); - ASSERT(!data->xdg_toplevel); - ASSERT(!data->xdg_surface); - ASSERT(!data->layer_surface); + surface_data_assert_no_role(data); data->surface = NULL; // Don't free surfaces to guarantee traversing popups is always safe // We're employing the missile memory management pattern here https://x.com/pomeranian99/status/858856994438094848 @@ -314,6 +338,66 @@ static void zwlr_layer_surface_v1_destroy(struct wl_resource *resource, const st surface_data_unmap(data); } +static void ext_session_lock_manager_v1_lock(struct wl_resource *resource, const struct wl_message* message, union wl_argument* args) { + NEW_ID_ARG(id, 0); + struct wl_resource* lock_resource = wl_resource_create( + wl_resource_get_client(resource), + &ext_session_lock_v1_interface, + wl_resource_get_version(resource), + id); + use_default_impl(lock_resource); + if (current_session_lock) { + ext_session_lock_v1_send_finished(lock_resource); + } else { + current_session_lock = lock_resource; + ext_session_lock_v1_send_locked(lock_resource); + } +} + +static void ext_session_lock_v1_destroy(struct wl_resource *resource, const struct wl_message* message, union wl_argument* args) { + if (resource == current_session_lock) { + FATAL(".destroy (instead of .unlock_and_destroy) called on active lock"); + } +} + +static void ext_session_lock_v1_unlock_and_destroy(struct wl_resource *resource, const struct wl_message* message, union wl_argument* args) { + if (resource != current_session_lock) { + FATAL(".unlock_and_destroy (instead of .destroy) called on inactive lock"); + } + current_session_lock = NULL; +} + +static void lock_surface_send_configure(SurfaceData* data, uint32_t width, uint32_t height) { + data->lock_surface_pending_serial = wl_display_next_serial(display); + ext_session_lock_surface_v1_send_configure(data->lock_surface, data->lock_surface_pending_serial, width, height); +} + +static void ext_session_lock_v1_get_lock_surface(struct wl_resource *resource, const struct wl_message* message, union wl_argument* args) { + NEW_ID_ARG(id, 0); + struct wl_resource* lock_surface = wl_resource_create( + wl_resource_get_client(resource), + &ext_session_lock_surface_v1_interface, + wl_resource_get_version(resource), + id); + use_default_impl(lock_surface); + RESOURCE_ARG(wl_surface, surface, 1); + RESOURCE_ARG(wl_output, output, 2); + ASSERT_EQ(output, output_global, "%p"); + SurfaceData* data = wl_resource_get_user_data(surface); + surface_data_set_role(data, SURFACE_ROLE_SESSION_LOCK); + wl_resource_set_user_data(lock_surface, data); + data->lock_surface = lock_surface; + lock_surface_send_configure(data, DEFAULT_OUTPUT_WIDTH, DEFAULT_OUTPUT_HEIGHT); +} + +static void ext_session_lock_surface_v1_ack_configure(struct wl_resource *resource, const struct wl_message* message, union wl_argument* args) { + UINT_ARG(serial, 0); + SurfaceData* data = wl_resource_get_user_data(resource); + if (serial == data->lock_surface_pending_serial) { + data->lock_surface_initial_configure_acked = 1; + } +} + void init() { OVERRIDE_REQUEST(wl_surface, commit); OVERRIDE_REQUEST(wl_surface, frame); @@ -333,6 +417,11 @@ void init() { OVERRIDE_REQUEST(zwlr_layer_surface_v1, set_size); OVERRIDE_REQUEST(zwlr_layer_surface_v1, get_popup); OVERRIDE_REQUEST(zwlr_layer_surface_v1, destroy); + OVERRIDE_REQUEST(ext_session_lock_manager_v1, lock); + OVERRIDE_REQUEST(ext_session_lock_v1, destroy); + OVERRIDE_REQUEST(ext_session_lock_v1, unlock_and_destroy); + OVERRIDE_REQUEST(ext_session_lock_v1, get_lock_surface); + OVERRIDE_REQUEST(ext_session_lock_surface_v1, ack_configure); wl_global_create(display, &wl_seat_interface, 6, NULL, wl_seat_bind); wl_global_create(display, &wl_output_interface, 2, NULL, wl_output_bind); @@ -342,5 +431,6 @@ void init() { default_global_create(display, &wl_subcompositor_interface, 1); default_global_create(display, &xdg_wm_base_interface, 2); default_global_create(display, &zwlr_layer_shell_v1_interface, 4); + default_global_create(display, &ext_session_lock_manager_v1_interface, 1); default_global_create(display, &xdg_wm_dialog_v1_interface, 1); } From ed1986cc0aee2b8947eac359c845bd26a4e5008f Mon Sep 17 00:00:00 2001 From: Sophie Winter Date: Sun, 26 Jan 2025 06:12:38 -0800 Subject: [PATCH 11/15] Add basic session lock tests --- .../integration-test-common.c | 62 +++++++++++++++---- .../integration-test-common.h | 9 +++ test/lock-tests/meson.build | 2 + test/lock-tests/test-can-lock-display.c | 33 ++++++++++ test/lock-tests/test-immediate-failure.c | 41 ++++++++++++ ...test-lock-can-be-created-without-locking.c | 14 ++++- 6 files changed, 148 insertions(+), 13 deletions(-) create mode 100644 test/lock-tests/test-can-lock-display.c create mode 100644 test/lock-tests/test-immediate-failure.c diff --git a/test/integration-test-common/integration-test-common.c b/test/integration-test-common/integration-test-common.c index c1988f0..676de2b 100644 --- a/test/integration-test-common/integration-test-common.c +++ b/test/integration-test-common/integration-test-common.c @@ -6,8 +6,7 @@ static int return_code = 0; static int callback_index = 0; static gboolean auto_continue = FALSE; -static gboolean next_step(gpointer _data) -{ +static gboolean next_step(gpointer _data) { (void)_data; CHECK_EXPECTATIONS(); @@ -23,27 +22,69 @@ static gboolean next_step(gpointer _data) return FALSE; } -GtkWindow* create_default_window() -{ +GtkWindow* create_default_window() { GtkWindow* window = GTK_WINDOW(gtk_window_new()); GtkWidget *label = gtk_label_new(""); gtk_label_set_markup( GTK_LABEL(label), "" - "Layer shell test" + "Test window" ""); gtk_window_set_child(window, label); return window; } -static void continue_button_callback(GtkWidget *_widget, gpointer _data) -{ +struct lock_signal_data_t { + +}; + +static void on_locked(GtkSessionLockInstance *lock, void *data) { + (void)lock; + enum lock_state_t* state = data; + ASSERT_EQ(*state, LOCK_STATE_NOT_YET_LOCKED, "%d"); + *state = LOCK_STATE_LOCKED; +} + +static void on_failed(GtkSessionLockInstance *lock, void *data) { + (void)lock; + enum lock_state_t* state = data; + ASSERT_EQ(*state, LOCK_STATE_NOT_YET_LOCKED, "%d"); + *state = LOCK_STATE_FAILED; +} + +static void on_unlocked(GtkSessionLockInstance *lock, void *data) { + (void)lock; + enum lock_state_t* state = data; + ASSERT_EQ(*state, LOCK_STATE_LOCKED, "%d"); + *state = LOCK_STATE_UNLOCKED; +} + +void connect_lock_signals(GtkSessionLockInstance* lock, enum lock_state_t* state) { + g_signal_connect(lock, "locked", G_CALLBACK(on_locked), state); + g_signal_connect(lock, "failed", G_CALLBACK(on_failed), state); + g_signal_connect(lock, "unlocked", G_CALLBACK(on_unlocked), state); +} + +void create_lock_windows(GtkSessionLockInstance* lock) { + GdkDisplay *display = gdk_display_get_default(); + GListModel *monitors = gdk_display_get_monitors(display); + guint n_monitors = g_list_model_get_n_items(monitors); + + for (guint i = 0; i < n_monitors; ++i) { + GdkMonitor *monitor = g_list_model_get_item(monitors, i); + + GtkWindow* window = create_default_window(); + gtk_session_lock_instance_assign_window_to_monitor(lock, window, monitor); + gtk_window_present(window); + } +} + +static void continue_button_callback(GtkWidget* _widget, gpointer _data) { (void)_widget; (void)_data; next_step(NULL); } -static void create_debug_control_window() -{ +static void create_debug_control_window() { // Make a window with a continue button for debugging GtkWindow *window = GTK_WINDOW(gtk_window_new()); gtk_layer_init_for_window(window); @@ -57,8 +98,7 @@ static void create_debug_control_window() // This will only be called once, so leaking the window is fine } -int main(int argc, char** argv) -{ +int main(int argc, char** argv) { EXPECT_MESSAGE(wl_display .get_registry); gtk_init(); diff --git a/test/integration-test-common/integration-test-common.h b/test/integration-test-common/integration-test-common.h index ecd87ce..d0dd832 100644 --- a/test/integration-test-common/integration-test-common.h +++ b/test/integration-test-common/integration-test-common.h @@ -29,4 +29,13 @@ extern void (* test_callbacks[])(void); GtkWindow* create_default_window(); +enum lock_state_t { + LOCK_STATE_NOT_YET_LOCKED = 0, + LOCK_STATE_LOCKED, + LOCK_STATE_FAILED, + LOCK_STATE_UNLOCKED, +}; +void connect_lock_signals(GtkSessionLockInstance* lock, enum lock_state_t* state); +void create_lock_windows(GtkSessionLockInstance* lock); + #endif // TEST_CLIENT_COMMON_H diff --git a/test/lock-tests/meson.build b/test/lock-tests/meson.build index 1796630..31c1adc 100644 --- a/test/lock-tests/meson.build +++ b/test/lock-tests/meson.build @@ -1,3 +1,5 @@ lock_tests = [ 'test-lock-can-be-created-without-locking', + 'test-can-lock-display', + 'test-immediate-failure', ] diff --git a/test/lock-tests/test-can-lock-display.c b/test/lock-tests/test-can-lock-display.c new file mode 100644 index 0000000..18065de --- /dev/null +++ b/test/lock-tests/test-can-lock-display.c @@ -0,0 +1,33 @@ +#include "integration-test-common.h" + +enum lock_state_t state = 0; +GtkSessionLockInstance* lock; + +static void callback_0() { + EXPECT_MESSAGE(ext_session_lock_manager_v1 .lock); + EXPECT_MESSAGE(ext_session_lock_v1 .get_lock_surface); + EXPECT_MESSAGE(ext_session_lock_v1 .locked); + + lock = gtk_session_lock_instance_new(); + connect_lock_signals(lock, &state); + + ASSERT(gtk_session_lock_instance_lock(lock)); + create_lock_windows(lock); +} + +static void callback_1() { + ASSERT_EQ(state, LOCK_STATE_LOCKED, "%d"); + EXPECT_MESSAGE(ext_session_lock_v1 .unlock_and_destroy); + + gtk_session_lock_instance_unlock(lock); +} + +static void callback_2() { + ASSERT_EQ(state, LOCK_STATE_UNLOCKED, "%d"); +} + +TEST_CALLBACKS( + callback_0, + callback_1, + callback_2, +) diff --git a/test/lock-tests/test-immediate-failure.c b/test/lock-tests/test-immediate-failure.c new file mode 100644 index 0000000..ae67d25 --- /dev/null +++ b/test/lock-tests/test-immediate-failure.c @@ -0,0 +1,41 @@ +#include "integration-test-common.h" + +enum lock_state_t state_a = 0; +GtkSessionLockInstance* lock_a; + +enum lock_state_t state_b = 0; +GtkSessionLockInstance* lock_b; + +static void callback_0() { + EXPECT_MESSAGE(ext_session_lock_manager_v1 .lock); + EXPECT_MESSAGE(ext_session_lock_v1 .get_lock_surface); + EXPECT_MESSAGE(ext_session_lock_v1 .locked); + + lock_a = gtk_session_lock_instance_new(); + connect_lock_signals(lock_a, &state_a); + + lock_b = gtk_session_lock_instance_new(); + connect_lock_signals(lock_b, &state_b); + + ASSERT(gtk_session_lock_instance_lock(lock_b)); + create_lock_windows(lock_b); +} + +static void callback_1() { + ASSERT_EQ(state_a, LOCK_STATE_NOT_YET_LOCKED, "%d"); + ASSERT_EQ(state_b, LOCK_STATE_LOCKED, "%d"); + UNEXPECT_MESSAGE(ext_session_lock_manager_v1 .lock); + UNEXPECT_MESSAGE(ext_session_lock_v1 .get_lock_surface); + UNEXPECT_MESSAGE(ext_session_lock_v1 .unlock_and_destroy); + UNEXPECT_MESSAGE(ext_session_lock_v1 .destroy); + UNEXPECT_MESSAGE(ext_session_lock_v1 .locked); + UNEXPECT_MESSAGE(ext_session_lock_v1 .finished); + + ASSERT(!gtk_session_lock_instance_lock(lock_a)); + ASSERT_EQ(state_a, LOCK_STATE_FAILED, "%d"); +} + +TEST_CALLBACKS( + callback_0, + callback_1, +) diff --git a/test/lock-tests/test-lock-can-be-created-without-locking.c b/test/lock-tests/test-lock-can-be-created-without-locking.c index 1cd801d..4b18004 100644 --- a/test/lock-tests/test-lock-can-be-created-without-locking.c +++ b/test/lock-tests/test-lock-can-be-created-without-locking.c @@ -1,10 +1,20 @@ #include "integration-test-common.h" +enum lock_state_t state = 0; +GtkSessionLockInstance* lock; + static void callback_0() { - UNEXPECT_MESSAGE(ext_session_lock_v1 .lock); + UNEXPECT_MESSAGE(ext_session_lock_manager_v1 .lock); + UNEXPECT_MESSAGE(ext_session_lock_v1 .get_lock_surface); + UNEXPECT_MESSAGE(ext_session_lock_v1 .locked); + + ASSERT(gtk_session_lock_is_supported()); - GtkSessionLockInstance* lock = gtk_session_lock_instance_new(); + lock = gtk_session_lock_instance_new(); + connect_lock_signals(lock, &state); g_object_unref(lock); + + ASSERT_EQ(state, LOCK_STATE_NOT_YET_LOCKED, "%d"); } TEST_CALLBACKS( From 2154aa467352c9ec65df19aab0bd5faaac8618a1 Mon Sep 17 00:00:00 2001 From: Sophie Winter Date: Sun, 26 Jan 2025 06:13:11 -0800 Subject: [PATCH 12/15] Add session lock examples to smoke tests --- test/smoke-tests/meson.build | 2 ++ test/smoke-tests/smoke_test_common.py | 14 ++++++++++---- test/smoke-tests/test-c-demo.py | 2 +- test/smoke-tests/test-c-example.py | 2 +- test/smoke-tests/test-c-session-lock.py | 6 ++++++ test/smoke-tests/test-python-example.py | 2 +- test/smoke-tests/test-python-session-lock.py | 13 +++++++++++++ test/smoke-tests/test-vala-example.py | 2 +- 8 files changed, 35 insertions(+), 8 deletions(-) create mode 100755 test/smoke-tests/test-c-session-lock.py create mode 100755 test/smoke-tests/test-python-session-lock.py diff --git a/test/smoke-tests/meson.build b/test/smoke-tests/meson.build index f2dab96..913088a 100644 --- a/test/smoke-tests/meson.build +++ b/test/smoke-tests/meson.build @@ -1,6 +1,8 @@ smoke_tests = [ 'test-c-example', 'test-c-demo', + 'test-c-session-lock', 'test-python-example', + 'test-python-session-lock', 'test-vala-example', ] diff --git a/test/smoke-tests/smoke_test_common.py b/test/smoke-tests/smoke_test_common.py index 25373cc..a29069a 100644 --- a/test/smoke-tests/smoke_test_common.py +++ b/test/smoke-tests/smoke_test_common.py @@ -14,12 +14,18 @@ def build_dir() -> str: def expect(*args) -> None: print('EXPECT: ' + ' '.join(args), file=sys.stderr) -def run(cmd: List[str], env: Dict[str, str]) -> None: - expect('zwlr_layer_shell_v1', '.get_layer_surface') - expect('wl_surface', '.commit') +def run(cmd: List[str], mode: str, env: Dict[str, str]) -> None: + if mode == 'layer': + expect('zwlr_layer_shell_v1', '.get_layer_surface') + expect('wl_surface', '.commit') + elif mode == 'lock': + expect('ext_session_lock_manager_v1', '.lock'); + expect('ext_session_lock_v1', '.get_lock_surface'); + expect('ext_session_lock_v1', '.locked'); + else: + assert False, 'Invalid mode ' + str(mode) try: result = subprocess.run(cmd, env={**os.environ, **env}, timeout=timeout) assert False, 'subprocess completed without timeout expiring, return code: ' + str(result.returncode) except subprocess.TimeoutExpired: pass - print('CHECK EXPECTATIONS COMPLETED', file=sys.stderr) diff --git a/test/smoke-tests/test-c-demo.py b/test/smoke-tests/test-c-demo.py index 505ddec..cf4defb 100755 --- a/test/smoke-tests/test-c-demo.py +++ b/test/smoke-tests/test-c-demo.py @@ -4,4 +4,4 @@ bin_path = smoke_test_common.build_dir() + '/examples/gtk4-layer-demo' args = [bin_path] + '-l top -a lrb -m 20,20,20,0 -e -k on-demand'.split() -smoke_test_common.run(args, {}) +smoke_test_common.run(args, 'layer', {}) diff --git a/test/smoke-tests/test-c-example.py b/test/smoke-tests/test-c-example.py index 5e23545..26c34f8 100755 --- a/test/smoke-tests/test-c-example.py +++ b/test/smoke-tests/test-c-example.py @@ -3,4 +3,4 @@ import smoke_test_common bin_path = smoke_test_common.build_dir() + '/examples/simple-example-c' -smoke_test_common.run([bin_path], {}) +smoke_test_common.run([bin_path], 'layer', {}) diff --git a/test/smoke-tests/test-c-session-lock.py b/test/smoke-tests/test-c-session-lock.py new file mode 100755 index 0000000..65d5a14 --- /dev/null +++ b/test/smoke-tests/test-c-session-lock.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python3 + +import smoke_test_common + +bin_path = smoke_test_common.build_dir() + '/examples/session-lock-c' +smoke_test_common.run([bin_path], 'lock', {}) diff --git a/test/smoke-tests/test-python-example.py b/test/smoke-tests/test-python-example.py index 995619f..5a10747 100755 --- a/test/smoke-tests/test-python-example.py +++ b/test/smoke-tests/test-python-example.py @@ -10,4 +10,4 @@ 'GI_TYPELIB_PATH': src_build_dir, 'LD_LIBRARY_PATH': src_build_dir, } -smoke_test_common.run(['python3', script_path], env) +smoke_test_common.run(['python3', script_path], 'layer', env) diff --git a/test/smoke-tests/test-python-session-lock.py b/test/smoke-tests/test-python-session-lock.py new file mode 100755 index 0000000..5914e4a --- /dev/null +++ b/test/smoke-tests/test-python-session-lock.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 + +import os +import smoke_test_common + +script_path = os.path.join(os.path.dirname(__file__), '..', '..', 'examples', 'session-lock.py') +assert os.path.isfile(script_path), 'script not found at ' + script_path +src_build_dir = smoke_test_common.build_dir() + '/src' +env = { + 'GI_TYPELIB_PATH': src_build_dir, + 'LD_LIBRARY_PATH': src_build_dir, +} +smoke_test_common.run(['python3', script_path], 'lock', env) diff --git a/test/smoke-tests/test-vala-example.py b/test/smoke-tests/test-vala-example.py index 019b956..484ca97 100755 --- a/test/smoke-tests/test-vala-example.py +++ b/test/smoke-tests/test-vala-example.py @@ -3,4 +3,4 @@ import smoke_test_common bin_path = smoke_test_common.build_dir() + '/examples/simple-example-vala' -smoke_test_common.run([bin_path], {}) +smoke_test_common.run([bin_path], 'layer', {}) From 010e70cdb95334669e76747190310adbd0ec6c9c Mon Sep 17 00:00:00 2001 From: Sophie Winter Date: Sun, 26 Jan 2025 07:03:43 -0800 Subject: [PATCH 13/15] Fix bug preventing the same object from locking multiple times --- src/gtk4-session-lock.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gtk4-session-lock.c b/src/gtk4-session-lock.c index d260bf7..a543e4c 100644 --- a/src/gtk4-session-lock.c +++ b/src/gtk4-session-lock.c @@ -111,6 +111,7 @@ gboolean gtk_session_lock_instance_lock(GtkSessionLockInstance* self) { void gtk_session_lock_instance_unlock(GtkSessionLockInstance* self) { if (self->is_locked) { g_signal_emit(self, session_lock_signals[SESSION_LOCK_SIGNAL_UNLOCKED], 0); + self->is_locked = FALSE; session_lock_unlock(); } } From 6e36f56ee7b6941a3933b39d6997404800bd63cb Mon Sep 17 00:00:00 2001 From: Sophie Winter Date: Sun, 26 Jan 2025 07:04:18 -0800 Subject: [PATCH 14/15] Add more session lock tests --- .../integration-test-common.c | 4 +- .../integration-test-common.h | 3 +- test/lock-tests/meson.build | 2 + test/lock-tests/test-async-failure.c | 40 ++++++++++ test/lock-tests/test-can-relock-display.c | 79 +++++++++++++++++++ test/lock-tests/test-immediate-failure.c | 2 +- ...test-lock-can-be-created-without-locking.c | 2 +- test/meson.build | 2 +- 8 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 test/lock-tests/test-async-failure.c create mode 100644 test/lock-tests/test-can-relock-display.c diff --git a/test/integration-test-common/integration-test-common.c b/test/integration-test-common/integration-test-common.c index 676de2b..091c071 100644 --- a/test/integration-test-common/integration-test-common.c +++ b/test/integration-test-common/integration-test-common.c @@ -41,14 +41,14 @@ struct lock_signal_data_t { static void on_locked(GtkSessionLockInstance *lock, void *data) { (void)lock; enum lock_state_t* state = data; - ASSERT_EQ(*state, LOCK_STATE_NOT_YET_LOCKED, "%d"); + ASSERT_EQ(*state, LOCK_STATE_UNLOCKED, "%d"); *state = LOCK_STATE_LOCKED; } static void on_failed(GtkSessionLockInstance *lock, void *data) { (void)lock; enum lock_state_t* state = data; - ASSERT_EQ(*state, LOCK_STATE_NOT_YET_LOCKED, "%d"); + ASSERT_EQ(*state, LOCK_STATE_UNLOCKED, "%d"); *state = LOCK_STATE_FAILED; } diff --git a/test/integration-test-common/integration-test-common.h b/test/integration-test-common/integration-test-common.h index d0dd832..80fbee1 100644 --- a/test/integration-test-common/integration-test-common.h +++ b/test/integration-test-common/integration-test-common.h @@ -30,10 +30,9 @@ extern void (* test_callbacks[])(void); GtkWindow* create_default_window(); enum lock_state_t { - LOCK_STATE_NOT_YET_LOCKED = 0, + LOCK_STATE_UNLOCKED = 0, LOCK_STATE_LOCKED, LOCK_STATE_FAILED, - LOCK_STATE_UNLOCKED, }; void connect_lock_signals(GtkSessionLockInstance* lock, enum lock_state_t* state); void create_lock_windows(GtkSessionLockInstance* lock); diff --git a/test/lock-tests/meson.build b/test/lock-tests/meson.build index 31c1adc..0e3e900 100644 --- a/test/lock-tests/meson.build +++ b/test/lock-tests/meson.build @@ -1,5 +1,7 @@ lock_tests = [ 'test-lock-can-be-created-without-locking', 'test-can-lock-display', + 'test-can-relock-display', 'test-immediate-failure', + 'test-async-failure', ] diff --git a/test/lock-tests/test-async-failure.c b/test/lock-tests/test-async-failure.c new file mode 100644 index 0000000..1530175 --- /dev/null +++ b/test/lock-tests/test-async-failure.c @@ -0,0 +1,40 @@ +#include "integration-test-common.h" + +// Not part of the public API but we're the tests, we can do what we like +#include "../../src/registry.h" +#include "ext-session-lock-v1-client.h" + +enum lock_state_t state = 0; +GtkSessionLockInstance* lock; + +static void callback_0() { + // We lock the display without going through the library in order to simulate a lock being held by another process + GdkDisplay* gdk_display = gdk_display_get_default(); + struct wl_display* wl_display = gdk_wayland_display_get_wl_display(gdk_display); + struct ext_session_lock_manager_v1* global = get_session_lock_global_from_display(wl_display); + struct ext_session_lock_v1* session_lock = ext_session_lock_manager_v1_lock(global); + (void)session_lock; +} + +static void callback_1() { + EXPECT_MESSAGE(ext_session_lock_manager_v1 .lock); + EXPECT_MESSAGE(ext_session_lock_v1 .get_lock_surface); + EXPECT_MESSAGE(ext_session_lock_v1 .finished); + + lock = gtk_session_lock_instance_new(); + connect_lock_signals(lock, &state); + + ASSERT(gtk_session_lock_instance_lock(lock)); + ASSERT_EQ(state, LOCK_STATE_UNLOCKED, "%d"); + create_lock_windows(lock); +} + +static void callback_2() { + ASSERT_EQ(state, LOCK_STATE_FAILED, "%d"); +} + +TEST_CALLBACKS( + callback_0, + callback_1, + callback_2, +) diff --git a/test/lock-tests/test-can-relock-display.c b/test/lock-tests/test-can-relock-display.c new file mode 100644 index 0000000..b6aa5d8 --- /dev/null +++ b/test/lock-tests/test-can-relock-display.c @@ -0,0 +1,79 @@ +#include "integration-test-common.h" + +enum lock_state_t state_a = 0; +GtkSessionLockInstance* lock_a; + +enum lock_state_t state_b = 0; +GtkSessionLockInstance* lock_b; + +static void callback_0() { + EXPECT_MESSAGE(ext_session_lock_manager_v1 .lock); + EXPECT_MESSAGE(ext_session_lock_v1 .get_lock_surface); + EXPECT_MESSAGE(ext_session_lock_v1 .locked); + + lock_a = gtk_session_lock_instance_new(); + connect_lock_signals(lock_a, &state_a); + ASSERT(!gtk_session_lock_instance_is_locked(lock_a)); + + ASSERT(gtk_session_lock_instance_lock(lock_a)); + create_lock_windows(lock_a); +} + +static void callback_1() { + ASSERT(gtk_session_lock_instance_is_locked(lock_a)); + ASSERT_EQ(state_a, LOCK_STATE_LOCKED, "%d"); + + EXPECT_MESSAGE(ext_session_lock_v1 .unlock_and_destroy); + + gtk_session_lock_instance_unlock(lock_a); + ASSERT(!gtk_session_lock_instance_is_locked(lock_a)); + ASSERT_EQ(state_a, LOCK_STATE_UNLOCKED, "%d"); + CHECK_EXPECTATIONS(); + + EXPECT_MESSAGE(ext_session_lock_manager_v1 .lock); + EXPECT_MESSAGE(ext_session_lock_v1 .get_lock_surface); + EXPECT_MESSAGE(ext_session_lock_v1 .locked); + + ASSERT(gtk_session_lock_instance_lock(lock_a)); + create_lock_windows(lock_a); +} + +static void callback_2() { + ASSERT(gtk_session_lock_instance_is_locked(lock_a)); + ASSERT_EQ(state_a, LOCK_STATE_LOCKED, "%d"); + + EXPECT_MESSAGE(ext_session_lock_v1 .unlock_and_destroy); + + gtk_session_lock_instance_unlock(lock_a); + ASSERT(!gtk_session_lock_instance_is_locked(lock_a)); + ASSERT_EQ(state_a, LOCK_STATE_UNLOCKED, "%d"); + CHECK_EXPECTATIONS(); + + EXPECT_MESSAGE(ext_session_lock_manager_v1 .lock); + EXPECT_MESSAGE(ext_session_lock_v1 .get_lock_surface); + EXPECT_MESSAGE(ext_session_lock_v1 .locked); + + lock_b = gtk_session_lock_instance_new(); + connect_lock_signals(lock_b, &state_b); + + ASSERT(gtk_session_lock_instance_lock(lock_b)); + create_lock_windows(lock_b); +} + +static void callback_3() { + ASSERT(gtk_session_lock_instance_is_locked(lock_b)); + ASSERT_EQ(state_b, LOCK_STATE_LOCKED, "%d"); + ASSERT(!gtk_session_lock_instance_is_locked(lock_a)); + ASSERT_EQ(state_a, LOCK_STATE_UNLOCKED, "%d"); + + EXPECT_MESSAGE(ext_session_lock_v1 .unlock_and_destroy); + + gtk_session_lock_instance_unlock(lock_b); +} + +TEST_CALLBACKS( + callback_0, + callback_1, + callback_2, + callback_3, +) diff --git a/test/lock-tests/test-immediate-failure.c b/test/lock-tests/test-immediate-failure.c index ae67d25..a3a4374 100644 --- a/test/lock-tests/test-immediate-failure.c +++ b/test/lock-tests/test-immediate-failure.c @@ -22,7 +22,7 @@ static void callback_0() { } static void callback_1() { - ASSERT_EQ(state_a, LOCK_STATE_NOT_YET_LOCKED, "%d"); + ASSERT_EQ(state_a, LOCK_STATE_UNLOCKED, "%d"); ASSERT_EQ(state_b, LOCK_STATE_LOCKED, "%d"); UNEXPECT_MESSAGE(ext_session_lock_manager_v1 .lock); UNEXPECT_MESSAGE(ext_session_lock_v1 .get_lock_surface); diff --git a/test/lock-tests/test-lock-can-be-created-without-locking.c b/test/lock-tests/test-lock-can-be-created-without-locking.c index 4b18004..a3b2bc5 100644 --- a/test/lock-tests/test-lock-can-be-created-without-locking.c +++ b/test/lock-tests/test-lock-can-be-created-without-locking.c @@ -14,7 +14,7 @@ static void callback_0() { connect_lock_signals(lock, &state); g_object_unref(lock); - ASSERT_EQ(state, LOCK_STATE_NOT_YET_LOCKED, "%d"); + ASSERT_EQ(state, LOCK_STATE_UNLOCKED, "%d"); } TEST_CALLBACKS( diff --git a/test/meson.build b/test/meson.build index af64f70..503e14d 100644 --- a/test/meson.build +++ b/test/meson.build @@ -18,7 +18,7 @@ foreach test_list : [['layer', layer_tests], ['lock', lock_tests]] test_full_name = test_prefix + '-' + test_name exe = executable( test_full_name, - files(join_paths(test_dir, test_name + '.c')), + files(join_paths(test_dir, test_name + '.c')), client_protocol_srcs, dependencies: [gtk, wayland_client, gtk_layer_shell, integration_test_common]) test( test_full_name, From bbb61ab61929d245d9b2a132de36559061998d96 Mon Sep 17 00:00:00 2001 From: Sophie Winter Date: Sun, 26 Jan 2025 07:15:59 -0800 Subject: [PATCH 15/15] Fix bugs around repeated locking logic --- include/gtk4-session-lock.h | 2 +- src/gtk4-session-lock.c | 13 ++++++++++--- test/lock-tests/test-immediate-failure.c | 1 + 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/include/gtk4-session-lock.h b/include/gtk4-session-lock.h index be6f437..3bac26b 100644 --- a/include/gtk4-session-lock.h +++ b/include/gtk4-session-lock.h @@ -71,7 +71,7 @@ GtkSessionLockInstance* gtk_session_lock_instance_new(); * Lock the screen. This should be called before assigning any windows to monitors. If this function fails the ::failed * signal is emitted, if it succeeds the ::locked signal is emitted. The ::failed signal may be emitted before the * function returns (for example, if another #GtkSessionLockInstance holds a lock) or later (if another process holds a - * lock) + * lock). The only case where neither signal is triggered is if the instance is already locked. * * Returns: false on immediate fail, true if lock acquisition was successfully started */ diff --git a/src/gtk4-session-lock.c b/src/gtk4-session-lock.c index a543e4c..d604c6f 100644 --- a/src/gtk4-session-lock.c +++ b/src/gtk4-session-lock.c @@ -21,6 +21,7 @@ gboolean gtk_session_lock_is_supported() { struct _GtkSessionLockInstance { GObject parent_instance; gboolean is_locked; + gboolean has_requested_lock; gboolean failed; }; @@ -48,6 +49,7 @@ static void gtk_session_lock_instance_class_init(GtkSessionLockInstanceClass *cc static void gtk_session_lock_instance_init(GtkSessionLockInstance *self) { self->is_locked = FALSE; + self->has_requested_lock = FALSE; self->failed = FALSE; } @@ -61,6 +63,9 @@ static void session_lock_locked_callback_impl(bool locked, void* data) { self->failed = TRUE; } self->is_locked = locked; + if (!locked) { + self->has_requested_lock = FALSE; + } g_signal_emit( self, session_lock_signals[ @@ -73,9 +78,8 @@ static void session_lock_locked_callback_impl(bool locked, void* data) { } gboolean gtk_session_lock_instance_lock(GtkSessionLockInstance* self) { - if (self->is_locked) { + if (self->has_requested_lock) { g_warning("Tried to lock multiple times without unlocking"); - g_signal_emit(self, session_lock_signals[SESSION_LOCK_SIGNAL_FAILED], 0); return false; } @@ -101,17 +105,20 @@ gboolean gtk_session_lock_instance_lock(GtkSessionLockInstance* self) { g_message("Move gtk4-layer-shell before libwayland-client in the linker options."); g_message("You may be able to fix with without recompiling by setting LD_PRELOAD=/path/to/libgtk4-layer-shell.so"); g_message("See https://github.com/wmww/gtk4-layer-shell/blob/main/linking.md for more info"); + g_signal_emit(self, session_lock_signals[SESSION_LOCK_SIGNAL_FAILED], 0); return false; } + self->has_requested_lock = TRUE; session_lock_lock(wl_display, session_lock_locked_callback_impl, self); return !self->failed; } void gtk_session_lock_instance_unlock(GtkSessionLockInstance* self) { if (self->is_locked) { - g_signal_emit(self, session_lock_signals[SESSION_LOCK_SIGNAL_UNLOCKED], 0); self->is_locked = FALSE; + self->has_requested_lock = FALSE; + g_signal_emit(self, session_lock_signals[SESSION_LOCK_SIGNAL_UNLOCKED], 0); session_lock_unlock(); } } diff --git a/test/lock-tests/test-immediate-failure.c b/test/lock-tests/test-immediate-failure.c index a3a4374..1a5c057 100644 --- a/test/lock-tests/test-immediate-failure.c +++ b/test/lock-tests/test-immediate-failure.c @@ -18,6 +18,7 @@ static void callback_0() { connect_lock_signals(lock_b, &state_b); ASSERT(gtk_session_lock_instance_lock(lock_b)); + ASSERT(!gtk_session_lock_instance_lock(lock_b)); create_lock_windows(lock_b); }