GTK Layer Shell tests

February 2, 2026 ยท View on GitHub

This directory is home to the gtk4-layer-shell test suite.

To run tests

ninja -C build test

(where build is the path to your build directory).

To run a single test:

ninja -C build && meson test -C build --verbose [testname]

To run all tests under valgrind:

GTKLS_VALGRIND=1 ninja -C build test

To run against the current Wayland compositor:

ninja -C build && ./build/test/<testname> --auto

This can help with debugging since you get to see the output.

Don't run the session lock tests this way unless you know what you're doing. Some of them may soft-brick your compositor. If this happens you may need to reboot.

You can drop the --auto flag to run the test interactively, so you can click through each section.

To run against the current compositor under wayland-debug:

ninja -C build && wayland-debug -f 'zwlr_*, xdg_*, ext_*' -r ./build/test/<testname> --auto

To run under the mock server, but separately: This can be useful to run the tests under wayland-debug or GDB

# server:
ninja -C build && GTKLS_TEST_DIR=/tmp ./build/test/mock-server/mock-server
# client:
ninja -C build && GTKLS_TEST_DIR=/tmp ./build/test/[testname] --auto

To run the test script without meson: This is mostly useful for debugging the test script

ninja -C build && test/run-integration-test.py build/test/<testname>

To add a new integration test

  1. Copy an existing integration test file
  2. Implement your test as a series of one or more callbacks
  3. Add its name to the list in test/integration-tests/meson.build

Scripts

  • tests-not-enabled.py is only run if tests are disabled, and explains to the user how to enable them
  • run-integration-test.py runs a single integration test
  • check-all-tests-are-in-meson.py fails if any test files exist that haven't been added to meson (an easy mistake to make)

Integration tests

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 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.

EXPECT_MESSAGE must also be used to expect any glib warnings/errors emitted.

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:

  • Creates a temporary directory in /tmp to serve as XDG_RUNTIME_DIR (this allows tests to run in parallel without interfering with each other)
  • Spins up a mock Wayland server
  • Runs the given integration test within it
  • (Both are run with WAYLAND_DEBUG=1 so protocol messages are written to stderr by libwayland)
  • Ensures both the client and server exit successfully
  • Parses the client's protocol message expectations
  • Ensures they match the protocol messages generated by libwayland

Mock server

Rather than running the integration tests in an external Wayland compositor, we implement our own mock Wayland compositor (located in mock-server). This doesn't show anything on-screen or get real user input, it simply gives the required responses to protocol messages. It's only dependency is libwayland. It implements most of the protocol with a single default dispatcher. This reads the message signature and takes whatever action appears to be required. The behavior of some messages is overridden in overrides.c.

Container images

These images are used to run the tests in CI

  • Generated a classic token here with write:packages, read:packages and delete:packages
  • podman login ghcr.io -u wmww and entered it on the prompt
  • podman build -f test/Containerfile -t ghcr.io/wmww/gtk4-layer-shell-ci:latest .
  • podman push ghcr.io/wmww/gtk4-layer-shell-ci:latest
  • Create container and shell into it: podman run -it --replace --name gtk4-layer-shell-ci-0 ghcr.io/wmww/gtk4-layer-shell-ci:latest bash
  • Open another shell: podman exec -it gtk4-layer-shell-ci-0 bash
  • Get size of image echo "$(("$(podman image inspect ghcr.io/wmww/gtk4-layer-shell-ci:latest --format '{{ .Size }}')" / 1024 / 1024)) MB"