Skip to content

Commit

Permalink
Merge pull request #65 from wmww/lock-tests
Browse files Browse the repository at this point in the history
Session lock tests
  • Loading branch information
wmww authored Jan 26, 2025
2 parents 46ba98c + bbb61ab commit 1e8c848
Show file tree
Hide file tree
Showing 62 changed files with 625 additions and 100 deletions.
4 changes: 1 addition & 3 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,4 @@ jobs:
- name: Build
run: ninja -C build
- name: Test
run: ninja -C build test
- name: Install
run: sudo ninja -C build install
run: meson test -C build --verbose
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <testname> --verbose -C build`
* Or, to run a specific test and print the complete output `meson test -C build --verbose <testname>`
* To watch a specific test run against the currently active Wayland compositor `ninja -C build && ./build/test/<testname> --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/<testname> --auto` can be helpful for debugging

## Licensing
100% MIT (unlike the GTK3 version of this library which contained GPL code copied from GTK)
2 changes: 1 addition & 1 deletion include/gtk4-session-lock.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
12 changes: 10 additions & 2 deletions src/gtk4-session-lock.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ gboolean gtk_session_lock_is_supported() {
struct _GtkSessionLockInstance {
GObject parent_instance;
gboolean is_locked;
gboolean has_requested_lock;
gboolean failed;
};

Expand Down Expand Up @@ -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;
}

Expand All @@ -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[
Expand All @@ -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;
}

Expand All @@ -101,15 +105,19 @@ 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) {
self->is_locked = FALSE;
self->has_requested_lock = FALSE;
g_signal_emit(self, session_lock_signals[SESSION_LOCK_SIGNAL_UNLOCKED], 0);
session_lock_unlock();
}
Expand Down
6 changes: 4 additions & 2 deletions test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ 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.

### 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:
Expand Down
3 changes: 2 additions & 1 deletion test/check-all-tests-are-in-meson.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
62 changes: 51 additions & 11 deletions test/integration-test-common/integration-test-common.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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),
"<span font_desc=\"20.0\">"
"Layer shell test"
"Test window"
"</span>");
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_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_UNLOCKED, "%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);
Expand All @@ -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();
Expand Down
11 changes: 10 additions & 1 deletion test/integration-test-common/integration-test-common.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define TEST_CLIENT_COMMON_H

#include "gtk4-layer-shell.h"
#include "gtk4-session-lock.h"
#include "test-common.h"
#include <gtk/gtk.h>
#include <gdk/gdk.h>
Expand All @@ -14,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")
Expand All @@ -28,4 +29,12 @@ extern void (* test_callbacks[])(void);

GtkWindow* create_default_window();

enum lock_state_t {
LOCK_STATE_UNLOCKED = 0,
LOCK_STATE_LOCKED,
LOCK_STATE_FAILED,
};
void connect_lock_signals(GtkSessionLockInstance* lock, enum lock_state_t* state);
void create_lock_windows(GtkSessionLockInstance* lock);

#endif // TEST_CLIENT_COMMON_H
31 changes: 0 additions & 31 deletions test/integration-tests/test-menu-popup.c

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
integration_tests = [
layer_tests = [
'test-is-supported-true',
'test-layer-surface-not-created',
'test-creation-properties',
Expand All @@ -19,7 +19,9 @@ integration_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',
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
44 changes: 44 additions & 0 deletions test/layer-tests/test-layer-surface-popup.c
Original file line number Diff line number Diff line change
@@ -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);

UNEXPECT_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,
)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
52 changes: 52 additions & 0 deletions test/layer-tests/test-xdg-toplevel-popup-with-layer-surface.c
Original file line number Diff line number Diff line change
@@ -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);

UNEXPECT_MESSAGE(zwlr_layer_surface_v1 .get_popup);
UNEXPECT_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,
)
Loading

0 comments on commit 1e8c848

Please sign in to comment.