From 838876ff65f372e258baeedac334470586aef007 Mon Sep 17 00:00:00 2001 From: Andrew Hamilton Date: Thu, 10 Sep 2026 01:55:51 +1000 Subject: [PATCH] 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. --- tests/test_smoke.py | 142 +++++++++++++++++++------------------------- 1 file changed, 60 insertions(+), 82 deletions(-) diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 3b7cda3..491cd40 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -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)