test: factor skip guards into _require_* helpers (phase 5)

30 host-Wayland, 6 Xwayland and 5 helper-binary skip guards become
_require_host_wayland() / _require_xwayland() / _require_file(path,
reason). Binary checks keep their own triage messages; os.access
checks and skipif/flaky markers are untouched. Full suite: 152
passed, 2 skipped.
This commit is contained in:
Andrew Hamilton 2026-09-10 01:55:51 +10:00
parent f10fff910c
commit 838876ff65

View file

@ -655,8 +655,7 @@ class Session:
@pytest.fixture(scope="function")
def session():
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
for path in (BIN, WL):
if not os.access(path, os.X_OK):
pytest.skip(f"build first: {path}")
@ -1287,6 +1286,25 @@ def _assert_no_crash(log):
assert pat not in log, f"crash indicator {pat!r} in log"
def _require_host_wayland():
"""Skip unless running under a live host Wayland session."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
def _require_xwayland():
"""Skip unless Xwayland and xmessage are available."""
if not HAVE_XWAYLAND:
pytest.skip("needs Xwayland and xmessage on PATH")
def _require_file(path, reason):
"""Skip unless a helper binary exists. Each caller keeps its own
triage message (missing binary vs missing dev headers)."""
if not os.path.exists(path):
pytest.skip(reason)
@pytest.fixture(scope="module")
def globals_session():
"""One shared standalone kick for all protocol-globals tests.
@ -1833,8 +1851,7 @@ def test_nested_primary_selection_bridged(tmp_path):
"""Nested mode: a tab's primary selection is fanned out to a second tab
exactly as standalone, and the host-side bridge publishes it over
ext-data-control (when the host implements it) without echo-looping."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
out = tmp_path / "primary.out"
with _run_nested("%s --primary-set 'hello' '#ff0000'" % WL,
"%s --primary-demand-expect '%s' hello '#00ff00'"
@ -1874,8 +1891,7 @@ def test_nested_host_primary_to_kick(tmp_path):
Skipped on hosts without ext-data-control (e.g. GNOME/mutter): the
host->nested primary mirror only exists on that backend, so this test
needs an ext-data-control host -- run it on a compositor like labwc/sway."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
token = f"kick-h2n-{os.getpid()}-{time.monotonic_ns()}"
out = tmp_path / "primary.out"
with _run_nested("%s --primary-demand-expect '%s' '%s' '#00ff00'"
@ -2646,8 +2662,7 @@ def test_hold_alt_down_repeats():
# This test asserts wall-clock repeat behavior itself, so it opts out
# of the suite-wide stretched repeat delay (repeat_off=False) with a
# private session; it stays marked flaky (starved timers repeat late).
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} '{RED}'", f"{WL} '{BLUE}'",
repeat_off=False) as session:
start = session.wait_any_active()
@ -2878,8 +2893,7 @@ def test_sub_window_hidden_when_parent_not_active():
its own slot glued right after the parent tab, and because the parent is
not the tab being shown it must NOT steal focus (no churn -- the crowned
tab stays active)."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
# Tab 0 (RED) opens a green extra toplevel from the same client.
with _run_nested(f"{WL} --extra '{GREEN}' '{RED}'", f"{WL} '{BLUE}'",
extra_args=("--tab-groups", "off"), wait="any") as s:
@ -2970,8 +2984,7 @@ def test_sub_window_activates_when_parent_active():
"""--tab-groups off: a related window whose parent tab is the one being
shown becomes the active tab on map (the second exception to the
no-steal rule)."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} --extra '{GREEN}' '{RED}'",
extra_args=("--tab-groups", "off")) as s:
s.wait_any_active() # the single tab is crowned, then ...
@ -2989,8 +3002,7 @@ def test_sub_windows_stack_mode_never_claims_a_slot():
rides on its owner's tab -- it logs [sub] stack, never claims a slot of
its own, and takes the keyboard only because its parent tab is the one
being shown."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} --extra '{GREEN}' '{RED}'",
extra_args=("--tab-groups", "on")) as s:
s.wait_any_active() # app 0 crowned on its first map
@ -3025,8 +3037,7 @@ def test_sub_windows_stack_mode_never_claims_a_slot():
def test_sub_windows_stack_mode_focus_follows_tab():
"""--tab-groups on: a stacked window of a background tab maps
without taking the keyboard; showing its tab hands focus to it."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
# Tab 0 (RED) opens a green extra; tab 1 (BLUE) is the startup-selected
# active tab, so the extra must map hidden.
with _run_nested(f"{WL} --extra '{GREEN}' '{RED}'", f"{WL} '{BLUE}'",
@ -3080,8 +3091,7 @@ def test_sub_windows_stack_mode_unstack_refocuses_then_close():
"""--tab-groups on: closing the stacked window ('X' in wltest)
refocuses the tab's own surface without killing the tab; Alt+Delete on
the tab then tears the whole unit down."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} --extra '{GREEN}' '{RED}'",
extra_args=("--tab-groups", "on"), wait="any") as s:
s.wait_for(r"\[sub\] focus app=0\b") # extra holds the keyboard
@ -3103,8 +3113,7 @@ def test_sub_windows_stack_mode_row_title_follows_top():
"""--tab-groups on: the overlay row shows the topmost stacked
window's title while it is mapped, and falls back to the tab's own
surface title when it closes ('X' in wltest)."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} --extra '{GREEN}' '{RED}'",
extra_args=("--tab-groups", "on"), wait="any") as s:
s.wait_for(r"\[sub\] focus app=0\b")
@ -3121,8 +3130,7 @@ def test_alt_delete_closes_active_window_not_tab():
only that window -- the tab stays up (the compositor stays alive, no new
slot is claimed) and the stacked member unstacks instead of the tab being
torn down."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} --extra '{GREEN}' '{RED}'",
extra_args=("--tab-groups", "on"), wait="any") as s:
# [sub] stack logs when the member is *created*, before its Wayland
@ -3160,8 +3168,7 @@ def _wait_exit(s, code, timeout=10.0):
def test_quit_confirm_multi_window():
"""Ctrl+Alt+Q with more than one window open raises the confirm modal
instead of quitting; Y/Enter confirms and quits cleanly with 0."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} '{RED}'", f"{WL} '{GREEN}'", wait="any") as s:
s.wait_for(r"\[claim\] app=0\b")
s.wait_for(r"\[claim\] app=1\b")
@ -3177,8 +3184,7 @@ def test_quit_confirm_multi_window():
def test_quit_confirm_dialog_renders():
"""The confirm-quit modal actually draws: with the question pending, a
frame dump shows the dark dialog box over the client content."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} '{RED}'", f"{WL} '{GREEN}'", wait="any") as s:
s.wait_for(r"\[claim\] app=0\b")
s.wait_for(r"\[claim\] app=1\b")
@ -3195,8 +3201,7 @@ def test_quit_confirm_dialog_renders():
def test_quit_cancel_keeps_running():
"""Ctrl+Alt+Q confirmation can be declined: N and Escape both dismiss it,
the compositor keeps running, and a later quit request re-asks."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} '{RED}'", f"{WL} '{GREEN}'", wait="any") as s:
s.wait_for(r"\[claim\] app=1\b")
@ -3219,8 +3224,7 @@ def test_quit_cancel_keeps_running():
def test_quit_single_window_no_confirm():
"""With a single window there is nothing to lose, so Ctrl+Alt+Q quits
immediately -- no confirm modal, clean exit 0."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} '{RED}'", wait="any") as s:
s.wait_for(r"\[claim\] app=0\b")
# Keys sent before the surface maps are silently dropped: the claim
@ -3238,8 +3242,7 @@ def test_alt_left_right_cycle_tab_group_members():
consumed even with no stack. With a main surface (RED) plus a stacked
extra (GREEN), the newest window (extra) is shown first; Alt+Left steps
back to the main, Alt+Right forward to the extra again."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} --extra '{GREEN}' '{RED}'",
extra_args=("--tab-groups", "on"), wait="any") as s:
s.wait_for(r"\[sub\] stack app=0\b")
@ -3260,8 +3263,7 @@ def test_alt_delete_activates_right_neighbor():
"""Alt+Delete closes the active tab, making the tab to its right active
(SIGTERM, escalated to SIGKILL only if the client lingers). With three
tabs [0,1,2] and 1 active, closing 1 must activate 2, not 0."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} '{RED}'", f"{WL} '{GREEN}'", f"{WL} '{BLUE}'") as s:
start = s.wait_any_active()
assert start in (0, 1, 2)
@ -3287,8 +3289,7 @@ def test_alt_delete_activates_right_neighbor():
def test_alt_delete_rightmost_falls_left():
"""Closing the rightmost tab has no right neighbor to hand active off to,
so it falls back to the left neighbor (index 1 of [0,1,2])."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} '{RED}'", f"{WL} '{GREEN}'", f"{WL} '{BLUE}'", wait="any") as s:
# Ensure the third tab has claimed a slot so index 2 really exists.
s.wait_for(r"\[claim\] app=2 pid=(\d+)")
@ -3307,8 +3308,7 @@ def test_alt_delete_rightmost_falls_left():
def test_alt_delete_second_tab_right_neighbor_two_tabs():
"""Two-tab session: closing tab 0 makes its right neighbor (1) active."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} '{RED}'", f"{WL} '{BLUE}'") as s:
# Settle the startup crown (last mapper promotes itself under load),
# then navigate to tab 0 deterministically before closing it.
@ -3330,8 +3330,7 @@ def test_alt_shift_arrows_reorder_and_wrap():
every other tab pushed one place, not swapped into place). The resulting
order is provable behaviorally: an Alt+Shift+T terminal claims the slot
right after the active tab's new position."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
old_term = os.environ.get("TERMINAL")
os.environ["TERMINAL"] = f"{WL} '{RED}'"
try:
@ -3396,8 +3395,7 @@ def test_alt_shift_arrows_reorder_and_wrap():
def test_modifier_super_flag():
"""--modifier super moves the tab-management chords to Super/Mod4: cycle,
reorder and close all work, while the Alt chords stop doing anything."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} '{RED}'", f"{WL} '{BLUE}'",
extra_args=["--modifier", "super"]) as s:
# Settle: under load either tab can hold the startup crown (last
@ -3440,8 +3438,7 @@ def test_modifier_super_flag():
def test_alt_delete_escalates_sigkill(tmp_path):
"""A client that traps SIGTERM must still be reaped by the SIGKILL
escalation, so Alt+Delete never strands a lingering tab."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
# A wrapper that ignores SIGTERM, then execs the test client so the
# tab's process group carries SIG_IGN for TERM across the exec.
wrapper = tmp_path / "trap-term.sh"
@ -3547,8 +3544,7 @@ def test_dash_h_prints_usage():
def test_no_args_opens_terminal():
"""With no command-line tabs, a single terminal tab opens and maps."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
for path in (BIN, WL):
if not os.access(path, os.X_OK):
pytest.skip(f"build first: {path}")
@ -3617,8 +3613,7 @@ def test_close_black_tab_active_last():
"""Alt+Delete on a black tab (its command has not connected yet) closes
it outright: the surviving tab becomes active, the black slot is removed
(closed tabs leave no hole), and closing the survivor exits cleanly."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
# CLI #0 (RED) maps hidden; the active tab is the still-black `sleep`.
with _run_nested(f"{WL} '{RED}'", "sleep 999") as s:
s.wait_for(r"\[diag\] map app=0")
@ -3649,8 +3644,7 @@ def test_start_cmd_index_selects_command():
"""--start-cmd-index N (1-based) starts focused on command N instead of
the default last command: index 1 makes the first command the active,
black tab from the very first frame, revealed when it maps."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} '{RED}'", f"{WL} '{GREEN}'", f"{WL} '{BLUE}'",
extra_args=["--start-cmd-index", "1"]) as s:
assert s.wait_any_active() == 0, \
@ -3881,8 +3875,7 @@ def xwayland_session():
The compositor starts focused on the *last* command, so the session
boots on the Wayland tab and Alt+Down wraps to the X11 one.
"""
if not HAVE_XWAYLAND:
pytest.skip("needs Xwayland and xmessage on PATH")
_require_xwayland()
s = _spawn_kick_standalone([f"{XMSG} hi", f"{WL} '{RED}'"])
try:
yield s
@ -3921,8 +3914,7 @@ def test_xwayland_ready_and_claim(xwayland_session):
def test_xwayland_display_env_exported():
"""Children see DISPLAY pointing at the embedded Xwayland; without
XWayland support the variable would be unset entirely."""
if not HAVE_XWAYLAND:
pytest.skip("needs Xwayland and xmessage on PATH")
_require_xwayland()
with _run_kick(["sh -c 'echo DISPLAY=$DISPLAY'"]) as s:
s.wait_for(r"DISPLAY=:(\d+)", timeout=15.0)
m = re.search(r"DISPLAY=:(\d+)", s.read_log())
@ -4016,10 +4008,8 @@ def test_x11_client_requests_driven():
fullscreen, move/resize, raise, maximize and a WM_DELETE close. The
fullscreen mirror, the raise-to-focus and the close-teardown are
asserted on the log; the rest ride on coverage plus survival."""
if not HAVE_XWAYLAND:
pytest.skip("needs Xwayland and xmessage on PATH")
if not os.path.exists(XR):
pytest.skip("needs build/xreqs (libX11 dev headers)")
_require_xwayland()
_require_file(XR, "needs build/xreqs (libX11 dev headers)")
with _run_kick(
["sh -c 'sleep 1; exec %s managed'" % XR,
"%s --app-id keep '#ff0000' 320x240" % WL]) as s:
@ -4057,10 +4047,8 @@ def test_x11_client_requests_driven():
def test_x11_unmapped_window_destroyed_untracked():
"""A window destroyed before ever mapping drops its pending surface
without a tab, a member or a trace."""
if not HAVE_XWAYLAND:
pytest.skip("needs Xwayland and xmessage on PATH")
if not os.path.exists(XR):
pytest.skip("needs build/xreqs (libX11 dev headers)")
_require_xwayland()
_require_file(XR, "needs build/xreqs (libX11 dev headers)")
with _run_kick(
["sh -c 'sleep 1; exec %s unmapped'" % XR,
"%s --app-id keep '#0000ff' 320x240" % WL], wait="any") as s:
@ -4073,8 +4061,7 @@ def test_x11_unmapped_window_destroyed_untracked():
def test_x11_or_menu_move_repositions(tmp_path):
"""An override-redirect menu that moves after mapping is repositioned
through the compositor's geometry path without disturbing the session."""
if not os.path.exists(XM):
pytest.skip("needs build/xormenu (libX11 dev headers)")
_require_file(XM, "needs build/xormenu (libX11 dev headers)")
disp_file = tmp_path / "xdisplay"
frame = menu = None
with _run_standalone(
@ -4215,8 +4202,7 @@ def test_x11_leader_window_never_becomes_tab():
permanent black tab while the frame was demoted to a tab-group member,
and Alt+Right onto the phantom main member crashed on a NULL surface
(wlr_seat_keyboard_notify_enter assertion)."""
if not HAVE_XWAYLAND:
pytest.skip("needs Xwayland and xmessage on PATH")
_require_xwayland()
if not os.access(XL, os.X_OK):
pytest.skip("build first: build/xleader")
# The short delay mirrors real toolkit init time: a raw-X client that
@ -4270,8 +4256,7 @@ def test_x11_menu_popup_attaches_as_member():
scene surface, so emacs menus "did nothing" silently. Real-emacs menu
rendering on this path is additionally verified by hand (white dropdown
pixels appear immediately after clicking the menu bar)."""
if not HAVE_XWAYLAND:
pytest.skip("needs Xwayland and xmessage on PATH")
_require_xwayland()
if not os.access(XP, os.X_OK):
pytest.skip("build first: build/xpopup")
with _run_kick(
@ -4347,8 +4332,7 @@ def test_alt_shift_move_no_member_is_noop():
"""Shift+Left/Right with the prefix held on a tab whose focused member
is the main surface still gets consumed (never leaks Shift+arrow to
the client) and never crashes, even though nothing moves."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} '#ff0000'",
extra_args=("--tab-groups", "on"), wait="any") as s:
s.chord_ack(ALT | SHIFT, LEFT)
@ -4371,8 +4355,7 @@ def test_layer_exclusive_reserves_tabs():
"""A layer surface with exclusive_zone > 0 must shrink the usable area
that maximized tabs are sized into (platform-verifies the two-pass
layout fix)."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} '{RED}'") as s:
s.wait_for(r"\[claim\] app=0")
s.wait_for(r"\[diag\] commit app=0")
@ -4413,8 +4396,7 @@ def test_layer_exclusive_reserves_tabs():
def test_layer_zero_exclusive_does_not_resize():
"""A layer surface with exclusive_zone == 0 must NOT shrink the usable
area -- tabs keep their full output size."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} '{RED}'") as s:
s.wait_for(r"\[claim\] app=0")
s.wait_for(r"\[diag\] commit app=0")
@ -4459,8 +4441,7 @@ def test_panel_menu_flush_under_bar():
top bar -- not shifted down by the bar's reserved edge (the menu-offset
regression). Deterministic: waits on kick's own log signals ([layer] map,
then the panel client's [popup] out= line), no pixel diffing or sleeps."""
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
panel = shutil.which("wf-panel-pi")
if not panel:
pytest.skip("wf-panel-pi not installed")
@ -5113,8 +5094,7 @@ def test_standalone_headless_or_menu_owner_closed(tmp_path):
drop keyboard focus, not activate freed memory (heap-use-after-free,
caught by ASan; on stock builds this guards the scenario stays alive
and crash-indicator-free)."""
if not os.path.exists(XM):
pytest.skip("needs build/xormenu (libX11 dev headers)")
_require_file(XM, "needs build/xormenu (libX11 dev headers)")
disp_file = tmp_path / "xdisplay"
frame = menu = None
with _run_standalone(
@ -5464,8 +5444,7 @@ def test_standalone_headless_x11_class_deleted(tmp_path):
"""An X11 window whose WM_CLASS is deleted keeps the compositor alive:
set_class with a NULL class must mirror "" (not raw NULL) onto the
foreign-toplevel handles (raw NULL segfaulted in wlroots' strdup)."""
if not os.path.exists(XC):
pytest.skip("needs build/xclass (libX11 dev headers)")
_require_file(XC, "needs build/xclass (libX11 dev headers)")
disp_file = tmp_path / "xdisplay"
probe_log = tmp_path / "xclass.log"
probe = None
@ -5565,8 +5544,7 @@ def test_alt_tab_hold_repeats():
without reshuffling the stack mid-hold."""
# Needs real wall-clock repeats like the Alt+Down hold test: private
# session with repeat_off=False (the suite default stretches the delay).
if not os.environ.get("WAYLAND_DISPLAY"):
pytest.skip("needs a live host Wayland session (WAYLAND_DISPLAY)")
_require_host_wayland()
with _run_nested(f"{WL} '{RED}'", f"{WL} '{BLUE}'",
repeat_off=False, wait="any") as session:
session.wait_surface_mapped(1)