test: parallel-safe suite (xdist), shared globals session, KICK_DUMP_DIR

Make ./test.sh -n4 work (~47s vs ~156s serial):

- kick.c: KICK_DUMP_DIR env override for frame-dump .ppm paths
  (default /tmp); per-boot dump_seq counters no longer collide
  across concurrent compositors.
- tests: every spawn gets a unique log file and a per-session dump
  directory owned by the Session (close removes the tree instead of
  globbing /tmp, which raced parallel workers' live dumps).
- 14 protocol-globals tests share one module-scoped keeper session
  via short-lived --globals-out clients instead of 14 boots.
- Two presence-waits converted from fixed sleeps to event waits.
- Harden the fullscreen round-trip: wait for seat keyboard focus
  ([diag] kbd-enter, new debug line in window_enter) before tapping
  F -- map alone does not guarantee enter under load and the tap was
  silently dropped.

Serial remains the deterministic gate (documented in test.sh and
README): a few pre-existing timing-sensitive tests can still flake
under parallel load; they pass alone and serially.
This commit is contained in:
Andrew Hamilton 2026-09-08 11:50:45 +10:00
parent a593d83b37
commit 7e2bb18a85
4 changed files with 107 additions and 71 deletions

View file

@ -347,6 +347,13 @@ Known-flaky tests are marked `@pytest.mark.flaky`. `pytest.ini` defaults to
`-m "not flaky"`, so they are excluded from every plain run (`./test.sh` or `-m "not flaky"`, so they are excluded from every plain run (`./test.sh` or
direct `pytest`). Run them on demand with `./test.sh -m flaky`. direct `pytest`). Run them on demand with `./test.sh -m flaky`.
Parallel runs: `./test.sh -n4` splits the suite across workers
(pytest-xdist), cutting wall time ~3x (156s to ~47s here). Every spawn
uses unique log paths and per-session dump directories (`KICK_DUMP_DIR`),
and sockets are pid-unique, so workers stay isolated. The serial run
remains the deterministic gate: a few timing-sensitive tests can flake
under parallel load (they pass alone and serially).
## Cursor shapes (`cursor-shape-v1`) ## Cursor shapes (`cursor-shape-v1`)
Kick advertises the `cursor-shape-v1` protocol (as `wp_cursor_shape_manager_v1`, Kick advertises the `cursor-shape-v1` protocol (as `wp_cursor_shape_manager_v1`,

40
kick.c
View file

@ -894,6 +894,14 @@ static void bridge_attach_seat(void);
static volatile sig_atomic_t dump_scene_req, dump_host_req; static volatile sig_atomic_t dump_scene_req, dump_host_req;
static int dump_seq; static int dump_seq;
static bool client_dumped = false; static bool client_dumped = false;
/* Directory for dump .ppm files (KICK_DUMP_DIR, default /tmp): parallel
test workers point each compositor at its own directory so per-boot
dump_seq counters cannot collide on filenames. */
static char dump_dir[512] = "/tmp";
static void dump_path(char *buf, size_t sz, const char *kind, int seq) {
snprintf(buf, sz, "%s/%s_%d.ppm", dump_dir, kind, seq);
}
/* Set by sigchld_handler whenever a child exits; the main loop clears it and /* Set by sigchld_handler whenever a child exits; the main loop clears it and
runs sweep_dead_startup_groups() to drop black tabs whose command exited runs sweep_dead_startup_groups() to drop black tabs whose command exited
@ -3000,6 +3008,12 @@ static void window_enter(struct kick_window *w) {
if (!kbd) kbd = &server.keyboard; if (!kbd) kbd = &server.keyboard;
wlr_seat_keyboard_notify_enter(server.seat, surface, wlr_seat_keyboard_notify_enter(server.seat, surface,
kbd->keycodes, kbd->num_keycodes, &kbd->modifiers); kbd->keycodes, kbd->num_keycodes, &kbd->modifiers);
/* Seat keyboard focus is what routes keys to the client; map alone
does not guarantee it has arrived (enter can lag map under
load), so tests tapping keys wait on this line, not the map. */
if (kick_log_level() >= KICK_LOG_DEBUG)
kick_log(KICK_LOG_DEBUG, "[diag] kbd-enter app=%d\n",
tab_index(tab));
} }
show_window(w->tree ? w : NULL); show_window(w->tree ? w : NULL);
kick_request_redraw(); kick_request_redraw();
@ -3500,9 +3514,8 @@ static void handle_toplevel_commit(struct wl_listener *listener, void *data) {
"nonblack=%lu/%lu mean=%lu\n", fmt, nonblack, "nonblack=%lu/%lu mean=%lu\n", fmt, nonblack,
(unsigned long)bw * bh, (unsigned long)bw * bh,
lum / ((unsigned long)bw * bh + 1)); lum / ((unsigned long)bw * bh + 1));
char cpath[64]; char cpath[512];
snprintf(cpath, sizeof(cpath), "/tmp/client_%d.ppm", dump_path(cpath, sizeof(cpath), "client", dump_seq);
dump_seq);
FILE *cf = fopen(cpath, "wb"); FILE *cf = fopen(cpath, "wb");
if (cf) { if (cf) {
fprintf(cf, "P6\n%d %d\n255\n", bw, bh); fprintf(cf, "P6\n%d %d\n255\n", bw, bh);
@ -8081,8 +8094,8 @@ static void dump_scene_buffer(struct wlr_buffer *buf) {
kick_log(KICK_LOG_INFO, "[dump] scene %dx%d fmt=0x%x " kick_log(KICK_LOG_INFO, "[dump] scene %dx%d fmt=0x%x "
"nonblack=%lu/%lu mean=%lu\n", w, h, format, nonblack, "nonblack=%lu/%lu mean=%lu\n", w, h, format, nonblack,
(unsigned long)w * h, lum / ((unsigned long)w * h + 1)); (unsigned long)w * h, lum / ((unsigned long)w * h + 1));
char path[64]; char path[512];
snprintf(path, sizeof(path), "/tmp/scene_%d.ppm", dump_seq); dump_path(path, sizeof(path), "scene", dump_seq);
FILE *f = fopen(path, "wb"); FILE *f = fopen(path, "wb");
if (f) { if (f) {
int w = buf->width, h = buf->height; int w = buf->width, h = buf->height;
@ -8124,8 +8137,8 @@ static void dump_host_back(void) {
kick_log(KICK_LOG_INFO, "[dump] host %dx%d nonblack=%lu/%lu mean=%lu\n", kick_log(KICK_LOG_INFO, "[dump] host %dx%d nonblack=%lu/%lu mean=%lu\n",
w, h, nonblack, (unsigned long)w * h, w, h, nonblack, (unsigned long)w * h,
lum / ((unsigned long)w * h + 1)); lum / ((unsigned long)w * h + 1));
char path[64]; char path[512];
snprintf(path, sizeof(path), "/tmp/host_%d.ppm", dump_seq); dump_path(path, sizeof(path), "host", dump_seq);
FILE *f = fopen(path, "wb"); FILE *f = fopen(path, "wb");
if (f) { if (f) {
fprintf(f, "P6\n%d %d\n255\n", w, h); fprintf(f, "P6\n%d %d\n255\n", w, h);
@ -8195,8 +8208,8 @@ static void dump_standalone_buffer(struct wlr_buffer *buf) {
kick_log(KICK_LOG_INFO, "[dump] host %dx%d nonblack=%lu/%lu mean=%lu\n", kick_log(KICK_LOG_INFO, "[dump] host %dx%d nonblack=%lu/%lu mean=%lu\n",
w, h, nonblack, (unsigned long)w * h, w, h, nonblack, (unsigned long)w * h,
lum / ((unsigned long)w * h + 1)); lum / ((unsigned long)w * h + 1));
char path[64]; char path[512];
snprintf(path, sizeof(path), "/tmp/host_%d.ppm", dump_seq); dump_path(path, sizeof(path), "host", dump_seq);
FILE *f = fopen(path, "wb"); FILE *f = fopen(path, "wb");
if (f) { if (f) {
fprintf(f, "P6\n%d %d\n255\n", w, h); fprintf(f, "P6\n%d %d\n255\n", w, h);
@ -11174,6 +11187,15 @@ int main(int argc, char *argv[]) {
level without touching non-async-signal-safe functions. */ level without touching non-async-signal-safe functions. */
kick_log_level_init(); kick_log_level_init();
/* KICK_DUMP_DIR: where frame-dump .ppm files land (default /tmp).
Test workers point each compositor at its own directory so the
per-boot dump_seq counters cannot collide on filenames. */
{
const char *d = getenv("KICK_DUMP_DIR");
if (d && d[0])
snprintf(dump_dir, sizeof(dump_dir), "%s", d);
}
/* KICK_SYNTH_INPUT: keep real host keyboard events out of the test /* KICK_SYNTH_INPUT: keep real host keyboard events out of the test
channel so the stdin feed is the only source of key/modifier input. */ channel so the stdin feed is the only source of key/modifier input. */
{ {

View file

@ -1,4 +1,10 @@
#!/bin/sh #!/bin/sh
# Test runner: ./test.sh [pytest args].
# Serial (`./test.sh`) is the deterministic gate. For iteration speed,
# `./test.sh -n4` runs workers in parallel (~3x faster here); workers are
# isolated (unique logs, per-session KICK_DUMP_DIR, pid-unique sockets),
# but a few timing-sensitive tests may flake under load -- re-run serially
# to confirm.
set -e set -e

View file

@ -19,7 +19,6 @@ Known-flaky tests are marked @pytest.mark.flaky; pytest.ini's default
""" """
import ctypes import ctypes
import glob
import os import os
import re import re
import shutil import shutil
@ -59,6 +58,15 @@ def _fresh_log():
legacy default path (used when a caller passes log= explicitly).""" legacy default path (used when a caller passes log= explicitly)."""
_log_seq[0] += 1 _log_seq[0] += 1
return "/tmp/tabtest-p%d-%d.log" % (os.getpid(), _log_seq[0]) return "/tmp/tabtest-p%d-%d.log" % (os.getpid(), _log_seq[0])
def _mk_dump_dir():
"""A fresh per-spawn dump directory (KICK_DUMP_DIR).
Parallel workers' compositors must not share /tmp dump filenames:
every boot restarts the dump_seq counter, so host_0.ppm etc. would
collide. The owning Session removes the tree in close()."""
return tempfile.mkdtemp(prefix="kick-dump-")
RED = "#ff0000" RED = "#ff0000"
BLUE = "#0000ff" BLUE = "#0000ff"
GREEN = "#00ff00" GREEN = "#00ff00"
@ -191,9 +199,10 @@ def _preexec_death(expected_ppid):
class Session: class Session:
"""A running kick instance plus helpers.""" """A running kick instance plus helpers."""
def __init__(self, proc, log=None): def __init__(self, proc, log=None, dump_dir=None):
self.proc = proc self.proc = proc
self._log = log or _fresh_log() self._log = log or _fresh_log()
self._dump_dir = dump_dir
self._socket = None self._socket = None
# -- log watching ---------------------------------------------------- # -- log watching ----------------------------------------------------
@ -560,8 +569,12 @@ class Session:
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
self.proc.kill() self.proc.kill()
self.proc.wait() self.proc.wait()
for ppm in glob.glob("/tmp/host_*.ppm") + glob.glob("/tmp/scene_*.ppm"): # Each session owns its dump directory (see _mk_dump_dir): remove
os.unlink(ppm) # the whole tree instead of globbing /tmp, which would race
# parallel workers' live dumps.
if self._dump_dir:
shutil.rmtree(self._dump_dir, ignore_errors=True)
self._dump_dir = None
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@ -572,15 +585,17 @@ def session():
if not os.access(path, os.X_OK): if not os.access(path, os.X_OK):
pytest.skip(f"build first: {path}") pytest.skip(f"build first: {path}")
log = _fresh_log() log = _fresh_log()
dump_dir = _mk_dump_dir()
proc = subprocess.Popen( proc = subprocess.Popen(
["env", "KICK_LOG=2", "KICK_SYNTH_INPUT=1", "stdbuf", "-oL", ["env", "KICK_LOG=2", "KICK_SYNTH_INPUT=1",
"KICK_DUMP_DIR=" + dump_dir, "stdbuf", "-oL",
BIN, f"{WL} '{RED}'", f"{WL} '{BLUE}'"], BIN, f"{WL} '{RED}'", f"{WL} '{BLUE}'"],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=open(log, "ab"), stderr=subprocess.STDOUT, stdout=open(log, "ab"), stderr=subprocess.STDOUT,
preexec_fn=_preexec_death(os.getpid()), preexec_fn=_preexec_death(os.getpid()),
) )
s = Session(proc, log) s = Session(proc, log, dump_dir)
try: try:
yield s yield s
finally: finally:
@ -600,6 +615,7 @@ def standalone_session():
if not os.access(path, os.X_OK): if not os.access(path, os.X_OK):
pytest.skip(f"build first: {path}") pytest.skip(f"build first: {path}")
log = _fresh_log() log = _fresh_log()
dump_dir = _mk_dump_dir()
# Clear any leftover XDG/compositor interaction: standalone must not # Clear any leftover XDG/compositor interaction: standalone must not
# connect to the host compositor (it would pick the wayland backend and # connect to the host compositor (it would pick the wayland backend and
@ -622,13 +638,14 @@ def standalone_session():
"WLR_BACKENDS=headless", "WLR_BACKENDS=headless",
"KICK_FONT=" + (env.get("KICK_FONT") or _pick_font() or ""), "KICK_FONT=" + (env.get("KICK_FONT") or _pick_font() or ""),
"XCURSOR_THEME=Adwaita", "XCURSOR_THEME=Adwaita",
"KICK_DUMP_DIR=" + dump_dir,
"stdbuf", "-oL", "stdbuf", "-oL",
BIN, f"{WL} '{RED}'", f"{WL} '{BLUE}'"], BIN, f"{WL} '{RED}'", f"{WL} '{BLUE}'"],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=open(log, "ab"), stderr=subprocess.STDOUT, stdout=open(log, "ab"), stderr=subprocess.STDOUT,
preexec_fn=_preexec_death(os.getpid()), preexec_fn=_preexec_death(os.getpid()),
) )
s = Session(proc, log) s = Session(proc, log, dump_dir)
try: try:
yield s yield s
finally: finally:
@ -1116,6 +1133,7 @@ def _spawn_standalone(extra_env, *apps, log=None):
interleaving their logs; the default is a fresh per-spawn file (see interleaving their logs; the default is a fresh per-spawn file (see
_fresh_log), so parallel workers never share a log.""" _fresh_log), so parallel workers never share a log."""
log = log or _fresh_log() log = log or _fresh_log()
dump_dir = _mk_dump_dir()
env = dict(os.environ) env = dict(os.environ)
env["KICK_LOG"] = "2" env["KICK_LOG"] = "2"
env["KICK_MODE"] = "standalone" env["KICK_MODE"] = "standalone"
@ -1124,6 +1142,7 @@ def _spawn_standalone(extra_env, *apps, log=None):
env.pop("XCURSOR_THEME", None) env.pop("XCURSOR_THEME", None)
env.pop("XCURSOR_PATH", None) env.pop("XCURSOR_PATH", None)
env.update(extra_env or {}) env.update(extra_env or {})
env["KICK_DUMP_DIR"] = dump_dir
cmd = ["env", "-i", cmd = ["env", "-i",
"PATH=" + env["PATH"], "PATH=" + env["PATH"],
"HOME=" + env.get("HOME", "/"), "HOME=" + env.get("HOME", "/"),
@ -1143,7 +1162,7 @@ def _spawn_standalone(extra_env, *apps, log=None):
cmd, stdin=subprocess.PIPE, cmd, stdin=subprocess.PIPE,
stdout=open(log, "ab"), stderr=subprocess.STDOUT, stdout=open(log, "ab"), stderr=subprocess.STDOUT,
preexec_fn=_preexec_death(os.getpid())) preexec_fn=_preexec_death(os.getpid()))
return Session(proc, log) return Session(proc, log, dump_dir)
def _spawn_nested_in(outer, *apps, log=None): def _spawn_nested_in(outer, *apps, log=None):
@ -1152,9 +1171,11 @@ def _spawn_nested_in(outer, *apps, log=None):
backend, so data-control/primary bridge paths are testable without a backend, so data-control/primary bridge paths are testable without a
labwc/sway host. The inner session gets its own log file.""" labwc/sway host. The inner session gets its own log file."""
log = log or _fresh_log() log = log or _fresh_log()
dump_dir = _mk_dump_dir()
env = dict(os.environ) env = dict(os.environ)
env["KICK_LOG"] = "2" env["KICK_LOG"] = "2"
env["KICK_SYNTH_INPUT"] = "1" env["KICK_SYNTH_INPUT"] = "1"
env["KICK_DUMP_DIR"] = dump_dir
env["WAYLAND_DISPLAY"] = outer.socket env["WAYLAND_DISPLAY"] = outer.socket
# The outer kick advertises no server decorations; take the direct # The outer kick advertises no server decorations; take the direct
# xdg-shell path instead of libdecor (which also needs KICK_LIBDECOR # xdg-shell path instead of libdecor (which also needs KICK_LIBDECOR
@ -1167,7 +1188,7 @@ def _spawn_nested_in(outer, *apps, log=None):
env=env, stdin=subprocess.PIPE, env=env, stdin=subprocess.PIPE,
stdout=open(log, "ab"), stderr=subprocess.STDOUT, stdout=open(log, "ab"), stderr=subprocess.STDOUT,
preexec_fn=_preexec_death(os.getpid())) preexec_fn=_preexec_death(os.getpid()))
return Session(proc, log) return Session(proc, log, dump_dir)
def _wait_file(path, timeout=15.0): def _wait_file(path, timeout=15.0):
@ -2528,6 +2549,9 @@ def test_client_fullscreen_round_trip(session):
watch both directions of the mirror.""" watch both directions of the mirror."""
idx = session.wait_any_active() idx = session.wait_any_active()
session.wait_surface_mapped(idx) session.wait_surface_mapped(idx)
# Tapping F before seat keyboard focus reaches the surface drops the
# key silently; map alone does not guarantee enter under load.
session.wait_for(r"\[diag\] kbd-enter app=%d\b" % idx)
session.tap(F) session.tap(F)
session.wait_for("host fullscreen=1") session.wait_for("host fullscreen=1")
session.wait_for("clients fullscreen=1") session.wait_for("clients fullscreen=1")
@ -2773,14 +2797,16 @@ def _nested_session(*tabs, extra_args=()):
if not os.access(path, os.X_OK): if not os.access(path, os.X_OK):
pytest.skip(f"build first: {path}") pytest.skip(f"build first: {path}")
log = _fresh_log() log = _fresh_log()
dump_dir = _mk_dump_dir()
proc = subprocess.Popen( proc = subprocess.Popen(
["env", "KICK_LOG=2", "KICK_SYNTH_INPUT=1", "stdbuf", "-oL", ["env", "KICK_LOG=2", "KICK_SYNTH_INPUT=1",
"KICK_DUMP_DIR=" + dump_dir, "stdbuf", "-oL",
BIN, *extra_args] + list(tabs), BIN, *extra_args] + list(tabs),
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=open(log, "ab"), stderr=subprocess.STDOUT, stdout=open(log, "ab"), stderr=subprocess.STDOUT,
preexec_fn=_preexec_death(os.getpid()), preexec_fn=_preexec_death(os.getpid()),
) )
return Session(proc, log) return Session(proc, log, dump_dir)
def test_alt_shift_t_becomes_active(session): def test_alt_shift_t_becomes_active(session):
@ -3485,17 +3511,19 @@ def test_no_args_opens_terminal():
if not os.access(path, os.X_OK): if not os.access(path, os.X_OK):
pytest.skip(f"build first: {path}") pytest.skip(f"build first: {path}")
log = _fresh_log() log = _fresh_log()
dump_dir = _mk_dump_dir()
# The one tab is the terminal named by TERMINAL (a wltest, so no real # The one tab is the terminal named by TERMINAL (a wltest, so no real
# terminal emulator has to be installed on the test host). # terminal emulator has to be installed on the test host).
proc = subprocess.Popen( proc = subprocess.Popen(
["env", "KICK_LOG=2", "KICK_SYNTH_INPUT=1", ["env", "KICK_LOG=2", "KICK_SYNTH_INPUT=1",
f"TERMINAL={WL} '{RED}'", "stdbuf", "-oL", BIN], f"TERMINAL={WL} '{RED}'", "KICK_DUMP_DIR=" + dump_dir,
"stdbuf", "-oL", BIN],
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=open(log, "ab"), stderr=subprocess.STDOUT, stdout=open(log, "ab"), stderr=subprocess.STDOUT,
preexec_fn=_preexec_death(os.getpid()), preexec_fn=_preexec_death(os.getpid()),
) )
s = Session(proc, log) s = Session(proc, log, dump_dir)
try: try:
s.wait_for(r"\[diag\] map app=0") s.wait_for(r"\[diag\] map app=0")
finally: finally:
@ -3664,6 +3692,7 @@ def _spawn_kick_standalone(commands):
""" """
env = dict(os.environ) env = dict(os.environ)
log = _fresh_log() log = _fresh_log()
dump_dir = _mk_dump_dir()
proc = subprocess.Popen( proc = subprocess.Popen(
["env", "-i", ["env", "-i",
"PATH=" + env["PATH"], "PATH=" + env["PATH"],
@ -3676,13 +3705,14 @@ def _spawn_kick_standalone(commands):
"WLR_BACKENDS=headless", "WLR_BACKENDS=headless",
"KICK_FONT=" + (env.get("KICK_FONT") or _pick_font() or ""), "KICK_FONT=" + (env.get("KICK_FONT") or _pick_font() or ""),
"XCURSOR_THEME=Adwaita", "XCURSOR_THEME=Adwaita",
"KICK_DUMP_DIR=" + dump_dir,
"stdbuf", "-oL", "stdbuf", "-oL",
BIN] + list(commands), BIN] + list(commands),
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stdout=open(log, "ab"), stderr=subprocess.STDOUT, stdout=open(log, "ab"), stderr=subprocess.STDOUT,
preexec_fn=_preexec_death(os.getpid()), preexec_fn=_preexec_death(os.getpid()),
) )
return Session(proc, log) return Session(proc, log, dump_dir)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@ -3997,17 +4027,19 @@ def test_mode_autodetect_stays_nested_without_console():
terminal-emulator launches keep the old default.""" terminal-emulator launches keep the old default."""
env = dict(os.environ) env = dict(os.environ)
log = _fresh_log() log = _fresh_log()
dump_dir = _mk_dump_dir()
proc = subprocess.Popen( proc = subprocess.Popen(
["env", "-i", ["env", "-i",
"PATH=" + env["PATH"], "PATH=" + env["PATH"],
"HOME=" + env.get("HOME", "/"), "HOME=" + env.get("HOME", "/"),
"XDG_RUNTIME_DIR=" + env.get("XDG_RUNTIME_DIR", "/tmp"), "XDG_RUNTIME_DIR=" + env.get("XDG_RUNTIME_DIR", "/tmp"),
"KICK_LOG=1", "KICK_LOG=1",
"KICK_DUMP_DIR=" + dump_dir,
"stdbuf", "-oL", BIN], "stdbuf", "-oL", BIN],
stdin=subprocess.PIPE, stdout=open(log, "ab"), stderr=subprocess.STDOUT, stdin=subprocess.PIPE, stdout=open(log, "ab"), stderr=subprocess.STDOUT,
preexec_fn=_preexec_death(os.getpid()), preexec_fn=_preexec_death(os.getpid()),
) )
s = Session(proc, log) s = Session(proc, log, dump_dir)
try: try:
s.wait_for(r"\[mode\] nested \(default\)") s.wait_for(r"\[mode\] nested \(default\)")
assert "[mode] standalone" not in s.read_log() assert "[mode] standalone" not in s.read_log()
@ -4743,46 +4775,26 @@ def test_standalone_headless_screencopy_frame(tmp_path):
s.close() s.close()
def test_standalone_headless_tearing_and_fractional_globals(tmp_path): def test_standalone_headless_tearing_and_fractional_globals(globals_session,
tmp_path):
"""The compositor advertises wp_tearing_control_manager_v1 (games request """The compositor advertises wp_tearing_control_manager_v1 (games request
unthrottled presentation) and wp_fractional_scale_manager_v1 (clients unthrottled presentation) and wp_fractional_scale_manager_v1 (clients
learn the output scale). Tearing drives the commit's async-flip flag learn the output scale). Tearing drives the commit's async-flip flag
from the shown surface's hint; fractional scale is notified at claim.""" from the shown surface's hint; fractional scale is notified at claim."""
out = tmp_path / "globals.txt" names = _globals_of(globals_session, tmp_path)
s = _spawn_standalone({}, "%s --globals-out '%s' '#112233'" % (WL, out)) for want in ("wp_tearing_control_manager_v1",
try: "wp_fractional_scale_manager_v1"):
deadline = time.monotonic() + 15.0 assert want in names, \
while time.monotonic() < deadline: f"missing global {want}; advertised: {sorted(names)}"
if out.exists():
break
time.sleep(0.1)
assert out.exists(), "wltest --globals-out never wrote its file"
names = {line.split()[0] for line in out.read_text().splitlines()}
for want in ("wp_tearing_control_manager_v1",
"wp_fractional_scale_manager_v1"):
assert want in names, \
f"missing global {want}; advertised: {sorted(names)}"
finally:
s.close()
def test_standalone_headless_shortcuts_inhibit_globals(tmp_path): def test_standalone_headless_shortcuts_inhibit_globals(globals_session,
tmp_path):
"""The compositor advertises zwp_keyboard_shortcuts_inhibit_manager_v1 """The compositor advertises zwp_keyboard_shortcuts_inhibit_manager_v1
so a fullscreen game/VM can grab the compositor's shortcuts.""" so a fullscreen game/VM can grab the compositor's shortcuts."""
out = tmp_path / "globals.txt" names = _globals_of(globals_session, tmp_path)
s = _spawn_standalone({}, "%s --globals-out '%s' '#112233'" % (WL, out)) assert "zwp_keyboard_shortcuts_inhibit_manager_v1" in names, \
try: f"missing inhibit manager; advertised: {sorted(names)}"
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
if out.exists():
break
time.sleep(0.1)
assert out.exists(), "wltest --globals-out never wrote its file"
names = {line.split()[0] for line in out.read_text().splitlines()}
assert "zwp_keyboard_shortcuts_inhibit_manager_v1" in names, \
f"missing inhibit manager; advertised: {sorted(names)}"
finally:
s.close()
def test_standalone_headless_shortcuts_inhibit_active(tmp_path): def test_standalone_headless_shortcuts_inhibit_active(tmp_path):
@ -4812,23 +4824,12 @@ def test_standalone_headless_shortcuts_inhibit_active(tmp_path):
s.close() s.close()
def test_standalone_headless_output_power_globals(tmp_path): def test_standalone_headless_output_power_globals(globals_session, tmp_path):
"""The compositor advertises zwlr_output_power_manager_v1 so a client """The compositor advertises zwlr_output_power_manager_v1 so a client
(wlopm) can blank/unblank the output.""" (wlopm) can blank/unblank the output."""
out = tmp_path / "globals.txt" names = _globals_of(globals_session, tmp_path)
s = _spawn_standalone({}, "%s --globals-out '%s' '#112233'" % (WL, out)) assert "zwlr_output_power_manager_v1" in names, \
try: f"missing power manager; advertised: {sorted(names)}"
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
if out.exists():
break
time.sleep(0.1)
assert out.exists(), "wltest --globals-out never wrote its file"
names = {line.split()[0] for line in out.read_text().splitlines()}
assert "zwlr_output_power_manager_v1" in names, \
f"missing power manager; advertised: {sorted(names)}"
finally:
s.close()
def _oneshot_sc(sock, out_path, timeout=10.0): def _oneshot_sc(sock, out_path, timeout=10.0):