Testing with the Test Compositor
cl-wl/test-compositor is a minimal compositor that runs inside your test
process, built on libwayland-server. It is connected to a client display
over a socketpair. It:
- advertises the globals a test asks for, at the versions it asks for;
- creates a
resourcefor every object the client creates; - records every
requestit receives, with its arguments as Lisp values; - sends the events and protocol errors the test tells it to;
- runs only when told to, so a test is deterministic.
It implements no protocol behaviour of its own. It does not answer pings, send configures or release buffers unless the test posts those events. That is the point: the test decides exactly what the client sees. cl-wl's own suite and cl-windowing's Wayland tests are written this way.
(asdf:load-system :cl-wl/test-compositor)
It needs libwayland-server (Debian/Ubuntu: libwayland-server0) and
cl-threads.
A first test
(defpackage #:my-tests
(:use #:cl #:cl-wl/wayland #:cl-wl/xdg-shell #:cl-wl/test-compositor)
(:local-nicknames (#:wl #:cl-wl)))
(in-package #:my-tests)
(with-test-compositor (compositor display (wl-compositor 6))
(let* ((wl-compositor (bind-global compositor display 'wl-compositor 6))
(surface (wl-compositor-create-surface wl-compositor)))
(wl-surface-set-buffer-transform surface :|90|)
(wl-surface-commit surface)
(pump compositor display)
(list (mapcar #'request-name (compositor-requests compositor))
(compositor-requests compositor "set_buffer_transform"))))
;; => (("bind" "create_surface" "set_buffer_transform" "commit")
;; (#<REQUEST set_buffer_transform :|90|>))
with-test-compositor takes the variables for the compositor and the
client display, followed by the globals to advertise as unevaluated
(class version) pairs. Both are destroyed when the body is left.
Moving messages: pump
Nothing moves until you say so. pump:
- flushes the client's requests;
- lets the compositor handle them (
compositor-dispatch); - dispatches the client's events with a timeout of 0;
and repeats while a round delivered events or brought requests. A
request that a handler sends in answer to an event, such as
xdg-surface-ack-configure from a :configure handler, therefore arrives
in the same pump. It returns the number of events delivered. The
compositor sends no events on its own, so pump always ends.
(with-test-compositor (compositor display (wl-compositor 6) (xdg-wm-base 6))
(let* ((surface (wl-compositor-create-surface
(bind-global compositor display 'wl-compositor 6)))
(xdg-surface (xdg-wm-base-get-xdg-surface
(bind-global compositor display 'xdg-wm-base 6) surface))
(toplevel (xdg-surface-get-toplevel xdg-surface))
(seen '()))
(wl:set-event-handlers toplevel
:configure (lambda (toplevel width height states)
(declare (ignore toplevel))
(push (list width height states) seen)))
(wl:set-event-handlers xdg-surface
:configure (lambda (xdg-surface serial)
(xdg-surface-ack-configure xdg-surface serial)))
(pump compositor display)
(post-event compositor (compositor-last-created compositor "get_toplevel")
:configure 640 480 '(:maximized :activated))
(post-event compositor (compositor-last-created compositor "get_xdg_surface")
:configure 42)
(pump compositor display)
(list seen
(mapcar #'request-arguments (compositor-requests compositor "ack_configure"))
(compositor-errors compositor))))
;; => (((640 480 (:MAXIMIZED :ACTIVATED))) ((42)) NIL)
compositor-dispatch runs only the compositor's half, for tests that
interleave the two ends themselves.
Binding: bind-global
cl-wl:bind blocks until the compositor has answered, which a
compositor that only runs when pumped never does. bind-global runs the
compositor on a thread while it binds, then pumps:
(bind-global compositor display 'wl-seat 7) ; => #<WL-SEAT id 3 v7>
For other calls that wait for an answer, such as cl-wl:roundtrip,
cl-wl:follow-globals or code under test that binds, wrap them in
with-compositor-thread:
(with-compositor-thread (compositor)
(my-client:initialize display)) ; may BIND and ROUNDTRIP freely
Inside with-compositor-thread the body must not touch the compositor
itself, because libwayland-server is not thread-safe.
Resources
A resource is the compositor's side of one client object. It is the
same object for as long as it lives, so resources compare with eq:
| Reader | Value |
|---|---|
resource-class |
the proxy class of its interface, such as wl-surface |
resource-version |
its version |
resource-live-p |
false once the client or the compositor destroyed it |
A resource dies when the client sends a destructor request, when the
compositor sends a destructor event on it (such as wl_callback's
:done), or when the client is disconnected.
What the client sent: requests
(compositor-requests compositor &optional name) returns the requests
received, oldest first, only those named name if it is given:
| Reader | Value |
|---|---|
request-name |
the protocol's name, such as "set_title", or "bind" |
request-target |
the resource it was sent to; NIL for a bind |
request-arguments |
its arguments in wire order, as cl-wl would deliver them |
request-created |
the resource it created, or NIL |
Arguments arrive as Lisp values: enum values as keywords
(:xrgb8888, :|90|), fixed numbers as exact rationals, strings,
objects as resources, arrays of enum values as keyword lists, other
32-bit arrays as vectors, byte arrays as octet vectors, and fds as
integers. A new object appears both among the arguments and as
request-created. A bind's arguments are (class version).
Two shortcuts find the resources of the newest request of a name, and
signal wl:wl-error, saying so, if there was none:
(compositor-last-created compositor "get_toplevel")returns the resource that request created; for"bind", the global bound.(compositor-last-target compositor "commit")returns the resource that request was sent to.
clear-requests forgets what has been recorded so far, which is useful
between the setup and the part a test checks.
Sending events: post-event
(post-event compositor toplevel :configure 640 480 '(:maximized :activated))
(post-event compositor seat :capabilities '(:pointer :keyboard))
(post-event compositor pointer :motion 100 5/2 -1)
Values are given as cl-wl delivers them: enum values as keywords or
keyword lists (integers are sent as they are), fixed numbers as reals,
strings as strings, objects as resources, arrays of enum values as
keyword lists, and arrays of integers as sequences. An octet vector is
sent as raw bytes, for a string or an array, which lets a test send
malformed data such as invalid UTF-8. A wrong number of values, or an
event the interface lacks, signals wl:wl-error naming what is expected,
as does posting on a resource that has been destroyed:
The :NAME event of #<RESOURCE wl_seat v7> takes 1 value: name, not 0.
#<RESOURCE wl_seat v7> has no :NOPE event. Its events are :CAPABILITIES, :NAME.
After a destructor event, such as wl_callback's :done, the resource
is destroyed, as a real compositor destroys it.
For an event that creates an object, such as wl_data_device's
:data-offer, (create-resource compositor parent class) makes the
compositor-side object to pass for its new_id.
Globals that come and go
(let ((output (add-global compositor 'wl-output 4))) ; a monitor plugged in
(pump compositor display)
…
(remove-global compositor output) ; and unplugged
(pump compositor display))
The client hears of each change at its next dispatch after pump or
compositor-dispatch. Use this to test code built on follow-globals.
Failures
;; A protocol error: the client's next dispatch signals COMPOSITOR-ERROR.
(post-error compositor (compositor-last-created compositor "create_surface")
(wl:enum-value 'wl-surface-error :invalid-size) "invalid size")
(handler-case (pump compositor display)
(wl:compositor-error (e)
(list (wl:compositor-error-interface e) ; "wl_surface"
(wl:enum-keywords 'wl-surface-error (wl:compositor-error-code e))))) ; :INVALID-SIZE
;; A dropped client: the next read signals CONNECTION-ERROR.
(disconnect-client compositor)
(handler-case (wl:dispatch display :timeout 1)
(wl:connection-error (e) (princ-to-string e)))
;; => "The Wayland connection failed: Broken pipe"
libwayland prints the protocol error's message to standard error, but it does not pass it on to the client.
Checking the compositor itself
Errors inside the compositor's callbacks, while it handles a request,
cannot unwind through libwayland. They are recorded instead. Every test
should check that (compositor-errors compositor) is empty.
Without the macro
(multiple-value-bind (compositor display)
(make-test-compositor '((wl-compositor 6) (wl-shm 1)))
(unwind-protect (run-tests compositor display)
(wl:disconnect display) ; the client first
(destroy-test-compositor compositor)))
Reference
| Symbol | What it does |
|---|---|
test-compositor |
The compositor's type. |
make-test-compositor globals |
Makes a compositor and a connected display; returns both. |
destroy-test-compositor compositor |
Destroys it. Disconnect the client first. |
with-test-compositor (compositor display &rest globals) &body |
Both, destroyed on exit. |
pump compositor display |
Moves messages both ways until nothing moves. Returns events delivered. |
compositor-dispatch compositor &optional timeout-ms |
Lets the compositor handle what has arrived and send its events. |
with-compositor-thread (compositor) &body |
Runs the compositor on a thread during the body. |
bind-global compositor display class version |
Binds a global and returns the proxy. |
add-global compositor class version |
Advertises a global; returns it for remove-global. |
remove-global compositor global |
Withdraws a global. |
disconnect-client compositor |
Drops the client. |
resource, resource-class, resource-version, resource-live-p |
The compositor's side of a client object. |
create-resource compositor parent class |
A compositor-side object, for events that create one. |
request, request-name, request-target, request-arguments, request-created |
A request received. |
compositor-requests compositor &optional name |
The requests received, oldest first. |
clear-requests compositor |
Forgets them. |
compositor-last-target compositor name |
The resource the newest name request was sent to. |
compositor-last-created compositor name |
The resource the newest name request created. |
compositor-errors compositor |
Errors raised in the compositor's callbacks, newest first. |
post-event compositor resource event &rest values |
Sends an event. |
post-error compositor resource code message |
Raises a protocol error. |
Thread rules: a compositor and its client display are used from one
thread, except inside with-compositor-thread, where the compositor runs
on a thread of its own and must not be touched.
Limits: it is Linux-only, because it uses socketpair with Linux
constants, and it serves a single client.