Add test suite
f50d353fa9c5146abe44563bf71fbbb6f6aef457
1 parent
686c5c01
Makefile
+54 -1
| 30 | 30 | HDRS := config.h swm.h util.h $(PROTO) |
|
| 31 | 31 | ||
| 32 | 32 | WL_SCANNER := $(shell $(PKG_CONFIG) --variable=wayland_scanner wayland-scanner) |
|
| 33 | 33 | WL_PROTO := $(shell $(PKG_CONFIG) --variable=pkgdatadir wayland-protocols) |
|
| 34 | 34 | ||
| 35 | + | TEST_BUILD ?= test/.build |
|
| 36 | + | TEST_SRC := $(TEST_BUILD)/source |
|
| 37 | + | TEST_BIN := $(TEST_BUILD)/bin |
|
| 38 | + | TEST_CPPFLAGS := -I$(TEST_SRC) $(CPPFLAGS) |
|
| 39 | + | TEST_CFLAGS := -std=c23 -g -O0 \ |
|
| 40 | + | -Wno-unused-function -Wno-unused-variable -Wno-unused-parameter |
|
| 41 | + | ||
| 42 | + | ifeq ($(TEST_COVERAGE),1) |
|
| 43 | + | TEST_CFLAGS += -fprofile-instr-generate -fcoverage-mapping |
|
| 44 | + | endif |
|
| 45 | + | ||
| 35 | 46 | all: man swm |
|
| 36 | 47 | ||
| 37 | 48 | man: swm.1 |
|
| 38 | 49 | ||
| 39 | 50 | swm.1: swm.1.adoc |
| 104 | 115 | makepkg --force |
|
| 105 | 116 | ||
| 106 | 117 | clean: |
|
| 107 | 118 | rm -f swm swm.1 *.o *-protocol.h *-protocol.c |
|
| 108 | 119 | ||
| 109 | - | .PHONY: all man fmt install uninstall archive package clean |
|
| 120 | + | $(TEST_SRC) $(TEST_BIN): |
|
| 121 | + | @mkdir -p $@ |
|
| 122 | + | ||
| 123 | + | $(TEST_SRC)/swm.c: swm.c | $(TEST_SRC) |
|
| 124 | + | @cp -p $< $@ |
|
| 125 | + | ||
| 126 | + | $(TEST_SRC)/config.h: config.def.h | $(TEST_SRC) |
|
| 127 | + | @cp -p $< $@ |
|
| 128 | + | ||
| 129 | + | TEST_COMMON := Makefile config.mk $(TEST_SRC)/swm.c $(TEST_SRC)/config.h \ |
|
| 130 | + | swm.h util.c util.h $(PROTO) ext-workspace-v1-protocol.c |
|
| 131 | + | ||
| 132 | + | $(TEST_BIN)/unit: test/unit.c $(TEST_COMMON) | $(TEST_BIN) |
|
| 133 | + | @echo "cc test/unit.c => $@" |
|
| 134 | + | @$(CC) $(TEST_CPPFLAGS) $(TEST_CFLAGS) test/unit.c util.c \ |
|
| 135 | + | ext-workspace-v1-protocol.c $(LDLIBS) -o $@ |
|
| 136 | + | ||
| 137 | + | $(TEST_BIN)/swm: $(TEST_COMMON) | $(TEST_BIN) |
|
| 138 | + | @echo "cc swm.c => $@" |
|
| 139 | + | @$(CC) $(TEST_CPPFLAGS) $(TEST_CFLAGS) $(TEST_SRC)/swm.c util.c \ |
|
| 140 | + | ext-workspace-v1-protocol.c $(LDLIBS) -o $@ |
|
| 141 | + | ||
| 142 | + | test-build: $(TEST_BIN)/unit $(TEST_BIN)/swm |
|
| 143 | + | ||
| 144 | + | test: test-build |
|
| 145 | + | @SWM_TEST_BUILD=$(abspath $(TEST_BUILD)) PKG_CONFIG=$(PKG_CONFIG) $(PYTHON) test/run.py |
|
| 146 | + | ||
| 147 | + | test-unit: $(TEST_BIN)/unit |
|
| 148 | + | @SWM_TEST_BUILD=$(abspath $(TEST_BUILD)) PKG_CONFIG=$(PKG_CONFIG) $(PYTHON) test/run.py unit |
|
| 149 | + | ||
| 150 | + | test-integration: $(TEST_BIN)/swm |
|
| 151 | + | @SWM_TEST_BUILD=$(abspath $(TEST_BUILD)) PKG_CONFIG=$(PKG_CONFIG) $(PYTHON) test/run.py integration |
|
| 152 | + | ||
| 153 | + | coverage: |
|
| 154 | + | @$(MAKE) --no-print-directory TEST_BUILD=$(TEST_BUILD)/coverage TEST_COVERAGE=1 test-build |
|
| 155 | + | @SWM_TEST_BUILD=$(abspath $(TEST_BUILD)/coverage) PKG_CONFIG=$(PKG_CONFIG) \ |
|
| 156 | + | $(PYTHON) test/run.py coverage |
|
| 157 | + | ||
| 158 | + | test-clean: |
|
| 159 | + | @SWM_TEST_BUILD=$(abspath $(TEST_BUILD)) $(PYTHON) test/run.py clean |
|
| 160 | + | ||
| 161 | + | .PHONY: all man fmt install uninstall archive package clean coverage |
|
| 162 | + | .PHONY: test-build test test-unit test-integration test-clean |
README
+17 -5
| 119 | 119 | ||
| 120 | 120 | Run the complete test suite with: |
|
| 121 | 121 | ||
| 122 | 122 | make test |
|
| 123 | 123 | ||
| 124 | - | It builds unit tests and protocol clients, runs a headless nested-compositor |
|
| 125 | - | integration suite (including a two-output run), collects LLVM source coverage, |
|
| 126 | - | and requires at least 80% aggregate production-code line coverage. The test |
|
| 127 | - | target therefore also requires LLVM's coverage tools, a POSIX shell, and the |
|
| 128 | - | runtime libraries needed by the normal build. |
|
| 124 | + | Make builds one focused C unit-test binary and a test compositor, then the |
|
| 125 | + | Python runner executes them alongside native Wayland and XWayland clients, |
|
| 126 | + | including a two-output run. Unchanged binaries are reused; use the narrower |
|
| 127 | + | targets while developing: |
|
| 128 | + | ||
| 129 | + | make test-unit |
|
| 130 | + | make test-integration |
|
| 131 | + | ||
| 132 | + | Coverage is informational and has its own target: |
|
| 133 | + | ||
| 134 | + | make coverage |
|
| 135 | + | ||
| 136 | + | Generated test files live under `test/.build/`; remove them with |
|
| 137 | + | `make test-clean`. All targets require Python 3 and the normal build |
|
| 138 | + | dependencies. Integration tests additionally require PyWayland and its protocol |
|
| 139 | + | scanner, xcffib, wlr-randr, and Xwayland; coverage requires LLVM's coverage |
|
| 140 | + | tools. |
|
| 129 | 141 | ||
| 130 | 142 | Other development targets are: |
|
| 131 | 143 | ||
| 132 | 144 | make man # build swm.1 from swm.1.adoc |
|
| 133 | 145 | make fmt # format tracked C sources with clang-format |
config.mk
+1 -0
| 1 | 1 | VERSION = 1.0.0 |
|
| 2 | 2 | PKG_CONFIG = pkg-config |
|
| 3 | 3 | CC = clang |
|
| 4 | + | PYTHON ?= python3 |
|
| 4 | 5 | PREFIX = /usr/local |
|
| 5 | 6 | MANDIR = $(PREFIX)/share/man |
|
| 6 | 7 | DATADIR = $(PREFIX)/share |
|
| 7 | 8 | WLR_INCS = `$(PKG_CONFIG) --cflags wlroots-0.20` |
|
| 8 | 9 | WLR_LIBS = `$(PKG_CONFIG) --libs wlroots-0.20` |
protocols/input-method-unstable-v2.xml
added
+494 -0
| 1 | + | <?xml version="1.0" encoding="UTF-8"?> |
|
| 2 | + | <protocol name="input_method_unstable_v2"> |
|
| 3 | + | ||
| 4 | + | <copyright> |
|
| 5 | + | Copyright © 2008-2011 Kristian Høgsberg |
|
| 6 | + | Copyright © 2010-2011 Intel Corporation |
|
| 7 | + | Copyright © 2012-2013 Collabora, Ltd. |
|
| 8 | + | Copyright © 2012, 2013 Intel Corporation |
|
| 9 | + | Copyright © 2015, 2016 Jan Arne Petersen |
|
| 10 | + | Copyright © 2017, 2018 Red Hat, Inc. |
|
| 11 | + | Copyright © 2018 Purism SPC |
|
| 12 | + | ||
| 13 | + | Permission is hereby granted, free of charge, to any person obtaining a |
|
| 14 | + | copy of this software and associated documentation files (the "Software"), |
|
| 15 | + | to deal in the Software without restriction, including without limitation |
|
| 16 | + | the rights to use, copy, modify, merge, publish, distribute, sublicense, |
|
| 17 | + | and/or sell copies of the Software, and to permit persons to whom the |
|
| 18 | + | Software is furnished to do so, subject to the following conditions: |
|
| 19 | + | ||
| 20 | + | The above copyright notice and this permission notice (including the next |
|
| 21 | + | paragraph) shall be included in all copies or substantial portions of the |
|
| 22 | + | Software. |
|
| 23 | + | ||
| 24 | + | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
| 25 | + | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
| 26 | + | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
|
| 27 | + | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
| 28 | + | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
|
| 29 | + | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
|
| 30 | + | DEALINGS IN THE SOFTWARE. |
|
| 31 | + | </copyright> |
|
| 32 | + | ||
| 33 | + | <description summary="Protocol for creating input methods"> |
|
| 34 | + | This protocol allows applications to act as input methods for compositors. |
|
| 35 | + | ||
| 36 | + | An input method context is used to manage the state of the input method. |
|
| 37 | + | ||
| 38 | + | Text strings are UTF-8 encoded, their indices and lengths are in bytes. |
|
| 39 | + | ||
| 40 | + | This document adheres to the RFC 2119 when using words like "must", |
|
| 41 | + | "should", "may", etc. |
|
| 42 | + | ||
| 43 | + | Warning! The protocol described in this file is experimental and |
|
| 44 | + | backward incompatible changes may be made. Backward compatible changes |
|
| 45 | + | may be added together with the corresponding interface version bump. |
|
| 46 | + | Backward incompatible changes are done by bumping the version number in |
|
| 47 | + | the protocol and interface names and resetting the interface version. |
|
| 48 | + | Once the protocol is to be declared stable, the 'z' prefix and the |
|
| 49 | + | version number in the protocol and interface names are removed and the |
|
| 50 | + | interface version number is reset. |
|
| 51 | + | </description> |
|
| 52 | + | ||
| 53 | + | <interface name="zwp_input_method_v2" version="1"> |
|
| 54 | + | <description summary="input method"> |
|
| 55 | + | An input method object allows for clients to compose text. |
|
| 56 | + | ||
| 57 | + | The objects connects the client to a text input in an application, and |
|
| 58 | + | lets the client to serve as an input method for a seat. |
|
| 59 | + | ||
| 60 | + | The zwp_input_method_v2 object can occupy two distinct states: active and |
|
| 61 | + | inactive. In the active state, the object is associated to and |
|
| 62 | + | communicates with a text input. In the inactive state, there is no |
|
| 63 | + | associated text input, and the only communication is with the compositor. |
|
| 64 | + | Initially, the input method is in the inactive state. |
|
| 65 | + | ||
| 66 | + | Requests issued in the inactive state must be accepted by the compositor. |
|
| 67 | + | Because of the serial mechanism, and the state reset on activate event, |
|
| 68 | + | they will not have any effect on the state of the next text input. |
|
| 69 | + | ||
| 70 | + | There must be no more than one input method object per seat. |
|
| 71 | + | </description> |
|
| 72 | + | ||
| 73 | + | <enum name="error"> |
|
| 74 | + | <entry name="role" value="0" summary="wl_surface has another role"/> |
|
| 75 | + | </enum> |
|
| 76 | + | ||
| 77 | + | <event name="activate"> |
|
| 78 | + | <description summary="input method has been requested"> |
|
| 79 | + | Notification that a text input focused on this seat requested the input |
|
| 80 | + | method to be activated. |
|
| 81 | + | ||
| 82 | + | This event serves the purpose of providing the compositor with an |
|
| 83 | + | active input method. |
|
| 84 | + | ||
| 85 | + | This event resets all state associated with previous enable, disable, |
|
| 86 | + | surrounding_text, text_change_cause, and content_type events, as well |
|
| 87 | + | as the state associated with set_preedit_string, commit_string, and |
|
| 88 | + | delete_surrounding_text requests. In addition, it marks the |
|
| 89 | + | zwp_input_method_v2 object as active, and makes any existing |
|
| 90 | + | zwp_input_popup_surface_v2 objects visible. |
|
| 91 | + | ||
| 92 | + | The surrounding_text, and content_type events must follow before the |
|
| 93 | + | next done event if the text input supports the respective |
|
| 94 | + | functionality. |
|
| 95 | + | ||
| 96 | + | State set with this event is double-buffered. It will get applied on |
|
| 97 | + | the next zwp_input_method_v2.done event, and stay valid until changed. |
|
| 98 | + | </description> |
|
| 99 | + | </event> |
|
| 100 | + | ||
| 101 | + | <event name="deactivate"> |
|
| 102 | + | <description summary="deactivate event"> |
|
| 103 | + | Notification that no focused text input currently needs an active |
|
| 104 | + | input method on this seat. |
|
| 105 | + | ||
| 106 | + | This event marks the zwp_input_method_v2 object as inactive. The |
|
| 107 | + | compositor must make all existing zwp_input_popup_surface_v2 objects |
|
| 108 | + | invisible until the next activate event. |
|
| 109 | + | ||
| 110 | + | State set with this event is double-buffered. It will get applied on |
|
| 111 | + | the next zwp_input_method_v2.done event, and stay valid until changed. |
|
| 112 | + | </description> |
|
| 113 | + | </event> |
|
| 114 | + | ||
| 115 | + | <event name="surrounding_text"> |
|
| 116 | + | <description summary="surrounding text event"> |
|
| 117 | + | Updates the surrounding plain text around the cursor, excluding the |
|
| 118 | + | preedit text. |
|
| 119 | + | ||
| 120 | + | If any preedit text is present, it is replaced with the cursor for the |
|
| 121 | + | purpose of this event. |
|
| 122 | + | ||
| 123 | + | The argument text is a buffer containing the preedit string, and must |
|
| 124 | + | include the cursor position, and the complete selection. It should |
|
| 125 | + | contain additional characters before and after these. There is a |
|
| 126 | + | maximum length of wayland messages, so text can not be longer than 4000 |
|
| 127 | + | bytes. |
|
| 128 | + | ||
| 129 | + | cursor is the byte offset of the cursor within the text buffer. |
|
| 130 | + | ||
| 131 | + | anchor is the byte offset of the selection anchor within the text |
|
| 132 | + | buffer. If there is no selected text, anchor must be the same as |
|
| 133 | + | cursor. |
|
| 134 | + | ||
| 135 | + | If this event does not arrive before the first done event, the input |
|
| 136 | + | method may assume that the text input does not support this |
|
| 137 | + | functionality and ignore following surrounding_text events. |
|
| 138 | + | ||
| 139 | + | Values set with this event are double-buffered. They will get applied |
|
| 140 | + | and set to initial values on the next zwp_input_method_v2.done |
|
| 141 | + | event. |
|
| 142 | + | ||
| 143 | + | The initial state for affected fields is empty, meaning that the text |
|
| 144 | + | input does not support sending surrounding text. If the empty values |
|
| 145 | + | get applied, subsequent attempts to change them may have no effect. |
|
| 146 | + | </description> |
|
| 147 | + | <arg name="text" type="string"/> |
|
| 148 | + | <arg name="cursor" type="uint"/> |
|
| 149 | + | <arg name="anchor" type="uint"/> |
|
| 150 | + | </event> |
|
| 151 | + | ||
| 152 | + | <event name="text_change_cause"> |
|
| 153 | + | <description summary="indicates the cause of surrounding text change"> |
|
| 154 | + | Tells the input method why the text surrounding the cursor changed. |
|
| 155 | + | ||
| 156 | + | Whenever the client detects an external change in text, cursor, or |
|
| 157 | + | anchor position, it must issue this request to the compositor. This |
|
| 158 | + | request is intended to give the input method a chance to update the |
|
| 159 | + | preedit text in an appropriate way, e.g. by removing it when the user |
|
| 160 | + | starts typing with a keyboard. |
|
| 161 | + | ||
| 162 | + | cause describes the source of the change. |
|
| 163 | + | ||
| 164 | + | The value set with this event is double-buffered. It will get applied |
|
| 165 | + | and set to its initial value on the next zwp_input_method_v2.done |
|
| 166 | + | event. |
|
| 167 | + | ||
| 168 | + | The initial value of cause is input_method. |
|
| 169 | + | </description> |
|
| 170 | + | <arg name="cause" type="uint" enum="zwp_text_input_v3.change_cause"/> |
|
| 171 | + | </event> |
|
| 172 | + | ||
| 173 | + | <event name="content_type"> |
|
| 174 | + | <description summary="content purpose and hint"> |
|
| 175 | + | Indicates the content type and hint for the current |
|
| 176 | + | zwp_input_method_v2 instance. |
|
| 177 | + | ||
| 178 | + | Values set with this event are double-buffered. They will get applied |
|
| 179 | + | on the next zwp_input_method_v2.done event. |
|
| 180 | + | ||
| 181 | + | The initial value for hint is none, and the initial value for purpose |
|
| 182 | + | is normal. |
|
| 183 | + | </description> |
|
| 184 | + | <arg name="hint" type="uint" enum="zwp_text_input_v3.content_hint"/> |
|
| 185 | + | <arg name="purpose" type="uint" enum="zwp_text_input_v3.content_purpose"/> |
|
| 186 | + | </event> |
|
| 187 | + | ||
| 188 | + | <event name="done"> |
|
| 189 | + | <description summary="apply state"> |
|
| 190 | + | Atomically applies state changes recently sent to the client. |
|
| 191 | + | ||
| 192 | + | The done event establishes and updates the state of the client, and |
|
| 193 | + | must be issued after any changes to apply them. |
|
| 194 | + | ||
| 195 | + | Text input state (content purpose, content hint, surrounding text, and |
|
| 196 | + | change cause) is conceptually double-buffered within an input method |
|
| 197 | + | context. |
|
| 198 | + | ||
| 199 | + | Events modify the pending state, as opposed to the current state in use |
|
| 200 | + | by the input method. A done event atomically applies all pending state, |
|
| 201 | + | replacing the current state. After done, the new pending state is as |
|
| 202 | + | documented for each related request. |
|
| 203 | + | ||
| 204 | + | Events must be applied in the order of arrival. |
|
| 205 | + | ||
| 206 | + | Neither current nor pending state are modified unless noted otherwise. |
|
| 207 | + | </description> |
|
| 208 | + | </event> |
|
| 209 | + | ||
| 210 | + | <request name="commit_string"> |
|
| 211 | + | <description summary="commit string"> |
|
| 212 | + | Send the commit string text for insertion to the application. |
|
| 213 | + | ||
| 214 | + | Inserts a string at current cursor position (see commit event |
|
| 215 | + | sequence). The string to commit could be either just a single character |
|
| 216 | + | after a key press or the result of some composing. |
|
| 217 | + | ||
| 218 | + | The argument text is a buffer containing the string to insert. There is |
|
| 219 | + | a maximum length of wayland messages, so text can not be longer than |
|
| 220 | + | 4000 bytes. |
|
| 221 | + | ||
| 222 | + | Values set with this event are double-buffered. They must be applied |
|
| 223 | + | and reset to initial on the next zwp_text_input_v3.commit request. |
|
| 224 | + | ||
| 225 | + | The initial value of text is an empty string. |
|
| 226 | + | </description> |
|
| 227 | + | <arg name="text" type="string"/> |
|
| 228 | + | </request> |
|
| 229 | + | ||
| 230 | + | <request name="set_preedit_string"> |
|
| 231 | + | <description summary="pre-edit string"> |
|
| 232 | + | Send the pre-edit string text to the application text input. |
|
| 233 | + | ||
| 234 | + | Place a new composing text (pre-edit) at the current cursor position. |
|
| 235 | + | Any previously set composing text must be removed. Any previously |
|
| 236 | + | existing selected text must be removed. The cursor is moved to a new |
|
| 237 | + | position within the preedit string. |
|
| 238 | + | ||
| 239 | + | The argument text is a buffer containing the preedit string. There is |
|
| 240 | + | a maximum length of wayland messages, so text can not be longer than |
|
| 241 | + | 4000 bytes. |
|
| 242 | + | ||
| 243 | + | The arguments cursor_begin and cursor_end are counted in bytes relative |
|
| 244 | + | to the beginning of the submitted string buffer. Cursor should be |
|
| 245 | + | hidden by the text input when both are equal to -1. |
|
| 246 | + | ||
| 247 | + | cursor_begin indicates the beginning of the cursor. cursor_end |
|
| 248 | + | indicates the end of the cursor. It may be equal or different than |
|
| 249 | + | cursor_begin. |
|
| 250 | + | ||
| 251 | + | Values set with this event are double-buffered. They must be applied on |
|
| 252 | + | the next zwp_input_method_v2.commit event. |
|
| 253 | + | ||
| 254 | + | The initial value of text is an empty string. The initial value of |
|
| 255 | + | cursor_begin, and cursor_end are both 0. |
|
| 256 | + | </description> |
|
| 257 | + | <arg name="text" type="string"/> |
|
| 258 | + | <arg name="cursor_begin" type="int"/> |
|
| 259 | + | <arg name="cursor_end" type="int"/> |
|
| 260 | + | </request> |
|
| 261 | + | ||
| 262 | + | <request name="delete_surrounding_text"> |
|
| 263 | + | <description summary="delete text"> |
|
| 264 | + | Remove the surrounding text. |
|
| 265 | + | ||
| 266 | + | before_length and after_length are the number of bytes before and after |
|
| 267 | + | the current cursor index (excluding the preedit text) to delete. |
|
| 268 | + | ||
| 269 | + | If any preedit text is present, it is replaced with the cursor for the |
|
| 270 | + | purpose of this event. In effect before_length is counted from the |
|
| 271 | + | beginning of preedit text, and after_length from its end (see commit |
|
| 272 | + | event sequence). |
|
| 273 | + | ||
| 274 | + | Values set with this event are double-buffered. They must be applied |
|
| 275 | + | and reset to initial on the next zwp_input_method_v2.commit request. |
|
| 276 | + | ||
| 277 | + | The initial values of both before_length and after_length are 0. |
|
| 278 | + | </description> |
|
| 279 | + | <arg name="before_length" type="uint"/> |
|
| 280 | + | <arg name="after_length" type="uint"/> |
|
| 281 | + | </request> |
|
| 282 | + | ||
| 283 | + | <request name="commit"> |
|
| 284 | + | <description summary="apply state"> |
|
| 285 | + | Apply state changes from commit_string, set_preedit_string and |
|
| 286 | + | delete_surrounding_text requests. |
|
| 287 | + | ||
| 288 | + | The state relating to these events is double-buffered, and each one |
|
| 289 | + | modifies the pending state. This request replaces the current state |
|
| 290 | + | with the pending state. |
|
| 291 | + | ||
| 292 | + | The connected text input is expected to proceed by evaluating the |
|
| 293 | + | changes in the following order: |
|
| 294 | + | ||
| 295 | + | 1. Replace existing preedit string with the cursor. |
|
| 296 | + | 2. Delete requested surrounding text. |
|
| 297 | + | 3. Insert commit string with the cursor at its end. |
|
| 298 | + | 4. Calculate surrounding text to send. |
|
| 299 | + | 5. Insert new preedit text in cursor position. |
|
| 300 | + | 6. Place cursor inside preedit text. |
|
| 301 | + | ||
| 302 | + | The serial number reflects the last state of the zwp_input_method_v2 |
|
| 303 | + | object known to the client. The value of the serial argument must be |
|
| 304 | + | equal to the number of done events already issued by that object. When |
|
| 305 | + | the compositor receives a commit request with a serial different than |
|
| 306 | + | the number of past done events, it must proceed as normal, except it |
|
| 307 | + | should not change the current state of the zwp_input_method_v2 object. |
|
| 308 | + | </description> |
|
| 309 | + | <arg name="serial" type="uint"/> |
|
| 310 | + | </request> |
|
| 311 | + | ||
| 312 | + | <request name="get_input_popup_surface"> |
|
| 313 | + | <description summary="create popup surface"> |
|
| 314 | + | Creates a new zwp_input_popup_surface_v2 object wrapping a given |
|
| 315 | + | surface. |
|
| 316 | + | ||
| 317 | + | The surface gets assigned the "input_popup" role. If the surface |
|
| 318 | + | already has an assigned role, the compositor must issue a protocol |
|
| 319 | + | error. |
|
| 320 | + | </description> |
|
| 321 | + | <arg name="id" type="new_id" interface="zwp_input_popup_surface_v2"/> |
|
| 322 | + | <arg name="surface" type="object" interface="wl_surface"/> |
|
| 323 | + | </request> |
|
| 324 | + | ||
| 325 | + | <request name="grab_keyboard"> |
|
| 326 | + | <description summary="grab hardware keyboard"> |
|
| 327 | + | Allow an input method to receive hardware keyboard input and process |
|
| 328 | + | key events to generate text events (with pre-edit) over the wire. This |
|
| 329 | + | allows input methods which compose multiple key events for inputting |
|
| 330 | + | text like it is done for CJK languages. |
|
| 331 | + | ||
| 332 | + | The compositor should send all keyboard events on the seat to the grab |
|
| 333 | + | holder via the returned wl_keyboard object. Nevertheless, the |
|
| 334 | + | compositor may decide not to forward any particular event. The |
|
| 335 | + | compositor must not further process any event after it has been |
|
| 336 | + | forwarded to the grab holder. |
|
| 337 | + | ||
| 338 | + | Releasing the resulting wl_keyboard object releases the grab. |
|
| 339 | + | </description> |
|
| 340 | + | <arg name="keyboard" type="new_id" |
|
| 341 | + | interface="zwp_input_method_keyboard_grab_v2"/> |
|
| 342 | + | </request> |
|
| 343 | + | ||
| 344 | + | <event name="unavailable"> |
|
| 345 | + | <description summary="input method unavailable"> |
|
| 346 | + | The input method ceased to be available. |
|
| 347 | + | ||
| 348 | + | The compositor must issue this event as the only event on the object if |
|
| 349 | + | there was another input_method object associated with the same seat at |
|
| 350 | + | the time of its creation. |
|
| 351 | + | ||
| 352 | + | The compositor must issue this request when the object is no longer |
|
| 353 | + | usable, e.g. due to seat removal. |
|
| 354 | + | ||
| 355 | + | The input method context becomes inert and should be destroyed after |
|
| 356 | + | deactivation is handled. Any further requests and events except for the |
|
| 357 | + | destroy request must be ignored. |
|
| 358 | + | </description> |
|
| 359 | + | </event> |
|
| 360 | + | ||
| 361 | + | <request name="destroy" type="destructor"> |
|
| 362 | + | <description summary="destroy the text input"> |
|
| 363 | + | Destroys the zwp_text_input_v2 object and any associated child |
|
| 364 | + | objects, i.e. zwp_input_popup_surface_v2 and |
|
| 365 | + | zwp_input_method_keyboard_grab_v2. |
|
| 366 | + | </description> |
|
| 367 | + | </request> |
|
| 368 | + | </interface> |
|
| 369 | + | ||
| 370 | + | <interface name="zwp_input_popup_surface_v2" version="1"> |
|
| 371 | + | <description summary="popup surface"> |
|
| 372 | + | This interface marks a surface as a popup for interacting with an input |
|
| 373 | + | method. |
|
| 374 | + | ||
| 375 | + | The compositor should place it near the active text input area. It must |
|
| 376 | + | be visible if and only if the input method is in the active state. |
|
| 377 | + | ||
| 378 | + | The client must not destroy the underlying wl_surface while the |
|
| 379 | + | zwp_input_popup_surface_v2 object exists. |
|
| 380 | + | </description> |
|
| 381 | + | ||
| 382 | + | <event name="text_input_rectangle"> |
|
| 383 | + | <description summary="set text input area position"> |
|
| 384 | + | Notify about the position of the area of the text input expressed as a |
|
| 385 | + | rectangle in surface local coordinates. |
|
| 386 | + | ||
| 387 | + | This is a hint to the input method telling it the relative position of |
|
| 388 | + | the text being entered. |
|
| 389 | + | </description> |
|
| 390 | + | <arg name="x" type="int"/> |
|
| 391 | + | <arg name="y" type="int"/> |
|
| 392 | + | <arg name="width" type="int"/> |
|
| 393 | + | <arg name="height" type="int"/> |
|
| 394 | + | </event> |
|
| 395 | + | ||
| 396 | + | <request name="destroy" type="destructor"/> |
|
| 397 | + | </interface> |
|
| 398 | + | ||
| 399 | + | <interface name="zwp_input_method_keyboard_grab_v2" version="1"> |
|
| 400 | + | <!-- Closely follows wl_keyboard version 6 --> |
|
| 401 | + | <description summary="keyboard grab"> |
|
| 402 | + | The zwp_input_method_keyboard_grab_v2 interface represents an exclusive |
|
| 403 | + | grab of the wl_keyboard interface associated with the seat. |
|
| 404 | + | </description> |
|
| 405 | + | ||
| 406 | + | <event name="keymap"> |
|
| 407 | + | <description summary="keyboard mapping"> |
|
| 408 | + | This event provides a file descriptor to the client which can be |
|
| 409 | + | memory-mapped to provide a keyboard mapping description. |
|
| 410 | + | </description> |
|
| 411 | + | <arg name="format" type="uint" enum="wl_keyboard.keymap_format" |
|
| 412 | + | summary="keymap format"/> |
|
| 413 | + | <arg name="fd" type="fd" summary="keymap file descriptor"/> |
|
| 414 | + | <arg name="size" type="uint" summary="keymap size, in bytes"/> |
|
| 415 | + | </event> |
|
| 416 | + | ||
| 417 | + | <event name="key"> |
|
| 418 | + | <description summary="key event"> |
|
| 419 | + | A key was pressed or released. |
|
| 420 | + | The time argument is a timestamp with millisecond granularity, with an |
|
| 421 | + | undefined base. |
|
| 422 | + | </description> |
|
| 423 | + | <arg name="serial" type="uint" summary="serial number of the key event"/> |
|
| 424 | + | <arg name="time" type="uint" summary="timestamp with millisecond granularity"/> |
|
| 425 | + | <arg name="key" type="uint" summary="key that produced the event"/> |
|
| 426 | + | <arg name="state" type="uint" enum="wl_keyboard.key_state" |
|
| 427 | + | summary="physical state of the key"/> |
|
| 428 | + | </event> |
|
| 429 | + | ||
| 430 | + | <event name="modifiers"> |
|
| 431 | + | <description summary="modifier and group state"> |
|
| 432 | + | Notifies clients that the modifier and/or group state has changed, and |
|
| 433 | + | it should update its local state. |
|
| 434 | + | </description> |
|
| 435 | + | <arg name="serial" type="uint" summary="serial number of the modifiers event"/> |
|
| 436 | + | <arg name="mods_depressed" type="uint" summary="depressed modifiers"/> |
|
| 437 | + | <arg name="mods_latched" type="uint" summary="latched modifiers"/> |
|
| 438 | + | <arg name="mods_locked" type="uint" summary="locked modifiers"/> |
|
| 439 | + | <arg name="group" type="uint" summary="keyboard layout"/> |
|
| 440 | + | </event> |
|
| 441 | + | ||
| 442 | + | <request name="release" type="destructor"> |
|
| 443 | + | <description summary="release the grab object"/> |
|
| 444 | + | </request> |
|
| 445 | + | ||
| 446 | + | <event name="repeat_info"> |
|
| 447 | + | <description summary="repeat rate and delay"> |
|
| 448 | + | Informs the client about the keyboard's repeat rate and delay. |
|
| 449 | + | ||
| 450 | + | This event is sent as soon as the zwp_input_method_keyboard_grab_v2 |
|
| 451 | + | object has been created, and is guaranteed to be received by the |
|
| 452 | + | client before any key press event. |
|
| 453 | + | ||
| 454 | + | Negative values for either rate or delay are illegal. A rate of zero |
|
| 455 | + | will disable any repeating (regardless of the value of delay). |
|
| 456 | + | ||
| 457 | + | This event can be sent later on as well with a new value if necessary, |
|
| 458 | + | so clients should continue listening for the event past the creation |
|
| 459 | + | of zwp_input_method_keyboard_grab_v2. |
|
| 460 | + | </description> |
|
| 461 | + | <arg name="rate" type="int" |
|
| 462 | + | summary="the rate of repeating keys in characters per second"/> |
|
| 463 | + | <arg name="delay" type="int" |
|
| 464 | + | summary="delay in milliseconds since key down until repeating starts"/> |
|
| 465 | + | </event> |
|
| 466 | + | </interface> |
|
| 467 | + | ||
| 468 | + | <interface name="zwp_input_method_manager_v2" version="1"> |
|
| 469 | + | <description summary="input method manager"> |
|
| 470 | + | The input method manager allows the client to become the input method on |
|
| 471 | + | a chosen seat. |
|
| 472 | + | ||
| 473 | + | No more than one input method must be associated with any seat at any |
|
| 474 | + | given time. |
|
| 475 | + | </description> |
|
| 476 | + | ||
| 477 | + | <request name="get_input_method"> |
|
| 478 | + | <description summary="request an input method object"> |
|
| 479 | + | Request a new input zwp_input_method_v2 object associated with a given |
|
| 480 | + | seat. |
|
| 481 | + | </description> |
|
| 482 | + | <arg name="seat" type="object" interface="wl_seat"/> |
|
| 483 | + | <arg name="input_method" type="new_id" interface="zwp_input_method_v2"/> |
|
| 484 | + | </request> |
|
| 485 | + | ||
| 486 | + | <request name="destroy" type="destructor"> |
|
| 487 | + | <description summary="destroy the input method manager"> |
|
| 488 | + | Destroys the zwp_input_method_manager_v2 object. |
|
| 489 | + | ||
| 490 | + | The zwp_input_method_v2 objects originating from it remain valid. |
|
| 491 | + | </description> |
|
| 492 | + | </request> |
|
| 493 | + | </interface> |
|
| 494 | + | </protocol> |
protocols/virtual-keyboard-unstable-v1.xml
added
+62 -0
| 1 | + | <?xml version="1.0" encoding="UTF-8"?> |
|
| 2 | + | <!-- SPDX-License-Identifier: MIT --> |
|
| 3 | + | <protocol name="virtual_keyboard_unstable_v1"> |
|
| 4 | + | <copyright> |
|
| 5 | + | Copyright © 2008-2011 Kristian Høgsberg |
|
| 6 | + | Copyright © 2010-2013 Intel Corporation |
|
| 7 | + | Copyright © 2012-2013 Collabora, Ltd. |
|
| 8 | + | Copyright © 2018 Purism SPC |
|
| 9 | + | ||
| 10 | + | Permission is hereby granted, free of charge, to any person obtaining a |
|
| 11 | + | copy of this software and associated documentation files (the "Software"), |
|
| 12 | + | to deal in the Software without restriction, including without limitation |
|
| 13 | + | the rights to use, copy, modify, merge, publish, distribute, sublicense, |
|
| 14 | + | and/or sell copies of the Software, and to permit persons to whom the |
|
| 15 | + | Software is furnished to do so, subject to the following conditions: |
|
| 16 | + | ||
| 17 | + | The above copyright notice and this permission notice shall be included |
|
| 18 | + | in all copies or substantial portions of the Software. |
|
| 19 | + | ||
| 20 | + | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
| 21 | + | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
| 22 | + | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
| 23 | + | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
| 24 | + | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
|
| 25 | + | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
|
| 26 | + | DEALINGS IN THE SOFTWARE. |
|
| 27 | + | </copyright> |
|
| 28 | + | <interface name="zwp_virtual_keyboard_v1" version="1"> |
|
| 29 | + | <description summary="virtual keyboard"/> |
|
| 30 | + | <request name="keymap"> |
|
| 31 | + | <arg name="format" type="uint"/> |
|
| 32 | + | <arg name="fd" type="fd"/> |
|
| 33 | + | <arg name="size" type="uint"/> |
|
| 34 | + | </request> |
|
| 35 | + | <enum name="error"> |
|
| 36 | + | <entry name="no_keymap" value="0" summary="No keymap was set"/> |
|
| 37 | + | </enum> |
|
| 38 | + | <request name="key"> |
|
| 39 | + | <arg name="time" type="uint"/> |
|
| 40 | + | <arg name="key" type="uint"/> |
|
| 41 | + | <arg name="state" type="uint"/> |
|
| 42 | + | </request> |
|
| 43 | + | <request name="modifiers"> |
|
| 44 | + | <arg name="mods_depressed" type="uint"/> |
|
| 45 | + | <arg name="mods_latched" type="uint"/> |
|
| 46 | + | <arg name="mods_locked" type="uint"/> |
|
| 47 | + | <arg name="group" type="uint"/> |
|
| 48 | + | </request> |
|
| 49 | + | <request name="destroy" type="destructor" since="1"/> |
|
| 50 | + | </interface> |
|
| 51 | + | <interface name="zwp_virtual_keyboard_manager_v1" version="1"> |
|
| 52 | + | <description summary="virtual keyboard manager"/> |
|
| 53 | + | <enum name="error"> |
|
| 54 | + | <entry name="unauthorized" value="0" |
|
| 55 | + | summary="client not authorized to use the interface"/> |
|
| 56 | + | </enum> |
|
| 57 | + | <request name="create_virtual_keyboard"> |
|
| 58 | + | <arg name="seat" type="object" interface="wl_seat"/> |
|
| 59 | + | <arg name="id" type="new_id" interface="zwp_virtual_keyboard_v1"/> |
|
| 60 | + | </request> |
|
| 61 | + | </interface> |
|
| 62 | + | </protocol> |
protocols/wlr-foreign-toplevel-management-unstable-v1.xml
added
+270 -0
| 1 | + | <?xml version="1.0" encoding="UTF-8"?> |
|
| 2 | + | <protocol name="wlr_foreign_toplevel_management_unstable_v1"> |
|
| 3 | + | <copyright> |
|
| 4 | + | Copyright © 2018 Ilia Bozhinov |
|
| 5 | + | ||
| 6 | + | Permission to use, copy, modify, distribute, and sell this |
|
| 7 | + | software and its documentation for any purpose is hereby granted |
|
| 8 | + | without fee, provided that the above copyright notice appear in |
|
| 9 | + | all copies and that both that copyright notice and this permission |
|
| 10 | + | notice appear in supporting documentation, and that the name of |
|
| 11 | + | the copyright holders not be used in advertising or publicity |
|
| 12 | + | pertaining to distribution of the software without specific, |
|
| 13 | + | written prior permission. The copyright holders make no |
|
| 14 | + | representations about the suitability of this software for any |
|
| 15 | + | purpose. It is provided "as is" without express or implied |
|
| 16 | + | warranty. |
|
| 17 | + | ||
| 18 | + | THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS |
|
| 19 | + | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND |
|
| 20 | + | FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY |
|
| 21 | + | SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
|
| 22 | + | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN |
|
| 23 | + | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, |
|
| 24 | + | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF |
|
| 25 | + | THIS SOFTWARE. |
|
| 26 | + | </copyright> |
|
| 27 | + | ||
| 28 | + | <interface name="zwlr_foreign_toplevel_manager_v1" version="3"> |
|
| 29 | + | <description summary="list and control opened apps"> |
|
| 30 | + | The purpose of this protocol is to enable the creation of taskbars |
|
| 31 | + | and docks by providing them with a list of opened applications and |
|
| 32 | + | letting them request certain actions on them, like maximizing, etc. |
|
| 33 | + | ||
| 34 | + | After a client binds the zwlr_foreign_toplevel_manager_v1, each opened |
|
| 35 | + | toplevel window will be sent via the toplevel event |
|
| 36 | + | </description> |
|
| 37 | + | ||
| 38 | + | <event name="toplevel"> |
|
| 39 | + | <description summary="a toplevel has been created"> |
|
| 40 | + | This event is emitted whenever a new toplevel window is created. It |
|
| 41 | + | is emitted for all toplevels, regardless of the app that has created |
|
| 42 | + | them. |
|
| 43 | + | ||
| 44 | + | All initial details of the toplevel(title, app_id, states, etc.) will |
|
| 45 | + | be sent immediately after this event via the corresponding events in |
|
| 46 | + | zwlr_foreign_toplevel_handle_v1. |
|
| 47 | + | </description> |
|
| 48 | + | <arg name="toplevel" type="new_id" interface="zwlr_foreign_toplevel_handle_v1"/> |
|
| 49 | + | </event> |
|
| 50 | + | ||
| 51 | + | <request name="stop"> |
|
| 52 | + | <description summary="stop sending events"> |
|
| 53 | + | Indicates the client no longer wishes to receive events for new toplevels. |
|
| 54 | + | However the compositor may emit further toplevel_created events, until |
|
| 55 | + | the finished event is emitted. |
|
| 56 | + | ||
| 57 | + | The client must not send any more requests after this one. |
|
| 58 | + | </description> |
|
| 59 | + | </request> |
|
| 60 | + | ||
| 61 | + | <event name="finished" type="destructor"> |
|
| 62 | + | <description summary="the compositor has finished with the toplevel manager"> |
|
| 63 | + | This event indicates that the compositor is done sending events to the |
|
| 64 | + | zwlr_foreign_toplevel_manager_v1. The server will destroy the object |
|
| 65 | + | immediately after sending this request, so it will become invalid and |
|
| 66 | + | the client should free any resources associated with it. |
|
| 67 | + | </description> |
|
| 68 | + | </event> |
|
| 69 | + | </interface> |
|
| 70 | + | ||
| 71 | + | <interface name="zwlr_foreign_toplevel_handle_v1" version="3"> |
|
| 72 | + | <description summary="an opened toplevel"> |
|
| 73 | + | A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel |
|
| 74 | + | window. Each app may have multiple opened toplevels. |
|
| 75 | + | ||
| 76 | + | Each toplevel has a list of outputs it is visible on, conveyed to the |
|
| 77 | + | client with the output_enter and output_leave events. |
|
| 78 | + | </description> |
|
| 79 | + | ||
| 80 | + | <event name="title"> |
|
| 81 | + | <description summary="title change"> |
|
| 82 | + | This event is emitted whenever the title of the toplevel changes. |
|
| 83 | + | </description> |
|
| 84 | + | <arg name="title" type="string"/> |
|
| 85 | + | </event> |
|
| 86 | + | ||
| 87 | + | <event name="app_id"> |
|
| 88 | + | <description summary="app-id change"> |
|
| 89 | + | This event is emitted whenever the app-id of the toplevel changes. |
|
| 90 | + | </description> |
|
| 91 | + | <arg name="app_id" type="string"/> |
|
| 92 | + | </event> |
|
| 93 | + | ||
| 94 | + | <event name="output_enter"> |
|
| 95 | + | <description summary="toplevel entered an output"> |
|
| 96 | + | This event is emitted whenever the toplevel becomes visible on |
|
| 97 | + | the given output. A toplevel may be visible on multiple outputs. |
|
| 98 | + | </description> |
|
| 99 | + | <arg name="output" type="object" interface="wl_output"/> |
|
| 100 | + | </event> |
|
| 101 | + | ||
| 102 | + | <event name="output_leave"> |
|
| 103 | + | <description summary="toplevel left an output"> |
|
| 104 | + | This event is emitted whenever the toplevel stops being visible on |
|
| 105 | + | the given output. It is guaranteed that an entered-output event |
|
| 106 | + | with the same output has been emitted before this event. |
|
| 107 | + | </description> |
|
| 108 | + | <arg name="output" type="object" interface="wl_output"/> |
|
| 109 | + | </event> |
|
| 110 | + | ||
| 111 | + | <request name="set_maximized"> |
|
| 112 | + | <description summary="requests that the toplevel be maximized"> |
|
| 113 | + | Requests that the toplevel be maximized. If the maximized state actually |
|
| 114 | + | changes, this will be indicated by the state event. |
|
| 115 | + | </description> |
|
| 116 | + | </request> |
|
| 117 | + | ||
| 118 | + | <request name="unset_maximized"> |
|
| 119 | + | <description summary="requests that the toplevel be unmaximized"> |
|
| 120 | + | Requests that the toplevel be unmaximized. If the maximized state actually |
|
| 121 | + | changes, this will be indicated by the state event. |
|
| 122 | + | </description> |
|
| 123 | + | </request> |
|
| 124 | + | ||
| 125 | + | <request name="set_minimized"> |
|
| 126 | + | <description summary="requests that the toplevel be minimized"> |
|
| 127 | + | Requests that the toplevel be minimized. If the minimized state actually |
|
| 128 | + | changes, this will be indicated by the state event. |
|
| 129 | + | </description> |
|
| 130 | + | </request> |
|
| 131 | + | ||
| 132 | + | <request name="unset_minimized"> |
|
| 133 | + | <description summary="requests that the toplevel be unminimized"> |
|
| 134 | + | Requests that the toplevel be unminimized. If the minimized state actually |
|
| 135 | + | changes, this will be indicated by the state event. |
|
| 136 | + | </description> |
|
| 137 | + | </request> |
|
| 138 | + | ||
| 139 | + | <request name="activate"> |
|
| 140 | + | <description summary="activate the toplevel"> |
|
| 141 | + | Request that this toplevel be activated on the given seat. |
|
| 142 | + | There is no guarantee the toplevel will be actually activated. |
|
| 143 | + | </description> |
|
| 144 | + | <arg name="seat" type="object" interface="wl_seat"/> |
|
| 145 | + | </request> |
|
| 146 | + | ||
| 147 | + | <enum name="state"> |
|
| 148 | + | <description summary="types of states on the toplevel"> |
|
| 149 | + | The different states that a toplevel can have. These have the same meaning |
|
| 150 | + | as the states with the same names defined in xdg-toplevel |
|
| 151 | + | </description> |
|
| 152 | + | ||
| 153 | + | <entry name="maximized" value="0" summary="the toplevel is maximized"/> |
|
| 154 | + | <entry name="minimized" value="1" summary="the toplevel is minimized"/> |
|
| 155 | + | <entry name="activated" value="2" summary="the toplevel is active"/> |
|
| 156 | + | <entry name="fullscreen" value="3" summary="the toplevel is fullscreen" since="2"/> |
|
| 157 | + | </enum> |
|
| 158 | + | ||
| 159 | + | <event name="state"> |
|
| 160 | + | <description summary="the toplevel state changed"> |
|
| 161 | + | This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 |
|
| 162 | + | is created and each time the toplevel state changes, either because of a |
|
| 163 | + | compositor action or because of a request in this protocol. |
|
| 164 | + | </description> |
|
| 165 | + | ||
| 166 | + | <arg name="state" type="array"/> |
|
| 167 | + | </event> |
|
| 168 | + | ||
| 169 | + | <event name="done"> |
|
| 170 | + | <description summary="all information about the toplevel has been sent"> |
|
| 171 | + | This event is sent after all changes in the toplevel state have been |
|
| 172 | + | sent. |
|
| 173 | + | ||
| 174 | + | This allows changes to the zwlr_foreign_toplevel_handle_v1 properties |
|
| 175 | + | to be seen as atomic, even if they happen via multiple events. |
|
| 176 | + | </description> |
|
| 177 | + | </event> |
|
| 178 | + | ||
| 179 | + | <request name="close"> |
|
| 180 | + | <description summary="request that the toplevel be closed"> |
|
| 181 | + | Send a request to the toplevel to close itself. The compositor would |
|
| 182 | + | typically use a shell-specific method to carry out this request, for |
|
| 183 | + | example by sending the xdg_toplevel.close event. However, this gives |
|
| 184 | + | no guarantees the toplevel will actually be destroyed. If and when |
|
| 185 | + | this happens, the zwlr_foreign_toplevel_handle_v1.closed event will |
|
| 186 | + | be emitted. |
|
| 187 | + | </description> |
|
| 188 | + | </request> |
|
| 189 | + | ||
| 190 | + | <request name="set_rectangle"> |
|
| 191 | + | <description summary="the rectangle which represents the toplevel"> |
|
| 192 | + | The rectangle of the surface specified in this request corresponds to |
|
| 193 | + | the place where the app using this protocol represents the given toplevel. |
|
| 194 | + | It can be used by the compositor as a hint for some operations, e.g |
|
| 195 | + | minimizing. The client is however not required to set this, in which |
|
| 196 | + | case the compositor is free to decide some default value. |
|
| 197 | + | ||
| 198 | + | If the client specifies more than one rectangle, only the last one is |
|
| 199 | + | considered. |
|
| 200 | + | ||
| 201 | + | The dimensions are given in surface-local coordinates. |
|
| 202 | + | Setting width=height=0 removes the already-set rectangle. |
|
| 203 | + | </description> |
|
| 204 | + | ||
| 205 | + | <arg name="surface" type="object" interface="wl_surface"/> |
|
| 206 | + | <arg name="x" type="int"/> |
|
| 207 | + | <arg name="y" type="int"/> |
|
| 208 | + | <arg name="width" type="int"/> |
|
| 209 | + | <arg name="height" type="int"/> |
|
| 210 | + | </request> |
|
| 211 | + | ||
| 212 | + | <enum name="error"> |
|
| 213 | + | <entry name="invalid_rectangle" value="0" |
|
| 214 | + | summary="the provided rectangle is invalid"/> |
|
| 215 | + | </enum> |
|
| 216 | + | ||
| 217 | + | <event name="closed"> |
|
| 218 | + | <description summary="this toplevel has been destroyed"> |
|
| 219 | + | This event means the toplevel has been destroyed. It is guaranteed there |
|
| 220 | + | won't be any more events for this zwlr_foreign_toplevel_handle_v1. The |
|
| 221 | + | toplevel itself becomes inert so any requests will be ignored except the |
|
| 222 | + | destroy request. |
|
| 223 | + | </description> |
|
| 224 | + | </event> |
|
| 225 | + | ||
| 226 | + | <request name="destroy" type="destructor"> |
|
| 227 | + | <description summary="destroy the zwlr_foreign_toplevel_handle_v1 object"> |
|
| 228 | + | Destroys the zwlr_foreign_toplevel_handle_v1 object. |
|
| 229 | + | ||
| 230 | + | This request should be called either when the client does not want to |
|
| 231 | + | use the toplevel anymore or after the closed event to finalize the |
|
| 232 | + | destruction of the object. |
|
| 233 | + | </description> |
|
| 234 | + | </request> |
|
| 235 | + | ||
| 236 | + | <!-- Version 2 additions --> |
|
| 237 | + | ||
| 238 | + | <request name="set_fullscreen" since="2"> |
|
| 239 | + | <description summary="request that the toplevel be fullscreened"> |
|
| 240 | + | Requests that the toplevel be fullscreened on the given output. If the |
|
| 241 | + | fullscreen state and/or the outputs the toplevel is visible on actually |
|
| 242 | + | change, this will be indicated by the state and output_enter/leave |
|
| 243 | + | events. |
|
| 244 | + | ||
| 245 | + | The output parameter is only a hint to the compositor. Also, if output |
|
| 246 | + | is NULL, the compositor should decide which output the toplevel will be |
|
| 247 | + | fullscreened on, if at all. |
|
| 248 | + | </description> |
|
| 249 | + | <arg name="output" type="object" interface="wl_output" allow-null="true"/> |
|
| 250 | + | </request> |
|
| 251 | + | ||
| 252 | + | <request name="unset_fullscreen" since="2"> |
|
| 253 | + | <description summary="request that the toplevel be unfullscreened"> |
|
| 254 | + | Requests that the toplevel be unfullscreened. If the fullscreen state |
|
| 255 | + | actually changes, this will be indicated by the state event. |
|
| 256 | + | </description> |
|
| 257 | + | </request> |
|
| 258 | + | ||
| 259 | + | <!-- Version 3 additions --> |
|
| 260 | + | ||
| 261 | + | <event name="parent" since="3"> |
|
| 262 | + | <description summary="parent change"> |
|
| 263 | + | This event is emitted whenever the parent of the toplevel changes. |
|
| 264 | + | ||
| 265 | + | No event is emitted when the parent handle is destroyed by the client. |
|
| 266 | + | </description> |
|
| 267 | + | <arg name="parent" type="object" interface="zwlr_foreign_toplevel_handle_v1" allow-null="true"/> |
|
| 268 | + | </event> |
|
| 269 | + | </interface> |
|
| 270 | + | </protocol> |
protocols/wlr-virtual-pointer-unstable-v1.xml
added
+65 -0
| 1 | + | <?xml version="1.0" encoding="UTF-8"?> |
|
| 2 | + | <!-- SPDX-License-Identifier: MIT --> |
|
| 3 | + | <protocol name="wlr_virtual_pointer_unstable_v1"> |
|
| 4 | + | <copyright> |
|
| 5 | + | Copyright © 2019 Josef Gajdusek |
|
| 6 | + | Permission is hereby granted, free of charge, to any person obtaining a |
|
| 7 | + | copy of this software and associated documentation files, to deal in the |
|
| 8 | + | Software without restriction, including without limitation the rights to |
|
| 9 | + | use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
| 10 | + | copies, subject to inclusion of this notice. THE SOFTWARE IS PROVIDED |
|
| 11 | + | "AS IS", WITHOUT WARRANTY OF ANY KIND. |
|
| 12 | + | </copyright> |
|
| 13 | + | <interface name="zwlr_virtual_pointer_v1" version="2"> |
|
| 14 | + | <description summary="virtual pointer"/> |
|
| 15 | + | <enum name="error"> |
|
| 16 | + | <entry name="invalid_axis" value="0" summary="invalid axis"/> |
|
| 17 | + | <entry name="invalid_axis_source" value="1" summary="invalid source"/> |
|
| 18 | + | </enum> |
|
| 19 | + | <request name="motion"> |
|
| 20 | + | <arg name="time" type="uint"/><arg name="dx" type="fixed"/> |
|
| 21 | + | <arg name="dy" type="fixed"/> |
|
| 22 | + | </request> |
|
| 23 | + | <request name="motion_absolute"> |
|
| 24 | + | <arg name="time" type="uint"/><arg name="x" type="uint"/> |
|
| 25 | + | <arg name="y" type="uint"/><arg name="x_extent" type="uint"/> |
|
| 26 | + | <arg name="y_extent" type="uint"/> |
|
| 27 | + | </request> |
|
| 28 | + | <request name="button"> |
|
| 29 | + | <arg name="time" type="uint"/><arg name="button" type="uint"/> |
|
| 30 | + | <arg name="state" type="uint" enum="wl_pointer.button_state"/> |
|
| 31 | + | </request> |
|
| 32 | + | <request name="axis"> |
|
| 33 | + | <arg name="time" type="uint"/> |
|
| 34 | + | <arg name="axis" type="uint" enum="wl_pointer.axis"/> |
|
| 35 | + | <arg name="value" type="fixed"/> |
|
| 36 | + | </request> |
|
| 37 | + | <request name="frame"/> |
|
| 38 | + | <request name="axis_source"> |
|
| 39 | + | <arg name="axis_source" type="uint" enum="wl_pointer.axis_source"/> |
|
| 40 | + | </request> |
|
| 41 | + | <request name="axis_stop"> |
|
| 42 | + | <arg name="time" type="uint"/> |
|
| 43 | + | <arg name="axis" type="uint" enum="wl_pointer.axis"/> |
|
| 44 | + | </request> |
|
| 45 | + | <request name="axis_discrete"> |
|
| 46 | + | <arg name="time" type="uint"/> |
|
| 47 | + | <arg name="axis" type="uint" enum="wl_pointer.axis"/> |
|
| 48 | + | <arg name="value" type="fixed"/><arg name="discrete" type="int"/> |
|
| 49 | + | </request> |
|
| 50 | + | <request name="destroy" type="destructor" since="1"/> |
|
| 51 | + | </interface> |
|
| 52 | + | <interface name="zwlr_virtual_pointer_manager_v1" version="2"> |
|
| 53 | + | <description summary="virtual pointer manager"/> |
|
| 54 | + | <request name="create_virtual_pointer"> |
|
| 55 | + | <arg name="seat" type="object" interface="wl_seat" allow-null="true"/> |
|
| 56 | + | <arg name="id" type="new_id" interface="zwlr_virtual_pointer_v1"/> |
|
| 57 | + | </request> |
|
| 58 | + | <request name="destroy" type="destructor" since="1"/> |
|
| 59 | + | <request name="create_virtual_pointer_with_output" since="2"> |
|
| 60 | + | <arg name="seat" type="object" interface="wl_seat" allow-null="true"/> |
|
| 61 | + | <arg name="output" type="object" interface="wl_output" allow-null="true"/> |
|
| 62 | + | <arg name="id" type="new_id" interface="zwlr_virtual_pointer_v1"/> |
|
| 63 | + | </request> |
|
| 64 | + | </interface> |
|
| 65 | + | </protocol> |
test/.gitignore
added
+2 -0
| 1 | + | .build/ |
|
| 2 | + | __pycache__/ |
test/clients.py
added
+924 -0
| 1 | + | #!/usr/bin/env python3 |
|
| 2 | + | ||
| 3 | + | """Protocol clients used by the Python integration runner.""" |
|
| 4 | + | ||
| 5 | + | from __future__ import annotations |
|
| 6 | + | ||
| 7 | + | import ctypes |
|
| 8 | + | import mmap |
|
| 9 | + | import os |
|
| 10 | + | import struct |
|
| 11 | + | import subprocess |
|
| 12 | + | import sys |
|
| 13 | + | import tempfile |
|
| 14 | + | import time |
|
| 15 | + | from pathlib import Path |
|
| 16 | + | ||
| 17 | + | from pywayland.client import Display |
|
| 18 | + | ||
| 19 | + | from protocols.ext_session_lock_v1 import ( |
|
| 20 | + | ExtSessionLockManagerV1, |
|
| 21 | + | ) |
|
| 22 | + | from protocols.ext_workspace_v1 import ExtWorkspaceManagerV1 |
|
| 23 | + | from protocols.idle_inhibit_unstable_v1 import ZwpIdleInhibitManagerV1 |
|
| 24 | + | from protocols.input_method_unstable_v2 import ZwpInputMethodManagerV2 |
|
| 25 | + | from protocols.keyboard_shortcuts_inhibit_unstable_v1 import ( |
|
| 26 | + | ZwpKeyboardShortcutsInhibitManagerV1, |
|
| 27 | + | ) |
|
| 28 | + | from protocols.pointer_constraints_unstable_v1 import ZwpPointerConstraintsV1 |
|
| 29 | + | from protocols.relative_pointer_unstable_v1 import ZwpRelativePointerManagerV1 |
|
| 30 | + | from protocols.text_input_unstable_v3 import ZwpTextInputManagerV3 |
|
| 31 | + | from protocols.virtual_keyboard_unstable_v1 import ZwpVirtualKeyboardManagerV1 |
|
| 32 | + | from protocols.wayland import ( |
|
| 33 | + | WlCompositor, |
|
| 34 | + | WlKeyboard, |
|
| 35 | + | WlOutput, |
|
| 36 | + | WlSeat, |
|
| 37 | + | WlShm, |
|
| 38 | + | ) |
|
| 39 | + | from protocols.wlr_foreign_toplevel_management_unstable_v1 import ( |
|
| 40 | + | ZwlrForeignToplevelManagerV1, |
|
| 41 | + | ) |
|
| 42 | + | from protocols.wlr_layer_shell_unstable_v1 import ZwlrLayerShellV1 |
|
| 43 | + | from protocols.wlr_output_power_management_unstable_v1 import ( |
|
| 44 | + | ZwlrOutputPowerManagerV1, |
|
| 45 | + | ) |
|
| 46 | + | from protocols.wlr_virtual_pointer_unstable_v1 import ZwlrVirtualPointerManagerV1 |
|
| 47 | + | from protocols.xdg_shell import XdgPositioner, XdgToplevel, XdgWmBase |
|
| 48 | + | ||
| 49 | + | ||
| 50 | + | class Failure(RuntimeError): |
|
| 51 | + | """Report a protocol-client failure.""" |
|
| 52 | + | ||
| 53 | + | ||
| 54 | + | class Connection: |
|
| 55 | + | """Bind requested globals on one Wayland connection.""" |
|
| 56 | + | ||
| 57 | + | def __init__(self, *interfaces): |
|
| 58 | + | self.display = Display() |
|
| 59 | + | self.display.connect() |
|
| 60 | + | self.registry = self.display.get_registry() |
|
| 61 | + | self.interfaces = {interface.name: interface for interface in interfaces} |
|
| 62 | + | self.globals: dict[str, object] = {} |
|
| 63 | + | self.registry.dispatcher["global"] = self._global |
|
| 64 | + | self.display.roundtrip() |
|
| 65 | + | ||
| 66 | + | def _global(self, registry, name: int, interface: str, version: int) -> None: |
|
| 67 | + | wanted = self.interfaces.get(interface) |
|
| 68 | + | if wanted is not None and interface not in self.globals: |
|
| 69 | + | self.globals[interface] = registry.bind(name, wanted, min(version, wanted.version)) |
|
| 70 | + | ||
| 71 | + | def get(self, interface): |
|
| 72 | + | """Return a required bound global.""" |
|
| 73 | + | ||
| 74 | + | try: |
|
| 75 | + | return self.globals[interface.name] |
|
| 76 | + | except KeyError as error: |
|
| 77 | + | raise Failure(f"compositor does not advertise {interface.name}") from error |
|
| 78 | + | ||
| 79 | + | def roundtrips(self, count: int = 1) -> None: |
|
| 80 | + | """Complete several request/event exchanges.""" |
|
| 81 | + | ||
| 82 | + | for _ in range(count): |
|
| 83 | + | if self.display.roundtrip() < 0: |
|
| 84 | + | raise Failure("Wayland roundtrip failed") |
|
| 85 | + | ||
| 86 | + | def close(self) -> None: |
|
| 87 | + | """Disconnect from the compositor.""" |
|
| 88 | + | ||
| 89 | + | self.display.disconnect() |
|
| 90 | + | ||
| 91 | + | ||
| 92 | + | def create_buffer(shm, width: int, height: int, color: int): |
|
| 93 | + | """Create a solid-color shared-memory buffer.""" |
|
| 94 | + | ||
| 95 | + | size = width * height * 4 |
|
| 96 | + | with tempfile.TemporaryFile(dir=os.environ.get("SWM_TEST_DIR")) as stream: |
|
| 97 | + | stream.truncate(size) |
|
| 98 | + | pixels = mmap.mmap(stream.fileno(), size) |
|
| 99 | + | pixels[:] = struct.pack("=I", color) * (width * height) |
|
| 100 | + | pixels.flush() |
|
| 101 | + | pool = shm.create_pool(stream.fileno(), size) |
|
| 102 | + | buffer = pool.create_buffer(0, width, height, width * 4, WlShm.format.argb8888) |
|
| 103 | + | pool.destroy() |
|
| 104 | + | buffer.dispatcher["release"] = lambda proxy: proxy.destroy() |
|
| 105 | + | return buffer |
|
| 106 | + | ||
| 107 | + | ||
| 108 | + | class Window: |
|
| 109 | + | """Own one mapped XDG toplevel and report configure state.""" |
|
| 110 | + | ||
| 111 | + | def __init__( |
|
| 112 | + | self, |
|
| 113 | + | connection: Connection, |
|
| 114 | + | title: str, |
|
| 115 | + | color: int, |
|
| 116 | + | parent=None, |
|
| 117 | + | fullscreen: bool = False, |
|
| 118 | + | report: Path | None = None, |
|
| 119 | + | ): |
|
| 120 | + | self.connection = connection |
|
| 121 | + | self.compositor = connection.get(WlCompositor) |
|
| 122 | + | self.shm = connection.get(WlShm) |
|
| 123 | + | self.wm_base = connection.get(XdgWmBase) |
|
| 124 | + | self.title = title |
|
| 125 | + | self.color = color |
|
| 126 | + | self.width = 320 |
|
| 127 | + | self.height = 200 |
|
| 128 | + | self.fullscreen = False |
|
| 129 | + | self.running = True |
|
| 130 | + | self.report = report |
|
| 131 | + | self.wm_base.dispatcher["ping"] = lambda proxy, serial: proxy.pong(serial) |
|
| 132 | + | self.surface = self.compositor.create_surface() |
|
| 133 | + | self.xdg_surface = self.wm_base.get_xdg_surface(self.surface) |
|
| 134 | + | self.xdg_surface.dispatcher["configure"] = self._surface_configure |
|
| 135 | + | self.toplevel = self.xdg_surface.get_toplevel() |
|
| 136 | + | self.toplevel.dispatcher["configure"] = self._toplevel_configure |
|
| 137 | + | self.toplevel.dispatcher["close"] = lambda proxy: setattr(self, "running", False) |
|
| 138 | + | self.toplevel.set_title(title) |
|
| 139 | + | self.toplevel.set_app_id("swm-test") |
|
| 140 | + | if parent is not None: |
|
| 141 | + | self.toplevel.set_parent(parent) |
|
| 142 | + | if fullscreen: |
|
| 143 | + | self.toplevel.set_fullscreen(None) |
|
| 144 | + | self.surface.commit() |
|
| 145 | + | ||
| 146 | + | def _toplevel_configure(self, proxy, width: int, height: int, states) -> None: |
|
| 147 | + | if width > 0: |
|
| 148 | + | self.width = width |
|
| 149 | + | if height > 0: |
|
| 150 | + | self.height = height |
|
| 151 | + | self.fullscreen = int(XdgToplevel.state.fullscreen) in list(states) |
|
| 152 | + | self._write_report() |
|
| 153 | + | ||
| 154 | + | def _surface_configure(self, proxy, serial: int) -> None: |
|
| 155 | + | proxy.ack_configure(serial) |
|
| 156 | + | self.draw() |
|
| 157 | + | ||
| 158 | + | def _write_report(self) -> None: |
|
| 159 | + | if self.report: |
|
| 160 | + | self.report.write_text(f"{self.width} {self.height} {int(self.fullscreen)}\n") |
|
| 161 | + | ||
| 162 | + | def draw(self) -> None: |
|
| 163 | + | """Attach a fresh buffer and commit the surface.""" |
|
| 164 | + | ||
| 165 | + | buffer = create_buffer(self.shm, self.width, self.height, self.color) |
|
| 166 | + | self.surface.attach(buffer, 0, 0) |
|
| 167 | + | self.surface.damage_buffer(0, 0, self.width, self.height) |
|
| 168 | + | self.surface.commit() |
|
| 169 | + | self._write_report() |
|
| 170 | + | ||
| 171 | + | def unmap(self) -> None: |
|
| 172 | + | """Detach the surface buffer.""" |
|
| 173 | + | ||
| 174 | + | self.surface.attach(None, 0, 0) |
|
| 175 | + | self.surface.commit() |
|
| 176 | + | ||
| 177 | + | def destroy(self) -> None: |
|
| 178 | + | """Destroy the complete toplevel role.""" |
|
| 179 | + | ||
| 180 | + | self.toplevel.destroy() |
|
| 181 | + | self.xdg_surface.destroy() |
|
| 182 | + | self.surface.destroy() |
|
| 183 | + | ||
| 184 | + | ||
| 185 | + | def xdg_client(arguments: list[str]) -> None: |
|
| 186 | + | """Run one persistent XDG window.""" |
|
| 187 | + | ||
| 188 | + | color = int(arguments[0], 16) if arguments else 0xFF336699 |
|
| 189 | + | title = arguments[1] if len(arguments) > 1 else "swm-test" |
|
| 190 | + | report = Path(os.environ["SWM_CLIENT_REPORT"]) if os.environ.get("SWM_CLIENT_REPORT") else None |
|
| 191 | + | connection = Connection(WlCompositor, WlShm, XdgWmBase) |
|
| 192 | + | window = Window( |
|
| 193 | + | connection, |
|
| 194 | + | title, |
|
| 195 | + | color, |
|
| 196 | + | fullscreen="fullscreen" in arguments[2:], |
|
| 197 | + | report=report, |
|
| 198 | + | ) |
|
| 199 | + | while window.running: |
|
| 200 | + | connection.display.dispatch(block=True) |
|
| 201 | + | window.destroy() |
|
| 202 | + | connection.close() |
|
| 203 | + | ||
| 204 | + | ||
| 205 | + | def xdg_lifecycle(arguments: list[str]) -> None: |
|
| 206 | + | """Repeatedly map and unmap one XDG toplevel.""" |
|
| 207 | + | ||
| 208 | + | count = int(arguments[0]) if arguments else 8 |
|
| 209 | + | connection = Connection(WlCompositor, WlShm, XdgWmBase) |
|
| 210 | + | window = Window(connection, "swm-xdg-lifecycle", 0xFF224466) |
|
| 211 | + | connection.roundtrips(3) |
|
| 212 | + | for index in range(count): |
|
| 213 | + | window.unmap() |
|
| 214 | + | connection.roundtrips(2) |
|
| 215 | + | window.color += 0x00050311 |
|
| 216 | + | window.surface.commit() |
|
| 217 | + | connection.roundtrips(2) |
|
| 218 | + | window.destroy() |
|
| 219 | + | connection.close() |
|
| 220 | + | ||
| 221 | + | ||
| 222 | + | def transient(arguments: list[str]) -> None: |
|
| 223 | + | """Exercise popup, parented, and fullscreen XDG surfaces.""" |
|
| 224 | + | ||
| 225 | + | count = int(arguments[0]) if arguments else 4 |
|
| 226 | + | connection = Connection(WlCompositor, WlShm, XdgWmBase) |
|
| 227 | + | parent = Window(connection, "swm-child-parent", 0xFF335577) |
|
| 228 | + | fullscreen = Window(connection, "swm-fullscreen", 0xFF224466, fullscreen=True) |
|
| 229 | + | connection.roundtrips(4) |
|
| 230 | + | if not fullscreen.fullscreen: |
|
| 231 | + | raise Failure("fullscreen window was not configured fullscreen") |
|
| 232 | + | ||
| 233 | + | positioner = connection.get(XdgWmBase).create_positioner() |
|
| 234 | + | positioner.set_size(120, 60) |
|
| 235 | + | positioner.set_anchor_rect(30, 20, 10, 10) |
|
| 236 | + | positioner.set_anchor(XdgPositioner.anchor.bottom_right) |
|
| 237 | + | positioner.set_gravity(XdgPositioner.gravity.bottom_right) |
|
| 238 | + | positioner.set_constraint_adjustment( |
|
| 239 | + | XdgPositioner.constraint_adjustment.slide_x |
|
| 240 | + | | XdgPositioner.constraint_adjustment.slide_y |
|
| 241 | + | ) |
|
| 242 | + | popup_surface = connection.get(WlCompositor).create_surface() |
|
| 243 | + | popup_xdg = connection.get(XdgWmBase).get_xdg_surface(popup_surface) |
|
| 244 | + | popup_configured = [] |
|
| 245 | + | ||
| 246 | + | def configure(proxy, serial) -> None: |
|
| 247 | + | proxy.ack_configure(serial) |
|
| 248 | + | buffer = create_buffer(connection.get(WlShm), 120, 60, 0xFF557733) |
|
| 249 | + | popup_surface.attach(buffer, 0, 0) |
|
| 250 | + | popup_surface.damage_buffer(0, 0, 120, 60) |
|
| 251 | + | popup_surface.commit() |
|
| 252 | + | popup_configured.append(True) |
|
| 253 | + | ||
| 254 | + | popup_xdg.dispatcher["configure"] = configure |
|
| 255 | + | popup = popup_xdg.get_popup(parent.xdg_surface, positioner) |
|
| 256 | + | popup.dispatcher["configure"] = lambda proxy, x, y, width, height: None |
|
| 257 | + | popup.dispatcher["popup_done"] = lambda proxy: None |
|
| 258 | + | popup.dispatcher["repositioned"] = lambda proxy, token: None |
|
| 259 | + | positioner.destroy() |
|
| 260 | + | popup_surface.commit() |
|
| 261 | + | connection.roundtrips(4) |
|
| 262 | + | if not popup_configured: |
|
| 263 | + | raise Failure("transient popup was not configured") |
|
| 264 | + | popup.destroy() |
|
| 265 | + | popup_xdg.destroy() |
|
| 266 | + | popup_surface.destroy() |
|
| 267 | + | connection.roundtrips() |
|
| 268 | + | ||
| 269 | + | for index in range(count): |
|
| 270 | + | child = Window( |
|
| 271 | + | connection, |
|
| 272 | + | f"swm-child-{index}", |
|
| 273 | + | 0xFF773355 + index * 0x0003070D, |
|
| 274 | + | parent=parent.toplevel, |
|
| 275 | + | ) |
|
| 276 | + | connection.roundtrips(3) |
|
| 277 | + | if not fullscreen.fullscreen: |
|
| 278 | + | raise Failure("transient child stole fullscreen") |
|
| 279 | + | child.destroy() |
|
| 280 | + | connection.roundtrips() |
|
| 281 | + | fullscreen.destroy() |
|
| 282 | + | parent.destroy() |
|
| 283 | + | connection.close() |
|
| 284 | + | ||
| 285 | + | ||
| 286 | + | def keymap_text() -> tuple[bytes, dict[str, int]]: |
|
| 287 | + | """Build a default XKB keymap and return modifier indices.""" |
|
| 288 | + | ||
| 289 | + | xkb = ctypes.CDLL("libxkbcommon.so.0") |
|
| 290 | + | xkb.xkb_context_new.argtypes = [ctypes.c_int] |
|
| 291 | + | xkb.xkb_context_new.restype = ctypes.c_void_p |
|
| 292 | + | xkb.xkb_keymap_new_from_names.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int] |
|
| 293 | + | xkb.xkb_keymap_new_from_names.restype = ctypes.c_void_p |
|
| 294 | + | xkb.xkb_keymap_get_as_string.argtypes = [ctypes.c_void_p, ctypes.c_int] |
|
| 295 | + | xkb.xkb_keymap_get_as_string.restype = ctypes.c_void_p |
|
| 296 | + | xkb.xkb_keymap_mod_get_index.argtypes = [ctypes.c_void_p, ctypes.c_char_p] |
|
| 297 | + | xkb.xkb_keymap_mod_get_index.restype = ctypes.c_uint |
|
| 298 | + | xkb.xkb_keymap_unref.argtypes = [ctypes.c_void_p] |
|
| 299 | + | xkb.xkb_context_unref.argtypes = [ctypes.c_void_p] |
|
| 300 | + | context = xkb.xkb_context_new(0) |
|
| 301 | + | keymap = xkb.xkb_keymap_new_from_names(context, None, 0) |
|
| 302 | + | pointer = xkb.xkb_keymap_get_as_string(keymap, 1) |
|
| 303 | + | text = ctypes.string_at(pointer) |
|
| 304 | + | ctypes.CDLL(None).free(ctypes.c_void_p(pointer)) |
|
| 305 | + | indices = { |
|
| 306 | + | "shift": xkb.xkb_keymap_mod_get_index(keymap, b"Shift"), |
|
| 307 | + | "ctrl": xkb.xkb_keymap_mod_get_index(keymap, b"Control"), |
|
| 308 | + | "alt": xkb.xkb_keymap_mod_get_index(keymap, b"Mod1"), |
|
| 309 | + | "logo": xkb.xkb_keymap_mod_get_index(keymap, b"Mod4"), |
|
| 310 | + | } |
|
| 311 | + | xkb.xkb_keymap_unref(keymap) |
|
| 312 | + | xkb.xkb_context_unref(context) |
|
| 313 | + | return text, indices |
|
| 314 | + | ||
| 315 | + | ||
| 316 | + | def keyboard(arguments: list[str]) -> None: |
|
| 317 | + | """Send a key and modifiers through virtual-keyboard-v1.""" |
|
| 318 | + | ||
| 319 | + | if len(arguments) != 2: |
|
| 320 | + | raise Failure("keyboard requires KEYCODE|none MODIFIERS") |
|
| 321 | + | key = None if arguments[0] == "none" else int(arguments[0], 0) |
|
| 322 | + | requested = int(arguments[1], 0) |
|
| 323 | + | connection = Connection(WlSeat, ZwpVirtualKeyboardManagerV1) |
|
| 324 | + | seat = connection.get(WlSeat) |
|
| 325 | + | manager = connection.get(ZwpVirtualKeyboardManagerV1) |
|
| 326 | + | virtual = manager.create_virtual_keyboard(seat) |
|
| 327 | + | text, indices = keymap_text() |
|
| 328 | + | with tempfile.TemporaryFile(dir=os.environ.get("SWM_TEST_DIR")) as stream: |
|
| 329 | + | stream.write(text + b"\0") |
|
| 330 | + | stream.flush() |
|
| 331 | + | virtual.keymap(WlKeyboard.keymap_format.xkb_v1, stream.fileno(), len(text) + 1) |
|
| 332 | + | modifiers = 0 |
|
| 333 | + | modifier_keys = ( |
|
| 334 | + | (1, "shift", 42), |
|
| 335 | + | (4, "ctrl", 29), |
|
| 336 | + | (8, "alt", 56), |
|
| 337 | + | (64, "logo", 125), |
|
| 338 | + | ) |
|
| 339 | + | for flag, name, code in modifier_keys: |
|
| 340 | + | if requested & flag: |
|
| 341 | + | modifiers |= 1 << indices[name] |
|
| 342 | + | virtual.key(0, code, WlKeyboard.key_state.pressed) |
|
| 343 | + | virtual.modifiers(modifiers, 0, 0, 0) |
|
| 344 | + | if key is not None: |
|
| 345 | + | virtual.key(1, key, WlKeyboard.key_state.pressed) |
|
| 346 | + | virtual.key(2, key, WlKeyboard.key_state.released) |
|
| 347 | + | connection.roundtrips() |
|
| 348 | + | ready = os.environ.get("SWM_KEYBOARD_READY") |
|
| 349 | + | if ready: |
|
| 350 | + | Path(ready).write_text("ready\n") |
|
| 351 | + | release = os.environ.get("SWM_KEYBOARD_RELEASE") |
|
| 352 | + | if release: |
|
| 353 | + | deadline = time.monotonic() + 2 |
|
| 354 | + | while not Path(release).exists(): |
|
| 355 | + | if time.monotonic() >= deadline: |
|
| 356 | + | raise Failure("timed out waiting to release virtual keyboard") |
|
| 357 | + | time.sleep(0.002) |
|
| 358 | + | for flag, _, code in modifier_keys: |
|
| 359 | + | if requested & flag: |
|
| 360 | + | virtual.key(3, code, WlKeyboard.key_state.released) |
|
| 361 | + | virtual.modifiers(0, 0, 0, 0) |
|
| 362 | + | virtual.destroy() |
|
| 363 | + | connection.roundtrips() |
|
| 364 | + | connection.close() |
|
| 365 | + | ||
| 366 | + | ||
| 367 | + | def pointer(arguments: list[str]) -> None: |
|
| 368 | + | """Move and click through virtual-pointer-v1.""" |
|
| 369 | + | ||
| 370 | + | x = int(arguments[0]) |
|
| 371 | + | button = 0x111 if len(arguments) > 1 and arguments[1] == "right" else 0x110 |
|
| 372 | + | connection = Connection(WlSeat, ZwlrVirtualPointerManagerV1) |
|
| 373 | + | manager = connection.get(ZwlrVirtualPointerManagerV1) |
|
| 374 | + | virtual = manager.create_virtual_pointer(connection.get(WlSeat)) |
|
| 375 | + | virtual.motion_absolute(0, x, 500, 1000, 1000) |
|
| 376 | + | virtual.frame() |
|
| 377 | + | virtual.button(1, button, 1) |
|
| 378 | + | virtual.motion(2, 25.0, 20.0) |
|
| 379 | + | virtual.frame() |
|
| 380 | + | virtual.button(3, button, 0) |
|
| 381 | + | virtual.axis_source(0) |
|
| 382 | + | virtual.axis_discrete(3, 0, 1.0, 1) |
|
| 383 | + | virtual.axis_stop(4, 0) |
|
| 384 | + | virtual.frame() |
|
| 385 | + | connection.roundtrips() |
|
| 386 | + | virtual.destroy() |
|
| 387 | + | connection.close() |
|
| 388 | + | ||
| 389 | + | ||
| 390 | + | def foreign(arguments: list[str]) -> None: |
|
| 391 | + | """Apply one foreign-toplevel action to a named window.""" |
|
| 392 | + | ||
| 393 | + | title, action = arguments |
|
| 394 | + | connection = Connection(WlSeat, ZwlrForeignToplevelManagerV1) |
|
| 395 | + | seat = connection.get(WlSeat) |
|
| 396 | + | manager = connection.get(ZwlrForeignToplevelManagerV1) |
|
| 397 | + | acted = False |
|
| 398 | + | observed = False |
|
| 399 | + | ||
| 400 | + | def toplevel(proxy, handle): |
|
| 401 | + | nonlocal acted, observed |
|
| 402 | + | state = {"title": "", "fullscreen": False} |
|
| 403 | + | fullscreen_state = int(handle.interface.state.fullscreen) |
|
| 404 | + | handle.dispatcher["title"] = lambda p, value: state.update(title=value) |
|
| 405 | + | ||
| 406 | + | def states(p, values): |
|
| 407 | + | nonlocal acted, observed |
|
| 408 | + | state["fullscreen"] = fullscreen_state in list(values) |
|
| 409 | + | if state["title"] != title: |
|
| 410 | + | return |
|
| 411 | + | if not acted: |
|
| 412 | + | if action == "activate": |
|
| 413 | + | handle.activate(seat) |
|
| 414 | + | elif action == "fullscreen": |
|
| 415 | + | handle.set_fullscreen(None) |
|
| 416 | + | elif action == "unfullscreen": |
|
| 417 | + | handle.unset_fullscreen() |
|
| 418 | + | elif action == "close": |
|
| 419 | + | handle.close() |
|
| 420 | + | else: |
|
| 421 | + | raise Failure(f"unknown foreign action: {action}") |
|
| 422 | + | acted = True |
|
| 423 | + | observed = ( |
|
| 424 | + | action == "close" |
|
| 425 | + | or action == "fullscreen" and state["fullscreen"] |
|
| 426 | + | or action in {"activate", "unfullscreen"} and not state["fullscreen"] |
|
| 427 | + | ) |
|
| 428 | + | ||
| 429 | + | handle.dispatcher["state"] = states |
|
| 430 | + | handle.dispatcher["done"] = lambda p: states( |
|
| 431 | + | p, [fullscreen_state] if state["fullscreen"] else [] |
|
| 432 | + | ) |
|
| 433 | + | handle.dispatcher["closed"] = lambda p: None |
|
| 434 | + | ||
| 435 | + | manager.dispatcher["toplevel"] = toplevel |
|
| 436 | + | for _ in range(20): |
|
| 437 | + | connection.roundtrips() |
|
| 438 | + | if acted and observed: |
|
| 439 | + | break |
|
| 440 | + | if not acted or not observed: |
|
| 441 | + | raise Failure(f"foreign-toplevel action {action} was not observed") |
|
| 442 | + | if action == "close": |
|
| 443 | + | connection.roundtrips(2) |
|
| 444 | + | connection.close() |
|
| 445 | + | ||
| 446 | + | ||
| 447 | + | def workspace(arguments: list[str]) -> None: |
|
| 448 | + | """Activate a named workspace through ext-workspace-v1.""" |
|
| 449 | + | ||
| 450 | + | if len(arguments) != 1: |
|
| 451 | + | raise Failure("workspace requires a workspace name") |
|
| 452 | + | target = arguments[0] |
|
| 453 | + | connection = Connection(ExtWorkspaceManagerV1) |
|
| 454 | + | manager = connection.get(ExtWorkspaceManagerV1) |
|
| 455 | + | workspaces: dict[str, object] = {} |
|
| 456 | + | ||
| 457 | + | def announced(proxy, handle): |
|
| 458 | + | handle.dispatcher["name"] = lambda p, name: workspaces.update({name: handle}) |
|
| 459 | + | handle.dispatcher["removed"] = lambda p: None |
|
| 460 | + | ||
| 461 | + | manager.dispatcher["workspace_group"] = lambda proxy, group: None |
|
| 462 | + | manager.dispatcher["workspace"] = announced |
|
| 463 | + | manager.dispatcher["done"] = lambda proxy: None |
|
| 464 | + | manager.dispatcher["finished"] = lambda proxy: None |
|
| 465 | + | connection.roundtrips(2) |
|
| 466 | + | if target not in workspaces: |
|
| 467 | + | raise Failure(f"workspace {target!r} was not advertised") |
|
| 468 | + | workspaces[target].activate() |
|
| 469 | + | manager.commit() |
|
| 470 | + | connection.roundtrips(2) |
|
| 471 | + | connection.close() |
|
| 472 | + | ||
| 473 | + | ||
| 474 | + | def output_power(arguments: list[str]) -> None: |
|
| 475 | + | """Turn one output off and back on through output-power-v1.""" |
|
| 476 | + | ||
| 477 | + | connection = Connection(WlOutput, ZwlrOutputPowerManagerV1) |
|
| 478 | + | power = connection.get(ZwlrOutputPowerManagerV1).get_output_power(connection.get(WlOutput)) |
|
| 479 | + | modes: list[int] = [] |
|
| 480 | + | failed: list[bool] = [] |
|
| 481 | + | power.dispatcher["mode"] = lambda proxy, mode: modes.append(mode) |
|
| 482 | + | power.dispatcher["failed"] = lambda proxy: failed.append(True) |
|
| 483 | + | connection.roundtrips() |
|
| 484 | + | if not modes or modes[-1] != 1: |
|
| 485 | + | raise Failure("output did not begin powered on") |
|
| 486 | + | power.set_mode(0) |
|
| 487 | + | connection.roundtrips(2) |
|
| 488 | + | if modes[-1] != 0: |
|
| 489 | + | raise Failure("output did not power off") |
|
| 490 | + | power.set_mode(1) |
|
| 491 | + | connection.roundtrips(2) |
|
| 492 | + | if modes[-1] != 1 or failed: |
|
| 493 | + | raise Failure("output did not power back on") |
|
| 494 | + | power.destroy() |
|
| 495 | + | connection.close() |
|
| 496 | + | ||
| 497 | + | ||
| 498 | + | def output_management(arguments: list[str]) -> None: |
|
| 499 | + | """Exercise output-management through the installed reference client.""" |
|
| 500 | + | ||
| 501 | + | command = ["wlr-randr"] |
|
| 502 | + | if arguments and arguments[0] in {"disable-first", "disable-second"}: |
|
| 503 | + | output = "HEADLESS-1" if arguments[0] == "disable-first" else "HEADLESS-2" |
|
| 504 | + | command += ["--output", output, "--off"] |
|
| 505 | + | else: |
|
| 506 | + | command += ["--output", "HEADLESS-1", "--on"] |
|
| 507 | + | subprocess.run([*command, "--dryrun"], check=True) |
|
| 508 | + | subprocess.run(command, check=True) |
|
| 509 | + | ||
| 510 | + | ||
| 511 | + | def layer(arguments: list[str]) -> None: |
|
| 512 | + | """Create, configure, map, and destroy layer-shell surfaces.""" |
|
| 513 | + | ||
| 514 | + | count = int(arguments[0]) if arguments else 2 |
|
| 515 | + | connection = Connection(WlCompositor, WlShm, ZwlrLayerShellV1) |
|
| 516 | + | compositor = connection.get(WlCompositor) |
|
| 517 | + | shm = connection.get(WlShm) |
|
| 518 | + | shell = connection.get(ZwlrLayerShellV1) |
|
| 519 | + | layers = [] |
|
| 520 | + | for index in range(count): |
|
| 521 | + | surface = compositor.create_surface() |
|
| 522 | + | role = shell.get_layer_surface( |
|
| 523 | + | surface, |
|
| 524 | + | None, |
|
| 525 | + | ZwlrLayerShellV1.layer.top, |
|
| 526 | + | f"swm-layer-{index}", |
|
| 527 | + | ) |
|
| 528 | + | configured = [] |
|
| 529 | + | ||
| 530 | + | def configure( |
|
| 531 | + | proxy, |
|
| 532 | + | serial, |
|
| 533 | + | width, |
|
| 534 | + | height, |
|
| 535 | + | surface=surface, |
|
| 536 | + | configured=configured, |
|
| 537 | + | index=index, |
|
| 538 | + | ): |
|
| 539 | + | proxy.ack_configure(serial) |
|
| 540 | + | width = width or 80 |
|
| 541 | + | height = height or 40 |
|
| 542 | + | surface.attach(create_buffer(shm, width, height, 0xCC112233 + index), 0, 0) |
|
| 543 | + | surface.damage_buffer(0, 0, width, height) |
|
| 544 | + | surface.commit() |
|
| 545 | + | configured.append(True) |
|
| 546 | + | ||
| 547 | + | role.dispatcher["configure"] = configure |
|
| 548 | + | role.dispatcher["closed"] = lambda proxy: None |
|
| 549 | + | role.set_size(80 + index, 40 + index) |
|
| 550 | + | role.set_anchor(1 << (index % 4)) |
|
| 551 | + | surface.commit() |
|
| 552 | + | layers.append((surface, role, configured)) |
|
| 553 | + | connection.roundtrips(3) |
|
| 554 | + | if not all(configured for _, _, configured in layers): |
|
| 555 | + | raise Failure("layer surface was not configured") |
|
| 556 | + | for surface, role, _ in reversed(layers): |
|
| 557 | + | role.destroy() |
|
| 558 | + | surface.destroy() |
|
| 559 | + | connection.roundtrips() |
|
| 560 | + | connection.close() |
|
| 561 | + | ||
| 562 | + | ||
| 563 | + | def session_lock(arguments: list[str]) -> None: |
|
| 564 | + | """Create and release session locks.""" |
|
| 565 | + | ||
| 566 | + | count = int(arguments[0]) if arguments else 2 |
|
| 567 | + | connection = Connection(WlCompositor, WlShm, WlOutput, ExtSessionLockManagerV1) |
|
| 568 | + | compositor = connection.get(WlCompositor) |
|
| 569 | + | shm = connection.get(WlShm) |
|
| 570 | + | output = connection.get(WlOutput) |
|
| 571 | + | manager = connection.get(ExtSessionLockManagerV1) |
|
| 572 | + | for index in range(count): |
|
| 573 | + | lock = manager.lock() |
|
| 574 | + | locked: list[bool] = [] |
|
| 575 | + | finished: list[bool] = [] |
|
| 576 | + | lock.dispatcher["locked"] = lambda proxy: locked.append(True) |
|
| 577 | + | lock.dispatcher["finished"] = lambda proxy: finished.append(True) |
|
| 578 | + | surface = compositor.create_surface() |
|
| 579 | + | role = lock.get_lock_surface(surface, output) |
|
| 580 | + | configured: list[bool] = [] |
|
| 581 | + | ||
| 582 | + | def configure(proxy, serial, width, height): |
|
| 583 | + | proxy.ack_configure(serial) |
|
| 584 | + | surface.attach(create_buffer(shm, width, height, 0xFF101010 + index), 0, 0) |
|
| 585 | + | surface.damage_buffer(0, 0, width, height) |
|
| 586 | + | surface.commit() |
|
| 587 | + | configured.append(True) |
|
| 588 | + | ||
| 589 | + | role.dispatcher["configure"] = configure |
|
| 590 | + | connection.roundtrips(3) |
|
| 591 | + | if not finished and (not locked or not configured): |
|
| 592 | + | raise Failure("session lock was not configured") |
|
| 593 | + | role.destroy() |
|
| 594 | + | surface.destroy() |
|
| 595 | + | if locked: |
|
| 596 | + | lock.unlock_and_destroy() |
|
| 597 | + | else: |
|
| 598 | + | lock.destroy() |
|
| 599 | + | connection.roundtrips() |
|
| 600 | + | connection.close() |
|
| 601 | + | ||
| 602 | + | ||
| 603 | + | def text_input(arguments: list[str]) -> None: |
|
| 604 | + | """Exercise text input, input methods, and input inhibitors together.""" |
|
| 605 | + | ||
| 606 | + | connection = Connection( |
|
| 607 | + | WlCompositor, |
|
| 608 | + | WlShm, |
|
| 609 | + | WlSeat, |
|
| 610 | + | XdgWmBase, |
|
| 611 | + | ZwpTextInputManagerV3, |
|
| 612 | + | ZwpInputMethodManagerV2, |
|
| 613 | + | ZwpKeyboardShortcutsInhibitManagerV1, |
|
| 614 | + | ZwpPointerConstraintsV1, |
|
| 615 | + | ZwpRelativePointerManagerV1, |
|
| 616 | + | ZwpIdleInhibitManagerV1, |
|
| 617 | + | ZwlrVirtualPointerManagerV1, |
|
| 618 | + | ) |
|
| 619 | + | seat = connection.get(WlSeat) |
|
| 620 | + | pointer = seat.get_pointer() |
|
| 621 | + | for event in ("enter", "leave", "motion", "button", "axis"): |
|
| 622 | + | pointer.dispatcher[event] = lambda proxy, *values: None |
|
| 623 | + | ||
| 624 | + | text = connection.get(ZwpTextInputManagerV3).get_text_input(seat) |
|
| 625 | + | method = connection.get(ZwpInputMethodManagerV2).get_input_method(seat) |
|
| 626 | + | state = { |
|
| 627 | + | "activated": False, |
|
| 628 | + | "surrounding": False, |
|
| 629 | + | "cause": False, |
|
| 630 | + | "content": False, |
|
| 631 | + | "preedit": False, |
|
| 632 | + | "commit": False, |
|
| 633 | + | "delete": False, |
|
| 634 | + | "done": False, |
|
| 635 | + | "rectangle": False, |
|
| 636 | + | "shortcuts": False, |
|
| 637 | + | "locked": False, |
|
| 638 | + | "relative": False, |
|
| 639 | + | "serial": 0, |
|
| 640 | + | } |
|
| 641 | + | ||
| 642 | + | def text_enter(proxy, surface) -> None: |
|
| 643 | + | if surface != window.surface: |
|
| 644 | + | return |
|
| 645 | + | text.enable() |
|
| 646 | + | text.set_surrounding_text("before after", 6, 6) |
|
| 647 | + | text.set_text_change_cause(1) |
|
| 648 | + | text.set_content_type(1, 0) |
|
| 649 | + | text.set_cursor_rectangle(7, 11, 3, 15) |
|
| 650 | + | text.commit() |
|
| 651 | + | ||
| 652 | + | def method_done(proxy) -> None: |
|
| 653 | + | state["serial"] += 1 |
|
| 654 | + | if not all(state[name] for name in ("activated", "surrounding", "cause", "content")): |
|
| 655 | + | return |
|
| 656 | + | method.set_preedit_string("preedit", 2, 4) |
|
| 657 | + | method.commit_string("committed") |
|
| 658 | + | method.delete_surrounding_text(2, 3) |
|
| 659 | + | method.commit(state["serial"]) |
|
| 660 | + | ||
| 661 | + | text.dispatcher["enter"] = text_enter |
|
| 662 | + | text.dispatcher["leave"] = lambda proxy, surface: None |
|
| 663 | + | text.dispatcher["preedit_string"] = lambda proxy, value, begin, end: state.update( |
|
| 664 | + | preedit=value == "preedit" and begin == 2 and end == 4 |
|
| 665 | + | ) |
|
| 666 | + | text.dispatcher["commit_string"] = lambda proxy, value: state.update( |
|
| 667 | + | commit=value == "committed" |
|
| 668 | + | ) |
|
| 669 | + | text.dispatcher["delete_surrounding_text"] = lambda proxy, before, after: state.update( |
|
| 670 | + | delete=before == 2 and after == 3 |
|
| 671 | + | ) |
|
| 672 | + | text.dispatcher["done"] = lambda proxy, serial: state.update(done=True) |
|
| 673 | + | ||
| 674 | + | popup_surface = connection.get(WlCompositor).create_surface() |
|
| 675 | + | popup = method.get_input_popup_surface(popup_surface) |
|
| 676 | + | popup.dispatcher["text_input_rectangle"] = lambda proxy, x, y, width, height: state.update( |
|
| 677 | + | rectangle=(x, y, width, height) == (0, -15, 3, 15) |
|
| 678 | + | ) |
|
| 679 | + | ||
| 680 | + | def activate(proxy) -> None: |
|
| 681 | + | state["activated"] = True |
|
| 682 | + | buffer = create_buffer(connection.get(WlShm), 32, 16, 0xFF8855AA) |
|
| 683 | + | popup_surface.attach(buffer, 0, 0) |
|
| 684 | + | popup_surface.damage_buffer(0, 0, 32, 16) |
|
| 685 | + | popup_surface.commit() |
|
| 686 | + | ||
| 687 | + | method.dispatcher["activate"] = activate |
|
| 688 | + | method.dispatcher["deactivate"] = lambda proxy: None |
|
| 689 | + | method.dispatcher["surrounding_text"] = lambda proxy, value, cursor, anchor: state.update( |
|
| 690 | + | surrounding=value == "before after" and cursor == 6 and anchor == 6 |
|
| 691 | + | ) |
|
| 692 | + | method.dispatcher["text_change_cause"] = lambda proxy, cause: state.update(cause=cause == 1) |
|
| 693 | + | method.dispatcher["content_type"] = lambda proxy, hint, purpose: state.update( |
|
| 694 | + | content=hint == 1 and purpose == 0 |
|
| 695 | + | ) |
|
| 696 | + | method.dispatcher["done"] = method_done |
|
| 697 | + | method.dispatcher["unavailable"] = lambda proxy: (_ for _ in ()).throw( |
|
| 698 | + | Failure("input method is unavailable") |
|
| 699 | + | ) |
|
| 700 | + | ||
| 701 | + | window = Window(connection, "text-input", 0xFF556677) |
|
| 702 | + | idle = connection.get(ZwpIdleInhibitManagerV1).create_inhibitor(window.surface) |
|
| 703 | + | shortcuts = connection.get(ZwpKeyboardShortcutsInhibitManagerV1).inhibit_shortcuts( |
|
| 704 | + | window.surface, seat |
|
| 705 | + | ) |
|
| 706 | + | shortcuts.dispatcher["active"] = lambda proxy: state.update(shortcuts=True) |
|
| 707 | + | shortcuts.dispatcher["inactive"] = lambda proxy: state.update(shortcuts=False) |
|
| 708 | + | locked = connection.get(ZwpPointerConstraintsV1).lock_pointer( |
|
| 709 | + | window.surface, pointer, None, 2 |
|
| 710 | + | ) |
|
| 711 | + | locked.dispatcher["locked"] = lambda proxy: state.update(locked=True) |
|
| 712 | + | locked.dispatcher["unlocked"] = lambda proxy: state.update(locked=False) |
|
| 713 | + | relative = connection.get(ZwpRelativePointerManagerV1).get_relative_pointer(pointer) |
|
| 714 | + | relative.dispatcher["relative_motion"] = lambda proxy, hi, lo, dx, dy, ux, uy: state.update( |
|
| 715 | + | relative=dx != 0 |
|
| 716 | + | ) |
|
| 717 | + | virtual = connection.get(ZwlrVirtualPointerManagerV1).create_virtual_pointer(seat) |
|
| 718 | + | ||
| 719 | + | for index in range(100): |
|
| 720 | + | virtual.motion_absolute(0, index % 10 * 100 + 50, index // 10 * 100 + 50, 1000, 1000) |
|
| 721 | + | virtual.frame() |
|
| 722 | + | connection.roundtrips() |
|
| 723 | + | if state["locked"]: |
|
| 724 | + | break |
|
| 725 | + | virtual.motion(10, 5.0, 0.0) |
|
| 726 | + | virtual.frame() |
|
| 727 | + | connection.roundtrips(3) |
|
| 728 | + | ||
| 729 | + | required = ( |
|
| 730 | + | "preedit", |
|
| 731 | + | "commit", |
|
| 732 | + | "delete", |
|
| 733 | + | "done", |
|
| 734 | + | "rectangle", |
|
| 735 | + | "shortcuts", |
|
| 736 | + | "locked", |
|
| 737 | + | "relative", |
|
| 738 | + | ) |
|
| 739 | + | missing = [name for name in required if not state[name]] |
|
| 740 | + | if missing: |
|
| 741 | + | raise Failure(f"text-input events not observed: {', '.join(missing)}") |
|
| 742 | + | ||
| 743 | + | text.disable() |
|
| 744 | + | text.commit() |
|
| 745 | + | connection.roundtrips(2) |
|
| 746 | + | window.destroy() |
|
| 747 | + | virtual.destroy() |
|
| 748 | + | relative.destroy() |
|
| 749 | + | locked.destroy() |
|
| 750 | + | shortcuts.destroy() |
|
| 751 | + | idle.destroy() |
|
| 752 | + | popup.destroy() |
|
| 753 | + | popup_surface.destroy() |
|
| 754 | + | text.destroy() |
|
| 755 | + | method.destroy() |
|
| 756 | + | connection.close() |
|
| 757 | + | ||
| 758 | + | ||
| 759 | + | def x11(arguments: list[str]) -> None: |
|
| 760 | + | """Exercise managed, configured, fullscreen, and unmanaged X11 windows.""" |
|
| 761 | + | ||
| 762 | + | import xcffib |
|
| 763 | + | import xcffib.xproto |
|
| 764 | + | ||
| 765 | + | connection = xcffib.connect() |
|
| 766 | + | setup = connection.get_setup() |
|
| 767 | + | screen = setup.roots[connection.pref_screen] |
|
| 768 | + | window = connection.generate_id() |
|
| 769 | + | mask = xcffib.xproto.CW.BackPixel | xcffib.xproto.CW.EventMask |
|
| 770 | + | values = [screen.white_pixel, xcffib.xproto.EventMask.StructureNotify] |
|
| 771 | + | connection.core.CreateWindow( |
|
| 772 | + | screen.root_depth, |
|
| 773 | + | window, |
|
| 774 | + | screen.root, |
|
| 775 | + | 30, |
|
| 776 | + | 30, |
|
| 777 | + | 220, |
|
| 778 | + | 140, |
|
| 779 | + | 0, |
|
| 780 | + | xcffib.xproto.WindowClass.InputOutput, |
|
| 781 | + | screen.root_visual, |
|
| 782 | + | mask, |
|
| 783 | + | values, |
|
| 784 | + | ) |
|
| 785 | + | wm_name = connection.core.InternAtom(False, len("WM_NAME"), "WM_NAME").reply().atom |
|
| 786 | + | string = connection.core.InternAtom(False, len("STRING"), "STRING").reply().atom |
|
| 787 | + | title = b"swm-xwayland-test" |
|
| 788 | + | connection.core.ChangeProperty( |
|
| 789 | + | xcffib.xproto.PropMode.Replace, |
|
| 790 | + | window, |
|
| 791 | + | wm_name, |
|
| 792 | + | string, |
|
| 793 | + | 8, |
|
| 794 | + | len(title), |
|
| 795 | + | title, |
|
| 796 | + | ) |
|
| 797 | + | wm_class = connection.core.InternAtom(False, len("WM_CLASS"), "WM_CLASS").reply().atom |
|
| 798 | + | class_name = b"x11-test\0X11-Test\0" |
|
| 799 | + | connection.core.ChangeProperty( |
|
| 800 | + | xcffib.xproto.PropMode.Replace, |
|
| 801 | + | window, |
|
| 802 | + | wm_class, |
|
| 803 | + | string, |
|
| 804 | + | 8, |
|
| 805 | + | len(class_name), |
|
| 806 | + | class_name, |
|
| 807 | + | ) |
|
| 808 | + | connection.core.MapWindow(window) |
|
| 809 | + | configure = ( |
|
| 810 | + | xcffib.xproto.ConfigWindow.X |
|
| 811 | + | | xcffib.xproto.ConfigWindow.Y |
|
| 812 | + | | xcffib.xproto.ConfigWindow.Width |
|
| 813 | + | | xcffib.xproto.ConfigWindow.Height |
|
| 814 | + | ) |
|
| 815 | + | connection.core.ConfigureWindow(window, configure, [40, 50, 360, 240]) |
|
| 816 | + | connection.flush() |
|
| 817 | + | for _ in range(100): |
|
| 818 | + | event = connection.poll_for_event() |
|
| 819 | + | if isinstance(event, xcffib.xproto.MapNotifyEvent): |
|
| 820 | + | break |
|
| 821 | + | time.sleep(0.01) |
|
| 822 | + | else: |
|
| 823 | + | raise Failure("XWayland window was not mapped") |
|
| 824 | + | ||
| 825 | + | connection.core.ConfigureWindow(window, configure, [70, 80, 400, 260]) |
|
| 826 | + | connection.core.GetInputFocus().reply() |
|
| 827 | + | ||
| 828 | + | unmanaged = connection.generate_id() |
|
| 829 | + | unmanaged_mask = xcffib.xproto.CW.OverrideRedirect | xcffib.xproto.CW.EventMask |
|
| 830 | + | connection.core.CreateWindow( |
|
| 831 | + | screen.root_depth, |
|
| 832 | + | unmanaged, |
|
| 833 | + | screen.root, |
|
| 834 | + | 15, |
|
| 835 | + | 25, |
|
| 836 | + | 180, |
|
| 837 | + | 100, |
|
| 838 | + | 0, |
|
| 839 | + | xcffib.xproto.WindowClass.InputOutput, |
|
| 840 | + | screen.root_visual, |
|
| 841 | + | unmanaged_mask, |
|
| 842 | + | [1, xcffib.xproto.EventMask.StructureNotify], |
|
| 843 | + | ) |
|
| 844 | + | unmanaged_title = b"x11-unmanaged" |
|
| 845 | + | connection.core.ChangeProperty( |
|
| 846 | + | xcffib.xproto.PropMode.Replace, |
|
| 847 | + | unmanaged, |
|
| 848 | + | wm_name, |
|
| 849 | + | string, |
|
| 850 | + | 8, |
|
| 851 | + | len(unmanaged_title), |
|
| 852 | + | unmanaged_title, |
|
| 853 | + | ) |
|
| 854 | + | connection.core.MapWindow(unmanaged) |
|
| 855 | + | connection.flush() |
|
| 856 | + | for _ in range(100): |
|
| 857 | + | attributes = connection.core.GetWindowAttributes(unmanaged).reply() |
|
| 858 | + | if attributes.map_state == xcffib.xproto.MapState.Viewable: |
|
| 859 | + | break |
|
| 860 | + | time.sleep(0.01) |
|
| 861 | + | else: |
|
| 862 | + | raise Failure("override-redirect XWayland window was not mapped") |
|
| 863 | + | connection.core.ConfigureWindow(unmanaged, configure, [30, 40, 220, 130]) |
|
| 864 | + | connection.core.GetInputFocus().reply() |
|
| 865 | + | ||
| 866 | + | net_state = connection.core.InternAtom( |
|
| 867 | + | False, len("_NET_WM_STATE"), "_NET_WM_STATE" |
|
| 868 | + | ).reply().atom |
|
| 869 | + | net_fullscreen = connection.core.InternAtom( |
|
| 870 | + | False, len("_NET_WM_STATE_FULLSCREEN"), "_NET_WM_STATE_FULLSCREEN" |
|
| 871 | + | ).reply().atom |
|
| 872 | + | message = xcffib.xproto.ClientMessageEvent.synthetic( |
|
| 873 | + | 32, |
|
| 874 | + | window, |
|
| 875 | + | net_state, |
|
| 876 | + | ([1, net_fullscreen, 0, 1, 0], "=5I"), |
|
| 877 | + | ) |
|
| 878 | + | event_mask = ( |
|
| 879 | + | xcffib.xproto.EventMask.SubstructureRedirect |
|
| 880 | + | | xcffib.xproto.EventMask.SubstructureNotify |
|
| 881 | + | ) |
|
| 882 | + | connection.core.SendEvent(False, screen.root, event_mask, message.pack()) |
|
| 883 | + | connection.flush() |
|
| 884 | + | connection.core.GetInputFocus().reply() |
|
| 885 | + | ||
| 886 | + | connection.core.UnmapWindow(window) |
|
| 887 | + | connection.core.UnmapWindow(unmanaged) |
|
| 888 | + | connection.core.DestroyWindow(window) |
|
| 889 | + | connection.core.DestroyWindow(unmanaged) |
|
| 890 | + | connection.flush() |
|
| 891 | + | connection.disconnect() |
|
| 892 | + | ||
| 893 | + | ||
| 894 | + | ROLES = { |
|
| 895 | + | "xdg": xdg_client, |
|
| 896 | + | "xdg-lifecycle": xdg_lifecycle, |
|
| 897 | + | "transient": transient, |
|
| 898 | + | "keyboard": keyboard, |
|
| 899 | + | "pointer": pointer, |
|
| 900 | + | "foreign": foreign, |
|
| 901 | + | "workspace": workspace, |
|
| 902 | + | "output-power": output_power, |
|
| 903 | + | "output-management": output_management, |
|
| 904 | + | "layer": layer, |
|
| 905 | + | "session-lock": session_lock, |
|
| 906 | + | "text-input": text_input, |
|
| 907 | + | "x11": x11, |
|
| 908 | + | } |
|
| 909 | + | ||
| 910 | + | ||
| 911 | + | def main() -> None: |
|
| 912 | + | """Run one selected protocol role.""" |
|
| 913 | + | ||
| 914 | + | if len(sys.argv) < 2 or sys.argv[1] not in ROLES: |
|
| 915 | + | raise Failure(f"usage: clients.py {'|'.join(ROLES)} [ARGS...]") |
|
| 916 | + | ROLES[sys.argv[1]](sys.argv[2:]) |
|
| 917 | + | ||
| 918 | + | ||
| 919 | + | if __name__ == "__main__": |
|
| 920 | + | try: |
|
| 921 | + | main() |
|
| 922 | + | except Exception as error: |
|
| 923 | + | print(error, file=sys.stderr) |
|
| 924 | + | raise SystemExit(1) |
test/run.py
added
+640 -0
| 1 | + | #!/usr/bin/env python3 |
|
| 2 | + | ||
| 3 | + | """Run swm's unit and headless integration tests.""" |
|
| 4 | + | ||
| 5 | + | from __future__ import annotations |
|
| 6 | + | ||
| 7 | + | import glob |
|
| 8 | + | import os |
|
| 9 | + | import re |
|
| 10 | + | import shlex |
|
| 11 | + | import shutil |
|
| 12 | + | import signal |
|
| 13 | + | import subprocess |
|
| 14 | + | import sys |
|
| 15 | + | import time |
|
| 16 | + | from pathlib import Path |
|
| 17 | + | ||
| 18 | + | ||
| 19 | + | SCRIPT = Path(__file__).resolve() |
|
| 20 | + | TESTDIR = SCRIPT.parent |
|
| 21 | + | ROOT = TESTDIR.parent |
|
| 22 | + | BUILD = Path(os.environ.get("SWM_TEST_BUILD", TESTDIR / ".build")).resolve() |
|
| 23 | + | SOURCE = BUILD / "source" |
|
| 24 | + | BIN = BUILD / "bin" |
|
| 25 | + | PROFILES = BUILD / "profiles" |
|
| 26 | + | LOGS = BUILD / "logs" |
|
| 27 | + | CHILDREN: list[subprocess.Popen] = [] |
|
| 28 | + | CURRENT_COMMAND: list[str] = [] |
|
| 29 | + | CURRENT_LOG: Path | None = None |
|
| 30 | + | ||
| 31 | + | ||
| 32 | + | class Failure(RuntimeError): |
|
| 33 | + | """Report a test failure without an internal traceback.""" |
|
| 34 | + | ||
| 35 | + | ||
| 36 | + | def command_text(command: list[str]) -> str: |
|
| 37 | + | """Format a command for diagnostics.""" |
|
| 38 | + | ||
| 39 | + | return shlex.join(map(str, command)) |
|
| 40 | + | ||
| 41 | + | ||
| 42 | + | def run(*args: object, env: dict[str, str] | None = None) -> str: |
|
| 43 | + | """Run a command and return its standard output.""" |
|
| 44 | + | ||
| 45 | + | global CURRENT_COMMAND |
|
| 46 | + | command = [str(arg) for arg in args] |
|
| 47 | + | CURRENT_COMMAND = command |
|
| 48 | + | result = subprocess.run(command, text=True, capture_output=True, env=env) |
|
| 49 | + | if result.returncode: |
|
| 50 | + | output = result.stdout + result.stderr |
|
| 51 | + | raise Failure( |
|
| 52 | + | f"command: {command_text(command)}\nstatus: {result.returncode}\n{output}" |
|
| 53 | + | ) |
|
| 54 | + | return result.stdout |
|
| 55 | + | ||
| 56 | + | ||
| 57 | + | def spawn(log: Path, *args: object, env: dict[str, str] | None = None) -> subprocess.Popen: |
|
| 58 | + | """Start and track a background command.""" |
|
| 59 | + | ||
| 60 | + | global CURRENT_COMMAND, CURRENT_LOG |
|
| 61 | + | command = [str(arg) for arg in args] |
|
| 62 | + | CURRENT_COMMAND = command |
|
| 63 | + | CURRENT_LOG = log |
|
| 64 | + | log.parent.mkdir(parents=True, exist_ok=True) |
|
| 65 | + | stream = log.open("w") |
|
| 66 | + | process = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT, env=env) |
|
| 67 | + | stream.close() |
|
| 68 | + | CHILDREN.append(process) |
|
| 69 | + | return process |
|
| 70 | + | ||
| 71 | + | ||
| 72 | + | def terminate(process: subprocess.Popen) -> None: |
|
| 73 | + | """Terminate one tracked process.""" |
|
| 74 | + | ||
| 75 | + | if process.poll() is None: |
|
| 76 | + | process.terminate() |
|
| 77 | + | try: |
|
| 78 | + | process.wait(timeout=1) |
|
| 79 | + | except subprocess.TimeoutExpired: |
|
| 80 | + | process.kill() |
|
| 81 | + | process.wait() |
|
| 82 | + | if process in CHILDREN: |
|
| 83 | + | CHILDREN.remove(process) |
|
| 84 | + | ||
| 85 | + | ||
| 86 | + | def cleanup_processes() -> None: |
|
| 87 | + | """Terminate all tracked processes in reverse order.""" |
|
| 88 | + | ||
| 89 | + | for process in reversed(CHILDREN[:]): |
|
| 90 | + | terminate(process) |
|
| 91 | + | ||
| 92 | + | ||
| 93 | + | def tail(path: Path, lines: int = 40) -> str: |
|
| 94 | + | """Return the tail of a diagnostic file.""" |
|
| 95 | + | ||
| 96 | + | if not path.exists(): |
|
| 97 | + | return "" |
|
| 98 | + | return "\n".join(path.read_text(errors="replace").splitlines()[-lines:]) |
|
| 99 | + | ||
| 100 | + | ||
| 101 | + | def eventually(description: str, predicate, timeout: float = 2.0): |
|
| 102 | + | """Wait until a predicate returns a truthy value.""" |
|
| 103 | + | ||
| 104 | + | deadline = time.monotonic() + timeout |
|
| 105 | + | last: Exception | None = None |
|
| 106 | + | while time.monotonic() < deadline: |
|
| 107 | + | try: |
|
| 108 | + | value = predicate() |
|
| 109 | + | if value: |
|
| 110 | + | return value |
|
| 111 | + | except (OSError, ValueError, IndexError) as error: |
|
| 112 | + | last = error |
|
| 113 | + | time.sleep(0.02) |
|
| 114 | + | detail = f": {last}" if last else "" |
|
| 115 | + | raise Failure(f"timeout waiting for {description}{detail}") |
|
| 116 | + | ||
| 117 | + | ||
| 118 | + | def test(name: str, body) -> None: |
|
| 119 | + | """Run and report one named test.""" |
|
| 120 | + | ||
| 121 | + | global CURRENT_COMMAND, CURRENT_LOG |
|
| 122 | + | CURRENT_COMMAND = [] |
|
| 123 | + | CURRENT_LOG = None |
|
| 124 | + | progress = os.environ.get("SWM_TEST_PROGRESS") |
|
| 125 | + | print(f"{name} ... ", end="", flush=True) |
|
| 126 | + | if progress: |
|
| 127 | + | with open(progress, "a") as stream: |
|
| 128 | + | stream.write(f"{name} ... ") |
|
| 129 | + | try: |
|
| 130 | + | body() |
|
| 131 | + | except Exception: |
|
| 132 | + | print("FAIL", flush=True) |
|
| 133 | + | if progress: |
|
| 134 | + | with open(progress, "a") as stream: |
|
| 135 | + | stream.write("FAIL\n") |
|
| 136 | + | raise |
|
| 137 | + | print("ok", flush=True) |
|
| 138 | + | if progress: |
|
| 139 | + | with open(progress, "a") as stream: |
|
| 140 | + | stream.write("ok\n") |
|
| 141 | + | ||
| 142 | + | ||
| 143 | + | def generate_python_protocols(pkgconfig_executable: str) -> None: |
|
| 144 | + | """Generate Python client protocol bindings.""" |
|
| 145 | + | ||
| 146 | + | wlproto = Path( |
|
| 147 | + | run(pkgconfig_executable, "--variable=pkgdatadir", "wayland-protocols").strip() |
|
| 148 | + | ) |
|
| 149 | + | xmls = [ |
|
| 150 | + | Path("/usr/share/wayland/wayland.xml"), |
|
| 151 | + | wlproto / "stable/xdg-shell/xdg-shell.xml", |
|
| 152 | + | wlproto / "staging/ext-workspace/ext-workspace-v1.xml", |
|
| 153 | + | wlproto / "staging/ext-session-lock/ext-session-lock-v1.xml", |
|
| 154 | + | wlproto / "unstable/idle-inhibit/idle-inhibit-unstable-v1.xml", |
|
| 155 | + | wlproto |
|
| 156 | + | / "unstable/keyboard-shortcuts-inhibit/keyboard-shortcuts-inhibit-unstable-v1.xml", |
|
| 157 | + | wlproto / "unstable/pointer-constraints/pointer-constraints-unstable-v1.xml", |
|
| 158 | + | wlproto / "unstable/relative-pointer/relative-pointer-unstable-v1.xml", |
|
| 159 | + | wlproto / "unstable/text-input/text-input-unstable-v3.xml", |
|
| 160 | + | ROOT / "protocols/input-method-unstable-v2.xml", |
|
| 161 | + | ROOT / "protocols/virtual-keyboard-unstable-v1.xml", |
|
| 162 | + | ROOT / "protocols/wlr-foreign-toplevel-management-unstable-v1.xml", |
|
| 163 | + | ROOT / "protocols/wlr-layer-shell-unstable-v1.xml", |
|
| 164 | + | ROOT / "protocols/wlr-output-power-management-unstable-v1.xml", |
|
| 165 | + | ROOT / "protocols/wlr-virtual-pointer-unstable-v1.xml", |
|
| 166 | + | ] |
|
| 167 | + | package = BUILD / "python" / "protocols" |
|
| 168 | + | shutil.rmtree(package, ignore_errors=True) |
|
| 169 | + | package.mkdir(parents=True) |
|
| 170 | + | (package / "__init__.py").touch() |
|
| 171 | + | run("pywayland-scanner", "-o", package, "-i", *xmls) |
|
| 172 | + | ||
| 173 | + | # pywayland 0.4 generates a circular import for ext-workspace-v1. The |
|
| 174 | + | # group events refer to objects that already exist, so they do not need an |
|
| 175 | + | # interface constructor. Dropping that annotation breaks the cycle while |
|
| 176 | + | # preserving the wire signature. |
|
| 177 | + | workspace_group = package / "ext_workspace_v1/ext_workspace_group_handle_v1.py" |
|
| 178 | + | generated = workspace_group.read_text() |
|
| 179 | + | generated = generated.replace( |
|
| 180 | + | "from .ext_workspace_handle_v1 import ExtWorkspaceHandleV1\n", "" |
|
| 181 | + | ).replace( |
|
| 182 | + | "Argument(ArgumentType.Object, interface=ExtWorkspaceHandleV1)", |
|
| 183 | + | "Argument(ArgumentType.Object)", |
|
| 184 | + | ) |
|
| 185 | + | workspace_group.write_text(generated) |
|
| 186 | + | ||
| 187 | + | ||
| 188 | + | def prepare_suite(unit: bool, integration: bool) -> None: |
|
| 189 | + | """Prepare runtime files for the selected tests.""" |
|
| 190 | + | ||
| 191 | + | binaries = [ |
|
| 192 | + | BIN / name |
|
| 193 | + | for name, selected in (("unit", unit), ("swm", integration)) |
|
| 194 | + | if selected |
|
| 195 | + | ] |
|
| 196 | + | missing = [binary for binary in binaries if not binary.exists()] |
|
| 197 | + | if missing: |
|
| 198 | + | raise Failure(f"test binary not built: {missing[0]}; run tests through make") |
|
| 199 | + | for directory in (PROFILES, LOGS): |
|
| 200 | + | shutil.rmtree(directory, ignore_errors=True) |
|
| 201 | + | directory.mkdir(parents=True) |
|
| 202 | + | if unit: |
|
| 203 | + | stubs = BUILD / "stubs" |
|
| 204 | + | shutil.rmtree(stubs, ignore_errors=True) |
|
| 205 | + | stubs.mkdir() |
|
| 206 | + | stub_commands = ( |
|
| 207 | + | "dbus-update-activation-environment", |
|
| 208 | + | "foot", |
|
| 209 | + | "waybar", |
|
| 210 | + | "swaybg", |
|
| 211 | + | "sh", |
|
| 212 | + | "wl-paste", |
|
| 213 | + | ) |
|
| 214 | + | for name in stub_commands: |
|
| 215 | + | (stubs / name).symlink_to("/bin/true") |
|
| 216 | + | if integration: |
|
| 217 | + | for executable in ("pywayland-scanner", "timeout"): |
|
| 218 | + | if not shutil.which(executable): |
|
| 219 | + | raise Failure(f"required tool not found: {executable}") |
|
| 220 | + | generate_python_protocols(os.environ.get("PKG_CONFIG", "pkg-config")) |
|
| 221 | + | source_config = (SOURCE / "config.h").read_text() |
|
| 222 | + | if re.search(r"#define\s+(?:MODKEY|MOD)\s+WLR_MODIFIER_ALT", source_config): |
|
| 223 | + | modkey = 8 |
|
| 224 | + | elif re.search(r"#define\s+(?:MODKEY|MOD)\s+WLR_MODIFIER_LOGO", source_config): |
|
| 225 | + | modkey = 64 |
|
| 226 | + | else: |
|
| 227 | + | raise Failure("unsupported modifier in config.def.h") |
|
| 228 | + | (BUILD / "modkey").write_text(str(modkey)) |
|
| 229 | + | ||
| 230 | + | ||
| 231 | + | def run_unit() -> None: |
|
| 232 | + | """Run every compositor unit test in an isolated process.""" |
|
| 233 | + | ||
| 234 | + | runtime = BUILD / "unit-runtime" |
|
| 235 | + | shutil.rmtree(runtime, ignore_errors=True) |
|
| 236 | + | runtime.mkdir() |
|
| 237 | + | env = os.environ.copy() |
|
| 238 | + | env["LLVM_PROFILE_FILE"] = str(PROFILES / "unit-%m-%p.profraw") |
|
| 239 | + | env["SWM_TEST_STUB_PATH"] = str(BUILD / "stubs") |
|
| 240 | + | env["SWM_TEST_DIR"] = str(runtime) |
|
| 241 | + | for name in run(BIN / "unit", "--list", env=env).splitlines(): |
|
| 242 | + | test(name, lambda name=name: run(BIN / "unit", name, env=env)) |
|
| 243 | + | shutil.rmtree(runtime) |
|
| 244 | + | ||
| 245 | + | ||
| 246 | + | def current_title() -> str: |
|
| 247 | + | """Return the compositor's published title.""" |
|
| 248 | + | ||
| 249 | + | path = Path(os.environ["XDG_RUNTIME_DIR"]) / "swm-title" |
|
| 250 | + | return path.read_text().rstrip("\n") if path.exists() else "" |
|
| 251 | + | ||
| 252 | + | ||
| 253 | + | def wait_title(title: str) -> None: |
|
| 254 | + | """Wait for the compositor to publish a title.""" |
|
| 255 | + | ||
| 256 | + | eventually(f"title {title!r}", lambda: current_title() == title) |
|
| 257 | + | ||
| 258 | + | ||
| 259 | + | def client_command(role: str, *args: object) -> list[str]: |
|
| 260 | + | """Return a Python protocol-client command.""" |
|
| 261 | + | ||
| 262 | + | return [sys.executable, str(TESTDIR / "clients.py"), role, *map(str, args)] |
|
| 263 | + | ||
| 264 | + | ||
| 265 | + | def client_log(name: str) -> Path: |
|
| 266 | + | """Return a fixture client's log path.""" |
|
| 267 | + | ||
| 268 | + | return Path(os.environ["SWM_TEST_LOG_DIR"]) / f"{name}.log" |
|
| 269 | + | ||
| 270 | + | ||
| 271 | + | def start_client(name: str, color: str, fullscreen: bool = False) -> subprocess.Popen: |
|
| 272 | + | """Start an XDG client and wait for its geometry report.""" |
|
| 273 | + | ||
| 274 | + | report = Path(os.environ["SWM_TEST_DIR"]) / f"{name}.geometry" |
|
| 275 | + | env = os.environ.copy() |
|
| 276 | + | env["SWM_CLIENT_REPORT"] = str(report) |
|
| 277 | + | args: list[object] = [color, name] |
|
| 278 | + | if fullscreen: |
|
| 279 | + | args.append("fullscreen") |
|
| 280 | + | process = spawn(client_log(name), *client_command("xdg", *args), env=env) |
|
| 281 | + | wait_title(name) |
|
| 282 | + | eventually(f"report {report}", lambda: report.exists() and report.stat().st_size) |
|
| 283 | + | return process |
|
| 284 | + | ||
| 285 | + | ||
| 286 | + | def geometry(name: str) -> list[int]: |
|
| 287 | + | """Return a client's reported geometry.""" |
|
| 288 | + | ||
| 289 | + | path = Path(os.environ["SWM_TEST_DIR"]) / f"{name}.geometry" |
|
| 290 | + | return list(map(int, path.read_text().split())) |
|
| 291 | + | ||
| 292 | + | ||
| 293 | + | def protocol(role: str, *args: object) -> str: |
|
| 294 | + | """Run one native-protocol action implemented in Python.""" |
|
| 295 | + | ||
| 296 | + | return run(*client_command(role, *args), env=os.environ.copy()) |
|
| 297 | + | ||
| 298 | + | ||
| 299 | + | def virtual_keyboard(key: object, modifiers: int = 0) -> None: |
|
| 300 | + | """Send one configured key combination.""" |
|
| 301 | + | ||
| 302 | + | configured = int((BUILD / "modkey").read_text()) | modifiers |
|
| 303 | + | protocol("keyboard", key, configured) |
|
| 304 | + | ||
| 305 | + | ||
| 306 | + | def held_pointer_click(name: str, x: int, button: str | None = None) -> None: |
|
| 307 | + | """Click while the compositor modifier is held by another client.""" |
|
| 308 | + | ||
| 309 | + | ready = Path(os.environ["SWM_TEST_DIR"]) / f"{name}.ready" |
|
| 310 | + | release = Path(os.environ["SWM_TEST_DIR"]) / f"{name}.release" |
|
| 311 | + | env = os.environ.copy() |
|
| 312 | + | env["SWM_KEYBOARD_READY"] = str(ready) |
|
| 313 | + | env["SWM_KEYBOARD_RELEASE"] = str(release) |
|
| 314 | + | configured = int((BUILD / "modkey").read_text()) |
|
| 315 | + | keyboard = spawn( |
|
| 316 | + | client_log(name), |
|
| 317 | + | *client_command("keyboard", "none", configured), |
|
| 318 | + | env=env, |
|
| 319 | + | ) |
|
| 320 | + | eventually("held keyboard readiness", lambda: ready.exists()) |
|
| 321 | + | args: list[object] = [x] |
|
| 322 | + | if button: |
|
| 323 | + | args.append(button) |
|
| 324 | + | protocol("pointer", *args) |
|
| 325 | + | release.write_text("release\n") |
|
| 326 | + | eventually("held keyboard exit", lambda: keyboard.poll() is not None) |
|
| 327 | + | if keyboard in CHILDREN: |
|
| 328 | + | CHILDREN.remove(keyboard) |
|
| 329 | + | ||
| 330 | + | ||
| 331 | + | def integration_session() -> None: |
|
| 332 | + | """Exercise compositor behavior in a one-output session.""" |
|
| 333 | + | ||
| 334 | + | one = start_client("one", "ff5588dd") |
|
| 335 | + | two = start_client("two", "ff55aa88") |
|
| 336 | + | ||
| 337 | + | def publication() -> None: |
|
| 338 | + | windows = (Path(os.environ["XDG_RUNTIME_DIR"]) / "swm-windows").read_text() |
|
| 339 | + | if not re.search(r"^\d+,\d+ \d+x\d+ one$", windows, re.MULTILINE): |
|
| 340 | + | raise Failure("missing published rectangle for one") |
|
| 341 | + | if not re.search(r"^\d+,\d+ \d+x\d+ two$", windows, re.MULTILINE): |
|
| 342 | + | raise Failure("missing published rectangle for two") |
|
| 343 | + | before = current_title() |
|
| 344 | + | virtual_keyboard(36) |
|
| 345 | + | wait_title("two" if before == "one" else "one") |
|
| 346 | + | ||
| 347 | + | test("window publication and focus", publication) |
|
| 348 | + | ||
| 349 | + | def layouts() -> None: |
|
| 350 | + | virtual_keyboard(33) |
|
| 351 | + | eventually("max-stack geometry", lambda: geometry("one")[:2] == geometry("two")[:2]) |
|
| 352 | + | virtual_keyboard(33) |
|
| 353 | + | focused = current_title() |
|
| 354 | + | virtual_keyboard(33, 1) |
|
| 355 | + | eventually("fullscreen enable", lambda: geometry(focused)[2] == 1) |
|
| 356 | + | virtual_keyboard(33, 1) |
|
| 357 | + | eventually("fullscreen disable", lambda: geometry(focused)[2] == 0) |
|
| 358 | + | ||
| 359 | + | test("layouts and fullscreen", layouts) |
|
| 360 | + | ||
| 361 | + | def workspaces() -> None: |
|
| 362 | + | current = current_title() |
|
| 363 | + | virtual_keyboard(20) |
|
| 364 | + | virtual_keyboard(20) |
|
| 365 | + | virtual_keyboard(3, 1) |
|
| 366 | + | eventually("focus after moving client", lambda: current_title() not in {"", current}) |
|
| 367 | + | virtual_keyboard(3) |
|
| 368 | + | wait_title(current) |
|
| 369 | + | virtual_keyboard(2, 1) |
|
| 370 | + | wait_title("") |
|
| 371 | + | virtual_keyboard(2) |
|
| 372 | + | wait_title(current) |
|
| 373 | + | virtual_keyboard(103) |
|
| 374 | + | wait_title("") |
|
| 375 | + | virtual_keyboard(108) |
|
| 376 | + | wait_title(current) |
|
| 377 | + | ||
| 378 | + | test("workspaces", workspaces) |
|
| 379 | + | ||
| 380 | + | def layout_configuration() -> None: |
|
| 381 | + | before_one = geometry("one") |
|
| 382 | + | before_two = geometry("two") |
|
| 383 | + | virtual_keyboard(36, 1) |
|
| 384 | + | eventually( |
|
| 385 | + | "client swap", |
|
| 386 | + | lambda: geometry("one")[0] == before_two[0] and geometry("two")[0] == before_one[0], |
|
| 387 | + | ) |
|
| 388 | + | for key in (28, 50, 19, 57): |
|
| 389 | + | virtual_keyboard(key) |
|
| 390 | + | eventually("master-top layout", lambda: geometry("one")[0] == geometry("two")[0]) |
|
| 391 | + | before = geometry("one") |
|
| 392 | + | virtual_keyboard(35) |
|
| 393 | + | eventually("master size change", lambda: geometry("one") != before) |
|
| 394 | + | ||
| 395 | + | test("layout configuration", layout_configuration) |
|
| 396 | + | ||
| 397 | + | def protocols() -> None: |
|
| 398 | + | target = current_title() |
|
| 399 | + | protocol("foreign", target, "fullscreen") |
|
| 400 | + | eventually("foreign fullscreen", lambda: geometry(target)[2] == 1) |
|
| 401 | + | protocol("foreign", target, "unfullscreen") |
|
| 402 | + | eventually("foreign unfullscreen", lambda: geometry(target)[2] == 0) |
|
| 403 | + | protocol("pointer", 100) |
|
| 404 | + | protocol("pointer", 900) |
|
| 405 | + | held_pointer_click("held-left", 100) |
|
| 406 | + | held_pointer_click("held-right", 100, "right") |
|
| 407 | + | for key, modifiers in ((51, 0), (52, 0), (51, 1), (52, 1), (57, 1), (43, 1)): |
|
| 408 | + | virtual_keyboard(key, modifiers) |
|
| 409 | + | protocol("output-power") |
|
| 410 | + | protocol("output-management") |
|
| 411 | + | ||
| 412 | + | test("control protocols", protocols) |
|
| 413 | + | ||
| 414 | + | def isolation() -> None: |
|
| 415 | + | protocol("workspace", 3) |
|
| 416 | + | wait_title("") |
|
| 417 | + | three = start_client("three", "ff8855aa") |
|
| 418 | + | full = start_client("full", "ffaa5588", True) |
|
| 419 | + | eventually("fullscreen state", lambda: geometry("full")[2] == 1) |
|
| 420 | + | protocol("foreign", "full", "close") |
|
| 421 | + | wait_title("three") |
|
| 422 | + | protocol("workspace", 1) |
|
| 423 | + | eventually("workspace protocol activation", lambda: current_title() in {"one", "two"}) |
|
| 424 | + | terminate(three) |
|
| 425 | + | terminate(full) |
|
| 426 | + | ||
| 427 | + | test("fullscreen isolation", isolation) |
|
| 428 | + | ||
| 429 | + | def lifecycle() -> None: |
|
| 430 | + | terminate(one) |
|
| 431 | + | terminate(two) |
|
| 432 | + | protocol("xdg-lifecycle", 2) |
|
| 433 | + | protocol("transient", 2) |
|
| 434 | + | protocol("layer", 2) |
|
| 435 | + | protocol("session-lock", 2) |
|
| 436 | + | protocol("text-input") |
|
| 437 | + | protocol("x11") |
|
| 438 | + | ||
| 439 | + | test("protocol lifecycle", lifecycle) |
|
| 440 | + | terminate(one) |
|
| 441 | + | terminate(two) |
|
| 442 | + | ||
| 443 | + | ||
| 444 | + | def integration_multioutput() -> None: |
|
| 445 | + | """Exercise workspace movement and output removal.""" |
|
| 446 | + | ||
| 447 | + | client = start_client("multi", "ff778899") |
|
| 448 | + | ||
| 449 | + | def multioutput() -> None: |
|
| 450 | + | virtual_keyboard(105, 1) |
|
| 451 | + | wait_title("") |
|
| 452 | + | virtual_keyboard(106, 1) |
|
| 453 | + | wait_title("multi") |
|
| 454 | + | virtual_keyboard(105, 4) |
|
| 455 | + | wait_title("") |
|
| 456 | + | virtual_keyboard(105, 1) |
|
| 457 | + | wait_title("multi") |
|
| 458 | + | protocol("output-management", "disable-second") |
|
| 459 | + | wait_title("multi") |
|
| 460 | + | ||
| 461 | + | test("multiple outputs", multioutput) |
|
| 462 | + | terminate(client) |
|
| 463 | + | ||
| 464 | + | ||
| 465 | + | def fixture_main(kind: str) -> None: |
|
| 466 | + | """Run one fixture inside swm's process tree.""" |
|
| 467 | + | ||
| 468 | + | result = Path(os.environ["SWM_TEST_RESULT"]) |
|
| 469 | + | status = "ok" |
|
| 470 | + | try: |
|
| 471 | + | if kind == "session": |
|
| 472 | + | integration_session() |
|
| 473 | + | elif kind == "multioutput": |
|
| 474 | + | integration_multioutput() |
|
| 475 | + | else: |
|
| 476 | + | raise Failure(f"unknown fixture: {kind}") |
|
| 477 | + | except Exception as error: |
|
| 478 | + | status = str(error) |
|
| 479 | + | finally: |
|
| 480 | + | cleanup_processes() |
|
| 481 | + | result.write_text(status + "\n") |
|
| 482 | + | os.kill(os.getppid(), signal.SIGTERM) |
|
| 483 | + | if status != "ok": |
|
| 484 | + | print(status, file=sys.stderr) |
|
| 485 | + | raise SystemExit(1) |
|
| 486 | + | ||
| 487 | + | ||
| 488 | + | def run_fixture(kind: str, outputs: int) -> None: |
|
| 489 | + | """Run one isolated headless compositor fixture.""" |
|
| 490 | + | ||
| 491 | + | runtime = BUILD / f"runtime-{kind}" |
|
| 492 | + | state = BUILD / f"state-{kind}" |
|
| 493 | + | fixture = BUILD / f"fixture-{kind}" |
|
| 494 | + | logs = LOGS / kind |
|
| 495 | + | for path in (runtime, state, fixture, logs): |
|
| 496 | + | shutil.rmtree(path, ignore_errors=True) |
|
| 497 | + | path.mkdir(parents=True, exist_ok=True) |
|
| 498 | + | runtime.chmod(0o700) |
|
| 499 | + | result = BUILD / f"{kind}.result" |
|
| 500 | + | progress = BUILD / f"{kind}.progress" |
|
| 501 | + | result.write_text("") |
|
| 502 | + | progress.write_text("") |
|
| 503 | + | env = os.environ.copy() |
|
| 504 | + | env.update( |
|
| 505 | + | { |
|
| 506 | + | "XDG_RUNTIME_DIR": str(runtime), |
|
| 507 | + | "XDG_STATE_HOME": str(state), |
|
| 508 | + | "SWM_NO_AUTOSTART": "1", |
|
| 509 | + | "WLR_BACKENDS": "headless", |
|
| 510 | + | "WLR_RENDERER": "pixman", |
|
| 511 | + | "WLR_LIBINPUT_NO_DEVICES": "1", |
|
| 512 | + | "WLR_HEADLESS_OUTPUTS": str(outputs), |
|
| 513 | + | "SWM_TEST_RESULT": str(result), |
|
| 514 | + | "SWM_TEST_PROGRESS": str(progress), |
|
| 515 | + | "SWM_TEST_DIR": str(fixture), |
|
| 516 | + | "SWM_TEST_LOG_DIR": str(logs), |
|
| 517 | + | "PYTHONPATH": str(BUILD / "python"), |
|
| 518 | + | "LLVM_PROFILE_FILE": str(PROFILES / "integration-%m-%p.profraw"), |
|
| 519 | + | } |
|
| 520 | + | ) |
|
| 521 | + | log = LOGS / f"swm-{kind}.log" |
|
| 522 | + | startup = f"{shlex.quote(sys.executable)} {shlex.quote(str(SCRIPT))} --fixture {kind}" |
|
| 523 | + | command = ["timeout", "--foreground", "30", str(BIN / "swm"), "-s", startup] |
|
| 524 | + | compositor = spawn(log, *command, env=env) |
|
| 525 | + | progress_offset = 0 |
|
| 526 | + | while compositor.poll() is None: |
|
| 527 | + | with progress.open() as stream: |
|
| 528 | + | stream.seek(progress_offset) |
|
| 529 | + | update = stream.read() |
|
| 530 | + | progress_offset = stream.tell() |
|
| 531 | + | if update: |
|
| 532 | + | print(update, end="", flush=True) |
|
| 533 | + | time.sleep(0.02) |
|
| 534 | + | status = compositor.wait() |
|
| 535 | + | CHILDREN.remove(compositor) |
|
| 536 | + | with progress.open() as stream: |
|
| 537 | + | stream.seek(progress_offset) |
|
| 538 | + | update = stream.read() |
|
| 539 | + | if update: |
|
| 540 | + | print(update, end="", flush=True) |
|
| 541 | + | outcome = result.read_text().strip() |
|
| 542 | + | if status or outcome != "ok": |
|
| 543 | + | raise Failure( |
|
| 544 | + | f"fixture {kind} failed\nresult: {outcome!r}\nstatus: {status}\n" |
|
| 545 | + | f"compositor log tail ({log}):\n{tail(log, 60)}" |
|
| 546 | + | ) |
|
| 547 | + | contents = log.read_text(errors="replace") |
|
| 548 | + | if re.search(r"pool exhausted|AddressSanitizer|runtime error:|protocol error", contents): |
|
| 549 | + | raise Failure(f"fixture {kind} reported a runtime failure\n{tail(log, 60)}") |
|
| 550 | + | for path in (runtime, state, fixture): |
|
| 551 | + | shutil.rmtree(path) |
|
| 552 | + | ||
| 553 | + | ||
| 554 | + | def run_integration() -> None: |
|
| 555 | + | """Run every integration fixture.""" |
|
| 556 | + | ||
| 557 | + | run_fixture("session", 1) |
|
| 558 | + | run_fixture("multioutput", 2) |
|
| 559 | + | ||
| 560 | + | ||
| 561 | + | def coverage_report() -> None: |
|
| 562 | + | """Merge profiles and report production coverage.""" |
|
| 563 | + | ||
| 564 | + | for executable in ("llvm-profdata", "llvm-cov"): |
|
| 565 | + | if not shutil.which(executable): |
|
| 566 | + | raise Failure(f"required coverage tool not found: {executable}") |
|
| 567 | + | raw = glob.glob(str(PROFILES / "*.profraw")) |
|
| 568 | + | if not raw: |
|
| 569 | + | raise Failure("no LLVM coverage profiles were produced") |
|
| 570 | + | profdata = BUILD / "coverage.profdata" |
|
| 571 | + | run("llvm-profdata", "merge", "-sparse", *raw, "-o", profdata) |
|
| 572 | + | report = run( |
|
| 573 | + | "llvm-cov", |
|
| 574 | + | "report", |
|
| 575 | + | BIN / "swm", |
|
| 576 | + | "-instr-profile", |
|
| 577 | + | profdata, |
|
| 578 | + | "-object", |
|
| 579 | + | BIN / "unit", |
|
| 580 | + | SOURCE / "swm.c", |
|
| 581 | + | ROOT / "util.c", |
|
| 582 | + | ) |
|
| 583 | + | files: list[str] = [] |
|
| 584 | + | total: list[str] | None = None |
|
| 585 | + | for line in report.splitlines(): |
|
| 586 | + | fields = line.split() |
|
| 587 | + | if len(fields) < 10: |
|
| 588 | + | continue |
|
| 589 | + | if fields[0] == "TOTAL": |
|
| 590 | + | total = fields |
|
| 591 | + | elif fields[0].endswith(".c"): |
|
| 592 | + | files.append(f"{Path(fields[0]).name} {fields[9]}") |
|
| 593 | + | if total is None: |
|
| 594 | + | raise Failure("could not parse llvm-cov TOTAL line") |
|
| 595 | + | percent = float(total[9].rstrip("%")) |
|
| 596 | + | print(f"coverage: {', '.join(files)}, aggregate {percent:.2f}%") |
|
| 597 | + | ||
| 598 | + | ||
| 599 | + | def clean() -> None: |
|
| 600 | + | """Remove all generated test artifacts.""" |
|
| 601 | + | ||
| 602 | + | shutil.rmtree(BUILD, ignore_errors=True) |
|
| 603 | + | print(f"clean: removed {BUILD}") |
|
| 604 | + | ||
| 605 | + | ||
| 606 | + | def main() -> None: |
|
| 607 | + | """Dispatch public runner modes and private fixture modes.""" |
|
| 608 | + | ||
| 609 | + | if len(sys.argv) == 3 and sys.argv[1] == "--fixture": |
|
| 610 | + | fixture_main(sys.argv[2]) |
|
| 611 | + | return |
|
| 612 | + | modes = {"unit", "integration", "coverage", "clean"} |
|
| 613 | + | if len(sys.argv) > 2 or (len(sys.argv) == 2 and sys.argv[1] not in modes): |
|
| 614 | + | raise Failure("usage: ./test/run.py [unit|integration|coverage|clean]") |
|
| 615 | + | mode = sys.argv[1] if len(sys.argv) == 2 else "all" |
|
| 616 | + | if mode == "clean": |
|
| 617 | + | clean() |
|
| 618 | + | return |
|
| 619 | + | run_units = mode in {"all", "unit", "coverage"} |
|
| 620 | + | run_integrations = mode in {"all", "integration", "coverage"} |
|
| 621 | + | prepare_suite(run_units, run_integrations) |
|
| 622 | + | if run_units: |
|
| 623 | + | run_unit() |
|
| 624 | + | if run_integrations: |
|
| 625 | + | run_integration() |
|
| 626 | + | if mode == "coverage": |
|
| 627 | + | coverage_report() |
|
| 628 | + | ||
| 629 | + | ||
| 630 | + | if __name__ == "__main__": |
|
| 631 | + | try: |
|
| 632 | + | main() |
|
| 633 | + | except Exception as error: |
|
| 634 | + | print(error, file=sys.stderr) |
|
| 635 | + | if CURRENT_COMMAND: |
|
| 636 | + | print(f"last command: {command_text(CURRENT_COMMAND)}", file=sys.stderr) |
|
| 637 | + | if CURRENT_LOG: |
|
| 638 | + | print(f"log tail ({CURRENT_LOG}):\n{tail(CURRENT_LOG)}", file=sys.stderr) |
|
| 639 | + | cleanup_processes() |
|
| 640 | + | raise SystemExit(1) |
test/unit.c
added
+884 -0
| 1 | + | #include <assert.h> |
|
| 2 | + | #include <fcntl.h> |
|
| 3 | + | #include <limits.h> |
|
| 4 | + | #include <stdlib.h> |
|
| 5 | + | #include <string.h> |
|
| 6 | + | #include <sys/wait.h> |
|
| 7 | + | ||
| 8 | + | #define main swm_program_main |
|
| 9 | + | #include "swm.c" |
|
| 10 | + | #undef main |
|
| 11 | + | ||
| 12 | + | /* Verify that releasing a pool item clears its storage. */ |
|
| 13 | + | static void test_pool(pool_t *pool) { |
|
| 14 | + | unsigned char *item = pool_take(pool); |
|
| 15 | + | size_t i; |
|
| 16 | + | ||
| 17 | + | assert(item); |
|
| 18 | + | memset(item, 0xa5, pool->item_size); |
|
| 19 | + | pool_release(pool, item); |
|
| 20 | + | ||
| 21 | + | for (i = 0; i < pool->item_size; i++) |
|
| 22 | + | assert(item[i] == 0); |
|
| 23 | + | } |
|
| 24 | + | ||
| 25 | + | /* Verify pool allocation limits and clearing for every compositor object type. */ |
|
| 26 | + | static void test_pools(void) { |
|
| 27 | + | client_t *allocated[MAX_CLIENTS]; |
|
| 28 | + | int saved, nullfd; |
|
| 29 | + | size_t i; |
|
| 30 | + | ||
| 31 | + | for (i = 0; i < MAX_CLIENTS; i++) { |
|
| 32 | + | allocated[i] = pool_take(&client_pool); |
|
| 33 | + | assert(allocated[i] == &client_items[i]); |
|
| 34 | + | } |
|
| 35 | + | saved = dup(STDERR_FILENO); |
|
| 36 | + | nullfd = open("/dev/null", O_WRONLY); |
|
| 37 | + | assert(saved >= 0 && nullfd >= 0); |
|
| 38 | + | assert(dup2(nullfd, STDERR_FILENO) >= 0); |
|
| 39 | + | assert(pool_take(&client_pool) == nullptr); |
|
| 40 | + | assert(dup2(saved, STDERR_FILENO) >= 0); |
|
| 41 | + | close(nullfd); |
|
| 42 | + | close(saved); |
|
| 43 | + | ||
| 44 | + | for (i = 0; i < MAX_CLIENTS; i++) |
|
| 45 | + | pool_release(&client_pool, allocated[i]); |
|
| 46 | + | ||
| 47 | + | test_pool(&monitor_pool); |
|
| 48 | + | test_pool(&layer_surface_pool); |
|
| 49 | + | test_pool(&keyboard_group_pool); |
|
| 50 | + | test_pool(&pointer_constraint_pool); |
|
| 51 | + | test_pool(&text_input_pool); |
|
| 52 | + | test_pool(&input_popup_pool); |
|
| 53 | + | test_pool(&session_lock_pool); |
|
| 54 | + | test_pool(&pending_spawn_pool); |
|
| 55 | + | test_pool(&window_state_pool); |
|
| 56 | + | test_pool(&static_listener_pool); |
|
| 57 | + | test_pool(&workspace_manager_pool); |
|
| 58 | + | test_pool(&workspace_handle_pool); |
|
| 59 | + | } |
|
| 60 | + | ||
| 61 | + | /* Require a geometry helper to return the expected rectangle. */ |
|
| 62 | + | static void assert_box(struct swm_box box, int x, int y, int width, int height) { |
|
| 63 | + | assert(box.x == x); |
|
| 64 | + | assert(box.y == y); |
|
| 65 | + | assert(box.width == width); |
|
| 66 | + | assert(box.height == height); |
|
| 67 | + | } |
|
| 68 | + | ||
| 69 | + | /* Verify pure geometry helpers. */ |
|
| 70 | + | static void test_geometry(void) { |
|
| 71 | + | struct swm_box bounds = { 10, 20, 100, 80 }; |
|
| 72 | + | struct swm_box box = { 200, 200, 0, -2 }; |
|
| 73 | + | struct swm_box client = { 100, 100, 300, 200 }; |
|
| 74 | + | struct swm_box cursor = { 20, 30, 4, 10 }; |
|
| 75 | + | struct swm_box output = { 0, 0, 500, 400 }; |
|
| 76 | + | ||
| 77 | + | swm_box_apply_bounds(&box, &bounds, 2); |
|
| 78 | + | assert_box(box, 105, 95, 5, 5); |
|
| 79 | + | box = (struct swm_box){ 10, 20, 100, 80 }; |
|
| 80 | + | assert_box(swm_box_resize(&box, 20, 10, 10, 1), 10, 20, 120, 90); |
|
| 81 | + | assert_box(swm_box_resize(&box, 500, 500, 5, 2), 105, 95, 5, 5); |
|
| 82 | + | swm_box_reconcile_commit(&box, 90, 70, 2, 5); |
|
| 83 | + | assert_box(box, 16, 26, 94, 74); |
|
| 84 | + | assert_box(swm_popup_position(&client, 2, &cursor, 400, 300, &output), 100, 0, 400, 300); |
|
| 85 | + | } |
|
| 86 | + | ||
| 87 | + | /* Verify pure workspace and stack-state helpers. */ |
|
| 88 | + | static void test_state(void) { |
|
| 89 | + | bool visible[] = { false, false, false, false, false }; |
|
| 90 | + | bool occupied[] = { false, false, true, false, true }; |
|
| 91 | + | struct swm_stack_state state = { 16, 1, 1 }; |
|
| 92 | + | int side; |
|
| 93 | + | ||
| 94 | + | assert(swm_workspace_state(false, false, false) == SWM_WORKSPACE_HIDDEN); |
|
| 95 | + | assert(swm_workspace_state(true, true, true) == (SWM_WORKSPACE_ACTIVE | SWM_WORKSPACE_URGENT)); |
|
| 96 | + | assert(swm_border_width(3, false, false, false, false, false) == 3); |
|
| 97 | + | assert(swm_border_width(3, true, false, false, false, false) == 0); |
|
| 98 | + | assert(swm_workspace_next(0, 5, 1, false, visible, occupied) == 2); |
|
| 99 | + | assert(swm_workspace_next(0, 5, -1, false, visible, occupied) == 4); |
|
| 100 | + | visible[2] = true; |
|
| 101 | + | assert(swm_workspace_next(0, 5, 1, false, visible, occupied) == 4); |
|
| 102 | + | assert(swm_workspace_next(0, 5, 1, true, visible, occupied) == 1); |
|
| 103 | + | assert(swm_stack_configure(&state, SWM_MASTER_SHRINK, SWM_MASTER_LEFT, &side)); |
|
| 104 | + | assert(state.msize == 15 && side == SWM_MASTER_LEFT); |
|
| 105 | + | ||
| 106 | + | for (int command = SWM_MASTER_GROW; command <= SWM_FLIP_LAYOUT; command++) |
|
| 107 | + | assert(swm_stack_configure(&state, command, SWM_MASTER_LEFT, &side)); |
|
| 108 | + | assert(!swm_stack_configure(&state, 99, SWM_MASTER_LEFT, &side)); |
|
| 109 | + | } |
|
| 110 | + | ||
| 111 | + | /* Verify pure rule and persistent-state parsing helpers. */ |
|
| 112 | + | static void test_parsing(void) { |
|
| 113 | + | char small[5], appid[MAX_WINDOW_STATE_FIELD], title[MAX_WINDOW_STATE_FIELD]; |
|
| 114 | + | struct swm_box geometry; |
|
| 115 | + | ||
| 116 | + | assert(swm_rule_matches("*", nullptr, "anything", "title")); |
|
| 117 | + | assert(swm_rule_matches("term", "shell", "my-terminal", "a shell")); |
|
| 118 | + | assert(!swm_rule_matches("browser", nullptr, "terminal", "browser")); |
|
| 119 | + | swm_sanitize_field(small, sizeof(small), "a\tb\nc", nullptr); |
|
| 120 | + | assert(!strcmp(small, "a b ")); |
|
| 121 | + | assert(swm_parse_window_state( |
|
| 122 | + | "org.app\tWindow\t1\t-2\t300\t200\n", appid, sizeof(appid), title, sizeof(title), &geometry |
|
| 123 | + | )); |
|
| 124 | + | assert(!strcmp(appid, "org.app")); |
|
| 125 | + | assert(!strcmp(title, "Window")); |
|
| 126 | + | assert_box(geometry, 1, -2, 300, 200); |
|
| 127 | + | assert( |
|
| 128 | + | !swm_parse_window_state("broken", appid, sizeof(appid), title, sizeof(title), &geometry) |
|
| 129 | + | ); |
|
| 130 | + | } |
|
| 131 | + | ||
| 132 | + | /* Verify layout cycling and complete output-area partitioning. */ |
|
| 133 | + | static void test_layouts(void) { |
|
| 134 | + | bool cycle[] = { true, true, false, true }; |
|
| 135 | + | struct swm_box area = { -3, 7, 64, 61 }; |
|
| 136 | + | struct swm_box boxes[64]; |
|
| 137 | + | struct swm_stack_state state; |
|
| 138 | + | size_t count, produced; |
|
| 139 | + | int rotate, flip, masters, stacks; |
|
| 140 | + | ||
| 141 | + | assert(swm_next_layout(0, 4, cycle) == 1); |
|
| 142 | + | assert(swm_next_layout(1, 4, cycle) == 3); |
|
| 143 | + | assert(swm_next_layout(3, 4, cycle) == 0); |
|
| 144 | + | ||
| 145 | + | for (rotate = 0; rotate <= 1; rotate++) |
|
| 146 | + | for (flip = 0; flip <= 1; flip++) |
|
| 147 | + | for (masters = 0; masters < 5; masters++) |
|
| 148 | + | for (stacks = 1; stacks < 5; stacks++) |
|
| 149 | + | for (count = 1; count < 13; count++) { |
|
| 150 | + | int covered = 0; |
|
| 151 | + | ||
| 152 | + | state = (struct swm_stack_state){ 13, masters, stacks }; |
|
| 153 | + | produced = swm_stack_layout(&area, &state, rotate, flip, count, boxes); |
|
| 154 | + | assert(produced == count); |
|
| 155 | + | ||
| 156 | + | for (size_t i = 0; i < count; i++) |
|
| 157 | + | covered += boxes[i].width * boxes[i].height; |
|
| 158 | + | assert(covered == area.width * area.height); |
|
| 159 | + | } |
|
| 160 | + | } |
|
| 161 | + | ||
| 162 | + | /* Verify environment expansion and output bounds. */ |
|
| 163 | + | static void test_utilities(void) { |
|
| 164 | + | char output[64]; |
|
| 165 | + | ||
| 166 | + | assert(setenv("SWM_TEST_ENV", "hello world", 1) == 0); |
|
| 167 | + | assert(env_expand(output, sizeof(output), "$SWM_TEST_ENV/tail") == 17); |
|
| 168 | + | assert(!strcmp(output, "hello world/tail")); |
|
| 169 | + | assert(env_expand(output, 4, "$SWM_TEST_ENV") == 0); |
|
| 170 | + | } |
|
| 171 | + | ||
| 172 | + | /* Verify free-workspace selection and published workspace states. */ |
|
| 173 | + | static void test_workspace_queries(void) { |
|
| 174 | + | client_t *client; |
|
| 175 | + | workspace_t *workspace; |
|
| 176 | + | int i; |
|
| 177 | + | ||
| 178 | + | for (i = 0; i < WSCOUNT; i++) { |
|
| 179 | + | workspaces[i].mon = (monitor_t *)1; |
|
| 180 | + | wl_list_init(&workspaces[i].handles); |
|
| 181 | + | } |
|
| 182 | + | workspaces[4].mon = nullptr; |
|
| 183 | + | assert(free_workspace() == &workspaces[4]); |
|
| 184 | + | workspaces[4].mon = (monitor_t *)1; |
|
| 185 | + | assert(free_workspace() == nullptr); |
|
| 186 | + | ||
| 187 | + | wl_list_init(&clients); |
|
| 188 | + | workspace = &workspaces[2]; |
|
| 189 | + | workspace->mon = nullptr; |
|
| 190 | + | assert(workspace_state(workspace) == EXT_WORKSPACE_HANDLE_V1_STATE_HIDDEN); |
|
| 191 | + | client = pool_take(&client_pool); |
|
| 192 | + | assert(client); |
|
| 193 | + | client->ws = workspace; |
|
| 194 | + | client->is_urgent = 1; |
|
| 195 | + | wl_list_insert(&clients, &client->link); |
|
| 196 | + | assert(workspace_state(workspace) == EXT_WORKSPACE_HANDLE_V1_STATE_URGENT); |
|
| 197 | + | workspace->mon = (monitor_t *)1; |
|
| 198 | + | assert( |
|
| 199 | + | workspace_state(workspace) == |
|
| 200 | + | (EXT_WORKSPACE_HANDLE_V1_STATE_ACTIVE | EXT_WORKSPACE_HANDLE_V1_STATE_URGENT) |
|
| 201 | + | ); |
|
| 202 | + | wl_list_remove(&client->link); |
|
| 203 | + | pool_release(&client_pool, client); |
|
| 204 | + | } |
|
| 205 | + | ||
| 206 | + | /* Verify state-file paths and client border-width policy. */ |
|
| 207 | + | static void test_paths_and_border_policy(void) { |
|
| 208 | + | const char *directory = getenv("SWM_TEST_DIR"); |
|
| 209 | + | client_t client = {}; |
|
| 210 | + | char path[PATH_MAX], expected[PATH_MAX]; |
|
| 211 | + | ||
| 212 | + | assert(directory); |
|
| 213 | + | assert(snprintf(path, sizeof(path), "%s/state", directory) < (int)sizeof(path)); |
|
| 214 | + | assert(setenv("XDG_STATE_HOME", path, 1) == 0); |
|
| 215 | + | assert(snprintf(expected, sizeof(expected), "%s/swm/windows", path) < (int)sizeof(expected)); |
|
| 216 | + | assert(!strcmp(window_state_path(), expected)); |
|
| 217 | + | unsetenv("XDG_STATE_HOME"); |
|
| 218 | + | assert(snprintf(path, sizeof(path), "%s/home", directory) < (int)sizeof(path)); |
|
| 219 | + | assert(setenv("HOME", path, 1) == 0); |
|
| 220 | + | assert( |
|
| 221 | + | snprintf(expected, sizeof(expected), "%s/.local/state/swm/windows", path) < |
|
| 222 | + | (int)sizeof(expected) |
|
| 223 | + | ); |
|
| 224 | + | assert(!strcmp(window_state_path(), expected)); |
|
| 225 | + | client.bw = 9; |
|
| 226 | + | assert(client_border_width(&client) == borderwidth); |
|
| 227 | + | client.is_fullscreen = 1; |
|
| 228 | + | assert(client_border_width(&client) == 0); |
|
| 229 | + | } |
|
| 230 | + | ||
| 231 | + | /* Initialize a minimal XDG client object for internal tests. */ |
|
| 232 | + | static void init_xdg_client( |
|
| 233 | + | client_t *client, |
|
| 234 | + | struct wlr_xdg_surface *surface, |
|
| 235 | + | struct wlr_xdg_toplevel *toplevel, |
|
| 236 | + | const char *appid, |
|
| 237 | + | const char *title |
|
| 238 | + | ) { |
|
| 239 | + | memset(client, 0, sizeof(*client)); |
|
| 240 | + | memset(surface, 0, sizeof(*surface)); |
|
| 241 | + | memset(toplevel, 0, sizeof(*toplevel)); |
|
| 242 | + | client->type = XDG_SHELL; |
|
| 243 | + | client->surface.xdg = surface; |
|
| 244 | + | surface->toplevel = toplevel; |
|
| 245 | + | toplevel->base = surface; |
|
| 246 | + | toplevel->app_id = (char *)appid; |
|
| 247 | + | toplevel->title = (char *)title; |
|
| 248 | + | } |
|
| 249 | + | ||
| 250 | + | /* Verify matching rules update client state and workspace assignment. */ |
|
| 251 | + | static void test_rule_application(void) { |
|
| 252 | + | client_t client; |
|
| 253 | + | struct wlr_xdg_surface surface; |
|
| 254 | + | struct wlr_xdg_toplevel toplevel; |
|
| 255 | + | ||
| 256 | + | init_xdg_client(&client, &surface, &toplevel, "org.telegram.desktop", "Telegram"); |
|
| 257 | + | wl_list_init(&mons); |
|
| 258 | + | selmon = nullptr; |
|
| 259 | + | apply_rules(&client, nullptr); |
|
| 260 | + | assert(client.is_floating == 1); |
|
| 261 | + | assert(client.is_borderless == 1); |
|
| 262 | + | assert(client.mon == nullptr); |
|
| 263 | + | } |
|
| 264 | + | ||
| 265 | + | /* Verify focus queries across mapped, hidden, and unmanaged clients. */ |
|
| 266 | + | static void test_focus_queries(void) { |
|
| 267 | + | client_t first, second, hidden; |
|
| 268 | + | struct wlr_xdg_surface surfaces[3]; |
|
| 269 | + | struct wlr_xdg_toplevel toplevels[3]; |
|
| 270 | + | monitor_t monitor = {}; |
|
| 271 | + | workspace_t shown = {}, other = {}; |
|
| 272 | + | ||
| 273 | + | shown.mon = &monitor; |
|
| 274 | + | monitor.ws = &shown; |
|
| 275 | + | other.mon = nullptr; |
|
| 276 | + | init_xdg_client(&first, &surfaces[0], &toplevels[0], "first", "First"); |
|
| 277 | + | init_xdg_client(&second, &surfaces[1], &toplevels[1], "second", "Second"); |
|
| 278 | + | init_xdg_client(&hidden, &surfaces[2], &toplevels[2], "hidden", "Hidden"); |
|
| 279 | + | first.ws = second.ws = &shown; |
|
| 280 | + | hidden.ws = &other; |
|
| 281 | + | first.mon = second.mon = &monitor; |
|
| 282 | + | ||
| 283 | + | wl_list_init(&clients); |
|
| 284 | + | wl_list_init(&fstack); |
|
| 285 | + | wl_list_insert(clients.prev, &first.link); |
|
| 286 | + | wl_list_insert(clients.prev, &second.link); |
|
| 287 | + | wl_list_insert(clients.prev, &hidden.link); |
|
| 288 | + | wl_list_insert(&fstack, &hidden.flink); |
|
| 289 | + | wl_list_insert(&fstack, &first.flink); |
|
| 290 | + | wl_list_insert(&fstack, &second.flink); |
|
| 291 | + | assert(focus_top(&monitor) == &second); |
|
| 292 | + | assert(focus_top(nullptr) == nullptr); |
|
| 293 | + | assert(focus_close(&second) == &first); |
|
| 294 | + | assert(focus_close(nullptr) == nullptr); |
|
| 295 | + | assert(focus_close(&hidden) == nullptr); |
|
| 296 | + | assert(client_main(nullptr) == nullptr); |
|
| 297 | + | assert(!clients_related(nullptr, &first)); |
|
| 298 | + | assert(clients_related(&first, &first)); |
|
| 299 | + | } |
|
| 300 | + | ||
| 301 | + | /* Release every saved window-state entry. */ |
|
| 302 | + | static void clear_window_states(void) { |
|
| 303 | + | window_state_t *state, *tmp; |
|
| 304 | + | ||
| 305 | + | wl_list_for_each_safe(state, tmp, &window_states, link) { |
|
| 306 | + | wl_list_remove(&state->link); |
|
| 307 | + | pool_release(&window_state_pool, state); |
|
| 308 | + | } |
|
| 309 | + | } |
|
| 310 | + | ||
| 311 | + | /* Verify saving, loading, finding, and forgetting persistent window state. */ |
|
| 312 | + | static void test_window_state_storage(void) { |
|
| 313 | + | char statehome[PATH_MAX]; |
|
| 314 | + | char filepath[MAX_STATE_PATH]; |
|
| 315 | + | client_t client; |
|
| 316 | + | struct wlr_xdg_surface surface; |
|
| 317 | + | struct wlr_xdg_toplevel toplevel; |
|
| 318 | + | window_state_t *state; |
|
| 319 | + | FILE *file; |
|
| 320 | + | ||
| 321 | + | assert( |
|
| 322 | + | snprintf(statehome, sizeof(statehome), "%s/window-state-XXXXXX", getenv("SWM_TEST_DIR")) < |
|
| 323 | + | (int)sizeof(statehome) |
|
| 324 | + | ); |
|
| 325 | + | assert(mkdtemp(statehome)); |
|
| 326 | + | setenv("XDG_STATE_HOME", statehome, 1); |
|
| 327 | + | wl_list_init(&window_states); |
|
| 328 | + | init_xdg_client(&client, &surface, &toplevel, "org.test\tapp", "Window\nTitle"); |
|
| 329 | + | client.persist_float = client.is_floating = 1; |
|
| 330 | + | client.geom = (struct wlr_box){ 11, -12, 640, 480 }; |
|
| 331 | + | remember_client(&client); |
|
| 332 | + | state = find_window_state(&client); |
|
| 333 | + | assert(state); |
|
| 334 | + | assert(!strcmp(state->appid, "org.test app")); |
|
| 335 | + | assert(!strcmp(state->title, "Window Title")); |
|
| 336 | + | assert(state->geom.x == 11 && state->geom.height == 480); |
|
| 337 | + | ||
| 338 | + | snprintf(filepath, sizeof(filepath), "%s/swm/windows", statehome); |
|
| 339 | + | file = fopen(filepath, "r"); |
|
| 340 | + | assert(file); |
|
| 341 | + | assert(fclose(file) == 0); |
|
| 342 | + | clear_window_states(); |
|
| 343 | + | load_window_states(); |
|
| 344 | + | assert(find_window_state(&client)); |
|
| 345 | + | forget_client(&client); |
|
| 346 | + | assert(find_window_state(&client) == nullptr); |
|
| 347 | + | ||
| 348 | + | file = fopen(filepath, "w"); |
|
| 349 | + | assert(file); |
|
| 350 | + | assert(fputs("bad line\nlegacy\t1\t2\t3\t4\n", file) >= 0); |
|
| 351 | + | assert(fclose(file) == 0); |
|
| 352 | + | load_window_states(); |
|
| 353 | + | assert(!wl_list_empty(&window_states)); |
|
| 354 | + | clear_window_states(); |
|
| 355 | + | unlink(filepath); |
|
| 356 | + | snprintf(filepath, sizeof(filepath), "%s/swm", statehome); |
|
| 357 | + | rmdir(filepath); |
|
| 358 | + | rmdir(statehome); |
|
| 359 | + | } |
|
| 360 | + | ||
| 361 | + | /* Verify SIGCHLD handling releases tracked child processes. */ |
|
| 362 | + | static void test_child_reaping(void) { |
|
| 363 | + | pending_spawn_t *spawned; |
|
| 364 | + | struct timespec delay = { 0, 1000000 }; |
|
| 365 | + | pid_t pid; |
|
| 366 | + | int i; |
|
| 367 | + | ||
| 368 | + | wl_list_init(&pending_spawns); |
|
| 369 | + | pid = fork(); |
|
| 370 | + | assert(pid >= 0); |
|
| 371 | + | ||
| 372 | + | if (pid == 0) |
|
| 373 | + | _exit(0); |
|
| 374 | + | spawned = pool_take(&pending_spawn_pool); |
|
| 375 | + | assert(spawned); |
|
| 376 | + | spawned->pid = pid; |
|
| 377 | + | wl_list_insert(&pending_spawns, &spawned->link); |
|
| 378 | + | child_pid = pid; |
|
| 379 | + | ||
| 380 | + | for (i = 0; i < 100 && !wl_list_empty(&pending_spawns); i++) { |
|
| 381 | + | handle_signal(SIGCHLD, nullptr); |
|
| 382 | + | nanosleep(&delay, nullptr); |
|
| 383 | + | } |
|
| 384 | + | assert(wl_list_empty(&pending_spawns)); |
|
| 385 | + | assert(child_pid == -1); |
|
| 386 | + | assert(handle_signal(0, nullptr) == 0); |
|
| 387 | + | } |
|
| 388 | + | ||
| 389 | + | /* Verify spawned commands are tracked and reaped. */ |
|
| 390 | + | static void test_spawn_tracking(void) { |
|
| 391 | + | const char *command[] = { "/bin/true", nullptr }; |
|
| 392 | + | monitor_t monitor = {}; |
|
| 393 | + | workspace_t workspace = {}; |
|
| 394 | + | struct timespec delay = { 0, 1000000 }; |
|
| 395 | + | int i; |
|
| 396 | + | ||
| 397 | + | assert(sigprocmask(SIG_BLOCK, nullptr, &original_signal_mask) == 0); |
|
| 398 | + | wl_list_init(&pending_spawns); |
|
| 399 | + | monitor.ws = &workspace; |
|
| 400 | + | selmon = &monitor; |
|
| 401 | + | spawn(&(arg_t){ .v = command }); |
|
| 402 | + | assert(!wl_list_empty(&pending_spawns)); |
|
| 403 | + | ||
| 404 | + | for (i = 0; i < 100 && !wl_list_empty(&pending_spawns); i++) { |
|
| 405 | + | handle_signal(SIGCHLD, nullptr); |
|
| 406 | + | nanosleep(&delay, nullptr); |
|
| 407 | + | } |
|
| 408 | + | assert(wl_list_empty(&pending_spawns)); |
|
| 409 | + | selmon = nullptr; |
|
| 410 | + | } |
|
| 411 | + | ||
| 412 | + | /* Verify client commands remain safe when no client can receive them. */ |
|
| 413 | + | static void test_commands_without_clients(void) { |
|
| 414 | + | struct wlr_output output = {}; |
|
| 415 | + | monitor_t monitor = {}; |
|
| 416 | + | arg_t argument; |
|
| 417 | + | int i; |
|
| 418 | + | ||
| 419 | + | monitor.wlr_output = &output; |
|
| 420 | + | monitor.ws = &workspaces[0]; |
|
| 421 | + | workspaces[0].mon = &monitor; |
|
| 422 | + | workspaces[0].lt = &layouts[0]; |
|
| 423 | + | workspaces[0].prevlt = nullptr; |
|
| 424 | + | workspaces[0].v = workspaces[0].h = (stack_state_t){ 16, 1, 1 }; |
|
| 425 | + | ||
| 426 | + | for (i = 1; i < WSCOUNT; i++) |
|
| 427 | + | workspaces[i].mon = (monitor_t *)1; |
|
| 428 | + | wl_list_init(&clients); |
|
| 429 | + | wl_list_init(&fstack); |
|
| 430 | + | wl_list_init(&mons); |
|
| 431 | + | wl_list_init(&ws_managers); |
|
| 432 | + | selmon = &monitor; |
|
| 433 | + | unsetenv("XDG_RUNTIME_DIR"); |
|
| 434 | + | ||
| 435 | + | cycle_layout(nullptr); |
|
| 436 | + | assert(workspaces[0].lt == &layouts[1]); |
|
| 437 | + | assert(workspaces[0].prevlt == &layouts[0]); |
|
| 438 | + | toggle_max_stack(nullptr); |
|
| 439 | + | assert(workspaces[0].lt == &layouts[4]); |
|
| 440 | + | toggle_max_stack(nullptr); |
|
| 441 | + | assert(workspaces[0].lt == &layouts[1]); |
|
| 442 | + | argument.i = MASTER_GROW; |
|
| 443 | + | stack_config(&argument); |
|
| 444 | + | assert(workspaces[0].h.msize == 17); |
|
| 445 | + | argument.i = LAYOUT_FLIP; |
|
| 446 | + | stack_config(&argument); |
|
| 447 | + | assert(workspaces[0].lt == &layouts[3]); |
|
| 448 | + | ||
| 449 | + | assert(focus_top(&monitor) == nullptr); |
|
| 450 | + | focus_stack(&(arg_t){ .i = 1 }); |
|
| 451 | + | focus_main(nullptr); |
|
| 452 | + | focus_urgent(nullptr); |
|
| 453 | + | tag(&(arg_t){ .i = 0 }); |
|
| 454 | + | tag(&(arg_t){ .i = WSCOUNT }); |
|
| 455 | + | tag_monitor(&(arg_t){ .i = WLR_DIRECTION_LEFT }); |
|
| 456 | + | toggle_floating(nullptr); |
|
| 457 | + | toggle_fullscreen(nullptr); |
|
| 458 | + | kill_client(nullptr); |
|
| 459 | + | raise_client(nullptr); |
|
| 460 | + | zoom(nullptr); |
|
| 461 | + | argument.i = WORKSPACE_NEXT; |
|
| 462 | + | cycle_workspace(&argument); |
|
| 463 | + | assert(monitor.ws == &workspaces[0]); |
|
| 464 | + | view(&(arg_t){ .i = WSCOUNT }); |
|
| 465 | + | ||
| 466 | + | selmon = nullptr; |
|
| 467 | + | cycle_layout(nullptr); |
|
| 468 | + | toggle_max_stack(nullptr); |
|
| 469 | + | stack_config(&argument); |
|
| 470 | + | cycle_workspace(&argument); |
|
| 471 | + | view(&(arg_t){ .i = 0 }); |
|
| 472 | + | } |
|
| 473 | + | ||
| 474 | + | /* Verify small geometry, focus, inhibition, and listener helpers. */ |
|
| 475 | + | static void test_small_helpers(void) { |
|
| 476 | + | client_t client = { .geom = { -20, 100, 2, 2 }, .bw = 2 }; |
|
| 477 | + | struct wlr_box bounds = { 0, 0, 80, 60 }; |
|
| 478 | + | static_listener_t *slot; |
|
| 479 | + | struct wlr_seat fake_seat = {}; |
|
| 480 | + | ||
| 481 | + | apply_bounds(&client, &bounds); |
|
| 482 | + | assert(client.geom.x == 0 && client.geom.y == 55); |
|
| 483 | + | assert(client.geom.width == 5 && client.geom.height == 5); |
|
| 484 | + | seat = &fake_seat; |
|
| 485 | + | assert(shortcuts_inhibited() == 0); |
|
| 486 | + | assert(focus_close(nullptr) == nullptr); |
|
| 487 | + | slot = pool_take(&static_listener_pool); |
|
| 488 | + | assert(slot); |
|
| 489 | + | listener_release(&slot->listener); |
|
| 490 | + | } |
|
| 491 | + | ||
| 492 | + | /* Verify XDG and XWayland client accessors without a running compositor. */ |
|
| 493 | + | static void test_client_accessors(void) { |
|
| 494 | + | client_t client = {}; |
|
| 495 | + | struct wlr_xdg_surface xdg = { .geometry = { 3, 4, 50, 60 } }; |
|
| 496 | + | struct wlr_xdg_toplevel toplevel = {}; |
|
| 497 | + | struct wlr_xwayland_surface xwayland = { |
|
| 498 | + | .class = "x11-app", |
|
| 499 | + | .title = "X11 title", |
|
| 500 | + | .x = 7, |
|
| 501 | + | .y = 8, |
|
| 502 | + | .width = 90, |
|
| 503 | + | .height = 100, |
|
| 504 | + | .modal = true, |
|
| 505 | + | .override_redirect = true, |
|
| 506 | + | .fullscreen = true, |
|
| 507 | + | }; |
|
| 508 | + | xcb_size_hints_t size_hints = { |
|
| 509 | + | .min_width = 20, |
|
| 510 | + | .min_height = 20, |
|
| 511 | + | .max_width = 20, |
|
| 512 | + | .max_height = 40, |
|
| 513 | + | }; |
|
| 514 | + | struct wlr_box box; |
|
| 515 | + | char storage[MAX_COMMAND_SIZE], *expanded[MAX_COMMAND_ARGS]; |
|
| 516 | + | const char *const command[] = { "before-$SWM_TEST_EXPAND", "$SWM_TEST_MISSING", nullptr }; |
|
| 517 | + | ||
| 518 | + | init_xdg_client(&client, &xdg, &toplevel, "xdg-app", "XDG title"); |
|
| 519 | + | xdg.geometry = (struct wlr_box){ 3, 4, 50, 60 }; |
|
| 520 | + | client.geom = (struct wlr_box){ 1, 2, 70, 80 }; |
|
| 521 | + | client.bw = 2; |
|
| 522 | + | assert(!strcmp(client_get_appid(&client), "xdg-app")); |
|
| 523 | + | assert(!strcmp(client_get_title(&client), "XDG title")); |
|
| 524 | + | client_get_clip(&client, &box); |
|
| 525 | + | assert(box.x == 3 && box.y == 4 && box.width == 68 && box.height == 78); |
|
| 526 | + | client_get_geometry(&client, &box); |
|
| 527 | + | assert(box.x == 3 && box.y == 4 && box.width == 50 && box.height == 60); |
|
| 528 | + | assert(client_get_parent(&client) == nullptr); |
|
| 529 | + | assert(!client_is_float_type(&client)); |
|
| 530 | + | toplevel.current = (struct wlr_xdg_toplevel_state){ |
|
| 531 | + | .min_width = 10, |
|
| 532 | + | .min_height = 10, |
|
| 533 | + | .max_width = 10, |
|
| 534 | + | .max_height = 20, |
|
| 535 | + | }; |
|
| 536 | + | assert(client_is_float_type(&client)); |
|
| 537 | + | assert(!client_is_unmanaged(&client)); |
|
| 538 | + | toplevel.requested.fullscreen = true; |
|
| 539 | + | assert(client_wants_fullscreen(&client)); |
|
| 540 | + | ||
| 541 | + | client.type = X11; |
|
| 542 | + | client.surface.xwayland = &xwayland; |
|
| 543 | + | assert(client_set_bounds(&client, 10, 20) == 0); |
|
| 544 | + | assert(!strcmp(client_get_appid(&client), "x11-app")); |
|
| 545 | + | assert(!strcmp(client_get_title(&client), "X11 title")); |
|
| 546 | + | client_get_clip(&client, &box); |
|
| 547 | + | assert(box.x == 0 && box.y == 0); |
|
| 548 | + | client_get_geometry(&client, &box); |
|
| 549 | + | assert(box.x == 7 && box.y == 8 && box.width == 90 && box.height == 100); |
|
| 550 | + | assert(client_get_parent(&client) == nullptr); |
|
| 551 | + | assert(client_is_float_type(&client)); |
|
| 552 | + | assert(client_is_unmanaged(&client)); |
|
| 553 | + | assert(client_wants_fullscreen(&client)); |
|
| 554 | + | xwayland.modal = false; |
|
| 555 | + | xwayland.size_hints = &size_hints; |
|
| 556 | + | assert(client_is_float_type(&client)); |
|
| 557 | + | xwayland.class = xwayland.title = nullptr; |
|
| 558 | + | assert(!strcmp(client_get_appid(&client), "broken")); |
|
| 559 | + | assert(!strcmp(client_get_title(&client), "broken")); |
|
| 560 | + | client_set_resizing(&client, true); |
|
| 561 | + | client_set_suspended(&client, true); |
|
| 562 | + | ||
| 563 | + | setenv("SWM_TEST_EXPAND", "expanded value", 1); |
|
| 564 | + | unsetenv("SWM_TEST_MISSING"); |
|
| 565 | + | expand_argv(command, expanded, storage); |
|
| 566 | + | assert(!strcmp(expanded[0], "before-expanded value")); |
|
| 567 | + | assert(!strcmp(expanded[1], "")); |
|
| 568 | + | assert(expanded[2] == nullptr); |
|
| 569 | + | publish_windows(nullptr); |
|
| 570 | + | } |
|
| 571 | + | ||
| 572 | + | /* Verify configured autostart commands and child tracking with harmless stubs. */ |
|
| 573 | + | static void test_autostart_execution(void) { |
|
| 574 | + | const char *stub_path = getenv("SWM_TEST_STUB_PATH"); |
|
| 575 | + | const char *old_path = getenv("PATH"); |
|
| 576 | + | char *saved_path; |
|
| 577 | + | struct timespec delay = { 0, 1000000 }; |
|
| 578 | + | size_t i; |
|
| 579 | + | int pending; |
|
| 580 | + | ||
| 581 | + | assert(stub_path && old_path); |
|
| 582 | + | saved_path = strdup(old_path); |
|
| 583 | + | assert(saved_path); |
|
| 584 | + | assert(setenv("PATH", stub_path, 1) == 0); |
|
| 585 | + | assert(sigprocmask(SIG_BLOCK, nullptr, &original_signal_mask) == 0); |
|
| 586 | + | wl_list_init(&pending_spawns); |
|
| 587 | + | autostart_len = 0; |
|
| 588 | + | autostart_exec(); |
|
| 589 | + | assert(autostart_len > 0); |
|
| 590 | + | ||
| 591 | + | for (i = 0; i < 100; i++) { |
|
| 592 | + | pending = 0; |
|
| 593 | + | handle_signal(SIGCHLD, nullptr); |
|
| 594 | + | ||
| 595 | + | for (size_t j = 0; j < autostart_len; j++) |
|
| 596 | + | pending |= autostart_pids[j] > 0; |
|
| 597 | + | ||
| 598 | + | if (!pending) |
|
| 599 | + | break; |
|
| 600 | + | nanosleep(&delay, nullptr); |
|
| 601 | + | } |
|
| 602 | + | assert(!pending); |
|
| 603 | + | assert(setenv("PATH", saved_path, 1) == 0); |
|
| 604 | + | free(saved_path); |
|
| 605 | + | } |
|
| 606 | + | ||
| 607 | + | /* Verify forwarding of every pointer-gesture event family. */ |
|
| 608 | + | static void test_pointer_gestures(void) { |
|
| 609 | + | struct wlr_pointer_swipe_begin_event swipe_begin = { .time_msec = 1, .fingers = 3 }; |
|
| 610 | + | struct wlr_pointer_swipe_update_event swipe_update = { .time_msec = 2, .dx = 1, .dy = -1 }; |
|
| 611 | + | struct wlr_pointer_swipe_end_event swipe_end = { .time_msec = 3, .cancelled = false }; |
|
| 612 | + | struct wlr_pointer_pinch_begin_event pinch_begin = { .time_msec = 4, .fingers = 2 }; |
|
| 613 | + | struct wlr_pointer_pinch_update_event pinch_update = { |
|
| 614 | + | .time_msec = 5, |
|
| 615 | + | .dx = 1, |
|
| 616 | + | .dy = 2, |
|
| 617 | + | .scale = 1.1, |
|
| 618 | + | .rotation = 3, |
|
| 619 | + | }; |
|
| 620 | + | struct wlr_pointer_pinch_end_event pinch_end = { .time_msec = 6, .cancelled = true }; |
|
| 621 | + | struct wlr_pointer_hold_begin_event hold_begin = { .time_msec = 7, .fingers = 2 }; |
|
| 622 | + | struct wlr_pointer_hold_end_event hold_end = { .time_msec = 8, .cancelled = false }; |
|
| 623 | + | ||
| 624 | + | dpy = wl_display_create(); |
|
| 625 | + | assert(dpy); |
|
| 626 | + | seat = wlr_seat_create(dpy, "test-seat"); |
|
| 627 | + | idle_notifier = wlr_idle_notifier_v1_create(dpy); |
|
| 628 | + | pointer_gestures = wlr_pointer_gestures_v1_create(dpy); |
|
| 629 | + | assert(seat && idle_notifier && pointer_gestures); |
|
| 630 | + | gesture_swipe_begin(nullptr, &swipe_begin); |
|
| 631 | + | gesture_swipe_update(nullptr, &swipe_update); |
|
| 632 | + | gesture_swipe_end(nullptr, &swipe_end); |
|
| 633 | + | gesture_pinch_begin(nullptr, &pinch_begin); |
|
| 634 | + | gesture_pinch_update(nullptr, &pinch_update); |
|
| 635 | + | gesture_pinch_end(nullptr, &pinch_end); |
|
| 636 | + | gesture_hold_begin(nullptr, &hold_begin); |
|
| 637 | + | gesture_hold_end(nullptr, &hold_end); |
|
| 638 | + | wl_display_destroy(dpy); |
|
| 639 | + | dpy = nullptr; |
|
| 640 | + | } |
|
| 641 | + | ||
| 642 | + | /* Verify text-input and input-method wrappers are created and released. */ |
|
| 643 | + | static void test_input_method_lifecycle(void) { |
|
| 644 | + | struct wlr_seat fake_seat = {}; |
|
| 645 | + | struct wlr_text_input_manager_v3 fake_ti_manager = {}; |
|
| 646 | + | struct wlr_text_input_v3 text_input = {}; |
|
| 647 | + | struct wlr_input_method_v2 method = {}; |
|
| 648 | + | text_input_t *wrapper = nullptr; |
|
| 649 | + | size_t i; |
|
| 650 | + | ||
| 651 | + | seat = &fake_seat; |
|
| 652 | + | ti_mgr = &fake_ti_manager; |
|
| 653 | + | wl_list_init(&fake_ti_manager.text_inputs); |
|
| 654 | + | assert(input_method_focused_text_input() == nullptr); |
|
| 655 | + | input_method = nullptr; |
|
| 656 | + | input_method_send_state(&text_input); |
|
| 657 | + | input_method_commit_notify(nullptr, &method); |
|
| 658 | + | ||
| 659 | + | wl_signal_init(&text_input.events.enable); |
|
| 660 | + | wl_signal_init(&text_input.events.commit); |
|
| 661 | + | wl_signal_init(&text_input.events.disable); |
|
| 662 | + | wl_signal_init(&text_input.events.destroy); |
|
| 663 | + | text_input_create_notify(nullptr, &text_input); |
|
| 664 | + | ||
| 665 | + | for (i = 0; i < LENGTH(text_input_used); i++) |
|
| 666 | + | ||
| 667 | + | if (text_input_used[i]) { |
|
| 668 | + | wrapper = &text_input_items[i]; |
|
| 669 | + | break; |
|
| 670 | + | } |
|
| 671 | + | assert(wrapper && wrapper->ti == &text_input); |
|
| 672 | + | text_input_enable_notify(&wrapper->enable, nullptr); |
|
| 673 | + | text_input_commit_notify(&wrapper->commit, nullptr); |
|
| 674 | + | text_input_disable_notify(&wrapper->disable, nullptr); |
|
| 675 | + | text_input_destroy_notify(&wrapper->destroy, nullptr); |
|
| 676 | + | assert(!text_input_used[wrapper - text_input_items]); |
|
| 677 | + | ||
| 678 | + | wl_signal_init(&method.events.commit); |
|
| 679 | + | wl_signal_init(&method.events.destroy); |
|
| 680 | + | wl_signal_init(&method.events.grab_keyboard); |
|
| 681 | + | wl_signal_init(&method.events.new_popup_surface); |
|
| 682 | + | input_method_create_notify(nullptr, &method); |
|
| 683 | + | assert(input_method == &method); |
|
| 684 | + | input_method_destroy_notify(nullptr, nullptr); |
|
| 685 | + | assert(input_method == nullptr); |
|
| 686 | + | } |
|
| 687 | + | ||
| 688 | + | /* Verify input handlers safely reject incomplete or inapplicable state. */ |
|
| 689 | + | static void test_input_early_paths(void) { |
|
| 690 | + | struct wlr_seat fake_seat = {}; |
|
| 691 | + | struct wlr_seat_pointer_request_set_cursor_event cursor_event = {}; |
|
| 692 | + | struct wlr_cursor_shape_manager_v1_request_set_shape_event shape_event = {}; |
|
| 693 | + | struct wlr_drag drag = {}; |
|
| 694 | + | keyboard_group_t group = {}; |
|
| 695 | + | monitor_t monitor = {}; |
|
| 696 | + | workspace_t workspace = {}; |
|
| 697 | + | struct wlr_output output = {}; |
|
| 698 | + | ||
| 699 | + | seat = &fake_seat; |
|
| 700 | + | cursor_mode = CURSOR_MOVE; |
|
| 701 | + | set_cursor(nullptr, &cursor_event); |
|
| 702 | + | set_cursor_shape(nullptr, &shape_event); |
|
| 703 | + | start_drag(nullptr, &drag); |
|
| 704 | + | assert(key_repeat(&group) == 0); |
|
| 705 | + | assert(key_binding(0, XKB_KEY_NoSymbol, 0) == 0); |
|
| 706 | + | ||
| 707 | + | wl_list_init(&clients); |
|
| 708 | + | wl_list_init(&fstack); |
|
| 709 | + | monitor.wlr_output = &output; |
|
| 710 | + | monitor.ws = &workspace; |
|
| 711 | + | workspace.mon = &monitor; |
|
| 712 | + | workspace.lt = &layouts[0]; |
|
| 713 | + | workspace.v = workspace.h = (stack_state_t){ 16, 1, 1 }; |
|
| 714 | + | master_left(&monitor); |
|
| 715 | + | master_right(&monitor); |
|
| 716 | + | master_top(&monitor); |
|
| 717 | + | master_bottom(&monitor); |
|
| 718 | + | max_stack(&monitor); |
|
| 719 | + | move_resize(&(arg_t){ .u = CURSOR_MOVE }); |
|
| 720 | + | } |
|
| 721 | + | ||
| 722 | + | /* Assert that the program exits with the requested status. */ |
|
| 723 | + | static void assert_program_exits(int argc, char **argv, int clear_runtime, int code) { |
|
| 724 | + | int status; |
|
| 725 | + | pid_t pid = fork(); |
|
| 726 | + | ||
| 727 | + | assert(pid >= 0); |
|
| 728 | + | ||
| 729 | + | if (pid == 0) { |
|
| 730 | + | close(STDERR_FILENO); |
|
| 731 | + | ||
| 732 | + | if (clear_runtime) |
|
| 733 | + | unsetenv("XDG_RUNTIME_DIR"); |
|
| 734 | + | optind = 1; |
|
| 735 | + | _exit(swm_program_main(argc, argv)); |
|
| 736 | + | } |
|
| 737 | + | assert(waitpid(pid, &status, 0) == pid); |
|
| 738 | + | assert(WIFEXITED(status) && WEXITSTATUS(status) == code); |
|
| 739 | + | } |
|
| 740 | + | ||
| 741 | + | /* Assert that the program exits with failure status. */ |
|
| 742 | + | static void assert_program_fails(int argc, char **argv, int clear_runtime) { |
|
| 743 | + | assert_program_exits(argc, argv, clear_runtime, 1); |
|
| 744 | + | } |
|
| 745 | + | ||
| 746 | + | /* Verify version output and invalid command-line invocations. */ |
|
| 747 | + | static void test_command_line_errors(void) { |
|
| 748 | + | char *version[] = { "swm", "-v", nullptr }; |
|
| 749 | + | char *help[] = { "swm", "-h", nullptr }; |
|
| 750 | + | char *extra[] = { "swm", "extra", nullptr }; |
|
| 751 | + | char *missing_runtime[] = { "swm", nullptr }; |
|
| 752 | + | ||
| 753 | + | assert_program_exits(2, version, 0, 0); /* -v prints the version and succeeds. */ |
|
| 754 | + | assert_program_fails(2, help, 0); |
|
| 755 | + | assert_program_fails(2, extra, 0); |
|
| 756 | + | assert_program_fails(1, missing_runtime, 1); |
|
| 757 | + | } |
|
| 758 | + | ||
| 759 | + | /* Verify pointer-constraint and decoration lifecycle handlers. */ |
|
| 760 | + | static void test_pointer_and_decoration_lifecycle(void) { |
|
| 761 | + | struct wlr_pointer_constraint_v1 constraint = {}; |
|
| 762 | + | struct wlr_surface surface = {}; |
|
| 763 | + | pointer_constraint_t *wrapper = nullptr; |
|
| 764 | + | struct wlr_xdg_surface xdg = {}; |
|
| 765 | + | struct wlr_xdg_toplevel toplevel = {}; |
|
| 766 | + | struct wlr_xdg_toplevel_decoration_v1 decoration = {}; |
|
| 767 | + | client_t client = {}; |
|
| 768 | + | struct wlr_seat fake_seat = {}; |
|
| 769 | + | size_t i; |
|
| 770 | + | ||
| 771 | + | seat = &fake_seat; |
|
| 772 | + | constraint.surface = &surface; |
|
| 773 | + | wl_signal_init(&constraint.events.destroy); |
|
| 774 | + | create_pointer_constraint(nullptr, &constraint); |
|
| 775 | + | ||
| 776 | + | for (i = 0; i < LENGTH(pointer_constraint_used); i++) |
|
| 777 | + | ||
| 778 | + | if (pointer_constraint_used[i]) { |
|
| 779 | + | wrapper = &pointer_constraint_items[i]; |
|
| 780 | + | break; |
|
| 781 | + | } |
|
| 782 | + | assert(wrapper && wrapper->constraint == &constraint); |
|
| 783 | + | destroy_pointer_constraint(&wrapper->destroy, nullptr); |
|
| 784 | + | assert(!pointer_constraint_used[wrapper - pointer_constraint_items]); |
|
| 785 | + | cursor_constrain(nullptr); |
|
| 786 | + | cursor_constrain(nullptr); |
|
| 787 | + | ||
| 788 | + | client.type = XDG_SHELL; |
|
| 789 | + | client.surface.xdg = &xdg; |
|
| 790 | + | client.decoration = &decoration; |
|
| 791 | + | xdg.toplevel = &toplevel; |
|
| 792 | + | toplevel.base = &xdg; |
|
| 793 | + | decoration.toplevel = &toplevel; |
|
| 794 | + | decoration.requested_mode = WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE; |
|
| 795 | + | request_decoration_mode(&client.set_decoration_mode, nullptr); |
|
| 796 | + | assert(client.bw == 0); |
|
| 797 | + | maximize_notify(&client.maximize, nullptr); |
|
| 798 | + | create_decoration(nullptr, &decoration); |
|
| 799 | + | urgent(nullptr, &(struct wlr_xdg_activation_v1_request_activate_event){}); |
|
| 800 | + | } |
|
| 801 | + | ||
| 802 | + | /* Verify modifier handling, repeatable bindings, and key-repeat dispatch. */ |
|
| 803 | + | static void test_keybindings_and_repeat(void) { |
|
| 804 | + | struct wlr_seat fake_seat = {}; |
|
| 805 | + | struct wlr_keyboard_group keyboard_group = {}; |
|
| 806 | + | keyboard_group_t group = {}; |
|
| 807 | + | struct wl_event_loop *loop; |
|
| 808 | + | xkb_keysym_t symbol = XKB_KEY_j; |
|
| 809 | + | ||
| 810 | + | seat = &fake_seat; |
|
| 811 | + | selmon = nullptr; |
|
| 812 | + | wl_list_init(&fstack); |
|
| 813 | + | assert(key_binding(MOD, XKB_KEY_1, false) == 1); |
|
| 814 | + | assert(key_binding(MOD | WLR_MODIFIER_CAPS, XKB_KEY_1, false) == 1); |
|
| 815 | + | assert(key_binding(MOD, XKB_KEY_1, true) == 0); |
|
| 816 | + | assert(key_binding(MOD, XKB_KEY_j, true) == 2); |
|
| 817 | + | assert(key_binding(0, XKB_KEY_NoSymbol, 0) == 0); |
|
| 818 | + | ||
| 819 | + | loop = wl_event_loop_create(); |
|
| 820 | + | assert(loop); |
|
| 821 | + | group.wlr_group = &keyboard_group; |
|
| 822 | + | group.nsyms = 1; |
|
| 823 | + | group.keysyms = &symbol; |
|
| 824 | + | group.mods = MOD; |
|
| 825 | + | keyboard_group.keyboard.repeat_info.rate = 25; |
|
| 826 | + | group.key_repeat_source = wl_event_loop_add_timer(loop, key_repeat, &group); |
|
| 827 | + | assert(group.key_repeat_source); |
|
| 828 | + | assert(key_repeat(&group) == 0); |
|
| 829 | + | wl_event_source_remove(group.key_repeat_source); |
|
| 830 | + | wl_event_loop_destroy(loop); |
|
| 831 | + | } |
|
| 832 | + | ||
| 833 | + | /* Name one compositor-internal unit test callable by the Python runner. */ |
|
| 834 | + | typedef struct { |
|
| 835 | + | const char *name; |
|
| 836 | + | void (*run)(void); |
|
| 837 | + | } internal_test_t; |
|
| 838 | + | ||
| 839 | + | /* Run the requested compositor-internal unit test. */ |
|
| 840 | + | int main(int argc, char **argv) { |
|
| 841 | + | static const internal_test_t tests[] = { |
|
| 842 | + | { "geometry", test_geometry }, |
|
| 843 | + | { "state", test_state }, |
|
| 844 | + | { "parsing", test_parsing }, |
|
| 845 | + | { "layouts", test_layouts }, |
|
| 846 | + | { "utilities", test_utilities }, |
|
| 847 | + | { "pools", test_pools }, |
|
| 848 | + | { "workspaces", test_workspace_queries }, |
|
| 849 | + | { "paths-and-borders", test_paths_and_border_policy }, |
|
| 850 | + | { "rules", test_rule_application }, |
|
| 851 | + | { "focus", test_focus_queries }, |
|
| 852 | + | { "window-state", test_window_state_storage }, |
|
| 853 | + | { "child-reaping", test_child_reaping }, |
|
| 854 | + | { "spawn-tracking", test_spawn_tracking }, |
|
| 855 | + | { "commands-without-clients", test_commands_without_clients }, |
|
| 856 | + | { "small-helpers", test_small_helpers }, |
|
| 857 | + | { "client-accessors", test_client_accessors }, |
|
| 858 | + | { "autostart", test_autostart_execution }, |
|
| 859 | + | { "pointer-gestures", test_pointer_gestures }, |
|
| 860 | + | { "input-method", test_input_method_lifecycle }, |
|
| 861 | + | { "input-early-paths", test_input_early_paths }, |
|
| 862 | + | { "command-line", test_command_line_errors }, |
|
| 863 | + | { "pointer-and-decoration", test_pointer_and_decoration_lifecycle }, |
|
| 864 | + | { "keybindings-and-repeat", test_keybindings_and_repeat }, |
|
| 865 | + | }; |
|
| 866 | + | ||
| 867 | + | if (argc == 2 && !strcmp(argv[1], "--list")) { |
|
| 868 | + | for (size_t i = 0; i < LENGTH(tests); i++) |
|
| 869 | + | puts(tests[i].name); |
|
| 870 | + | return 0; |
|
| 871 | + | } |
|
| 872 | + | if (argc != 2) { |
|
| 873 | + | fprintf(stderr, "usage: unit --list|TEST\n"); |
|
| 874 | + | return 2; |
|
| 875 | + | } |
|
| 876 | + | for (size_t i = 0; i < LENGTH(tests); i++) { |
|
| 877 | + | if (!strcmp(argv[1], tests[i].name)) { |
|
| 878 | + | tests[i].run(); |
|
| 879 | + | return 0; |
|
| 880 | + | } |
|
| 881 | + | } |
|
| 882 | + | fprintf(stderr, "unknown internal test: %s\n", argv[1]); |
|
| 883 | + | return 2; |
|
| 884 | + | } |