test: de-flake the suite for parallel runs (-n10 clean 4x, ~25s)

Parallel runs (-n10, needed since -n20 overwhelms the desktop
compositor) exposed load-sensitive tests as failures. Fix the
fixable, mark the rest flaky (all marked tests pass serially;
./test.sh -m flaky stays green in ~33s):

Systemic fixes (free on success -- poll loops exit early):
- Session poll-loop timeouts retuned for loaded boxes: wait_for
  15->20s, wait_any_active/active_frame 10->20s, wait_app_active/
  wait_surface_mapped 10->15s, wait_active/change 3->10s,
  cycle_ack 5->15s; drop explicit 3s waits onto the new defaults.
- Startup-crown race: wait_any_active could catch a transient crown
  while the other tab was still pending, so four tests now settle
  both maps before reading the crown; alt_shift_t quiesces maps
  before snapshotting the transition count.
- Pointer-constraint and output-mgmt poll deadlines 5/20->15/30s
  (per-iteration dump+parse cost under load, not logic).

Flaky marks (all pass serially; fail only via load timing -- key
chords/taps landing in shifted state, overlay/pixel timing, host
roundtrips over the shared desktop compositor, X11 startup races,
repeat-rate wall clocks, multi-client dances):
hold_alt, arrows_reorder, sub_windows x4, fullscreen x3,
clipboard x3, pointer x3, screen_mode, layer x2, x11 x3,
startup-crown, modal, quit, plain_delete, cycle_wraps,
function_keys, alt_delete x2, tab_death, gestures_roundtrip,
modifier_super, no_args, alt_shift_move, close_black_tab,
output_mgmt_apply, screen_mode_sets_output_size.
This commit is contained in:
Andrew Hamilton 2026-09-08 13:38:47 +10:00
parent 7e2bb18a85
commit d8332db375

View file

@ -211,7 +211,7 @@ class Session:
with open(self._log, "r", errors="replace") as f:
return f.read()
def wait_for(self, pattern, timeout=15.0):
def wait_for(self, pattern, timeout=20.0):
"""Wait until `pattern` matches somewhere in the whole log.
Re-scans from the start each poll: kick's log lines can arrive
@ -350,7 +350,7 @@ class Session:
key = DOWN if direction > 0 else UP
self.chord(ALT, key)
def cycle_ack(self, direction, timeout=5.0):
def cycle_ack(self, direction, timeout=15.0):
"""Tap Alt+arrow and block until the compositor logs a *new*
active-tab line reflecting the cycle, then return the fresh value.
@ -387,7 +387,7 @@ class Session:
m = ACTIVE_RE.findall(self.read_log())
return int(m[-1]) if m else None
def wait_active(self, want, timeout=3.0):
def wait_active(self, want, timeout=10.0):
since = time.monotonic()
while time.monotonic() - since < timeout:
if self.active() == want:
@ -395,7 +395,7 @@ class Session:
time.sleep(0.1)
pytest.fail(f"never reached active app={want}")
def wait_active_change(self, away_from, timeout=3.0):
def wait_active_change(self, away_from, timeout=10.0):
"""Wait for active to differ from `away_from`; return the new value.
Unlike wait_active(), this ignores historical log lines: it only
@ -410,7 +410,7 @@ class Session:
time.sleep(0.1)
pytest.fail(f"active app never changed away from {away_from}")
def wait_any_active(self, timeout=10.0):
def wait_any_active(self, timeout=20.0):
"""Wait until the startup-selected tab has activated (its first map),
return its index.
@ -430,7 +430,7 @@ class Session:
time.sleep(0.1)
pytest.fail("no tab was ever activated (startup tab never mapped)")
def wait_active_frame(self, timeout=10.0):
def wait_active_frame(self, timeout=20.0):
"""Wait until the active tab has painted a real frame: the composed
(or host-presented) frame's centre is no longer the black backdrop.
Guards single-read pixel asserts that need black to mean "overlay
@ -446,7 +446,7 @@ class Session:
time.sleep(0.1)
pytest.fail("active tab never painted a frame (centre stayed black)")
def wait_app_active(self, index, timeout=10.0):
def wait_app_active(self, index, timeout=15.0):
"""Wait until the single app `index` reports itself active (for
sessions launched with fewer than two tabs)."""
rx = re.compile(r"active app=%d\b" % index)
@ -457,7 +457,7 @@ class Session:
time.sleep(0.1)
pytest.fail(f"app {index} never logged an active line")
def wait_surface_mapped(self, index, timeout=10.0):
def wait_surface_mapped(self, index, timeout=15.0):
"""Wait until the client surface of tab `index` has mapped.
``wait_any_active`` returns as soon as the startup tab logs an
@ -906,6 +906,7 @@ def test_standalone_headless_cursor_relative_motion(standalone_session):
assert s.proc.poll() is None
@pytest.mark.flaky
def test_standalone_headless_locked_pointer_holds():
"""A LOCKED pointer constraint pins the visible cursor: once the
constraint activates on pointer enter, relative motion must not carry the
@ -941,14 +942,14 @@ def test_standalone_headless_locked_pointer_holds():
# Warp onto the surface: the constraint activates and the cursor is
# painted at the lock position.
s.warp(*lock)
deadline = time.monotonic() + 5.0
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
if "[constraint] locked on surface" in s.read_log():
break
time.sleep(0.1)
else:
pytest.fail("locked constraint never activated")
deadline = time.monotonic() + 5.0
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
if distinct(patch_at(s.dump_host(), *lock), pure_lock):
break
@ -958,7 +959,7 @@ def test_standalone_headless_locked_pointer_holds():
# Relative motion must not move the pinned cursor.
s.ptr_motion(*[target[0] - lock[0], target[1] - lock[1]])
deadline = time.monotonic() + 5.0
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
path = s.dump_host()
if distinct(patch_at(path, *lock), pure_lock) and \
@ -972,6 +973,7 @@ def test_standalone_headless_locked_pointer_holds():
s.close()
@pytest.mark.flaky
def test_standalone_headless_locked_pointer_release():
"""Dropping a locked constraint mid-session tears it down: the destroy
path runs and the cursor is free to move again."""
@ -993,6 +995,7 @@ def test_standalone_headless_locked_pointer_release():
s.close()
@pytest.mark.flaky
def test_standalone_headless_confined_pointer_clamps():
"""A CONFINED pointer constraint keeps the visible cursor inside the
constraint region even when relative motion would carry it outside."""
@ -1028,14 +1031,14 @@ def test_standalone_headless_confined_pointer_clamps():
"clamped probe should be pure client colour"
s.warp(*start)
deadline = time.monotonic() + 5.0
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
if "[constraint] confined on surface" in s.read_log():
break
time.sleep(0.1)
else:
pytest.fail("confined constraint never activated")
deadline = time.monotonic() + 5.0
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
if distinct(patch_at(s.dump_host(), *start), pure_start):
break
@ -1045,7 +1048,7 @@ def test_standalone_headless_confined_pointer_clamps():
# A big relative motion clamps to the region edge, not the raw target.
s.ptr_motion(500, 400)
deadline = time.monotonic() + 5.0
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
path = s.dump_host()
if distinct(patch_at(path, *clamped), pure_clamped) and \
@ -1059,6 +1062,7 @@ def test_standalone_headless_confined_pointer_clamps():
s.close()
@pytest.mark.flaky
def test_standalone_headless_screen_mode_cursor_stays_aligned():
"""--screen-mode + pointer motion must keep the visible cursor exactly on
the warp target plus the relative delta, on a non-default output size. The
@ -1102,7 +1106,7 @@ def test_standalone_headless_screen_mode_cursor_stays_aligned():
"moved probe area should be pure client colour"
s.warp(*start)
deadline = time.monotonic() + 5.0
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
if distinct(patch_at(s.dump_host(), *start), before_start):
break
@ -1111,7 +1115,7 @@ def test_standalone_headless_screen_mode_cursor_stays_aligned():
pytest.fail("cursor never became visible at the start target")
s.ptr_motion(*delta)
deadline = time.monotonic() + 5.0
deadline = time.monotonic() + 15.0
while time.monotonic() < deadline:
path = s.dump_host()
if distinct(patch_at(path, *moved), before_moved) and \
@ -1575,6 +1579,7 @@ def test_standalone_headless_dialog_globals(globals_session, tmp_path):
f"missing dialog global; advertised: {sorted(names)}"
@pytest.mark.flaky
def test_standalone_headless_modal_steals_tab(tmp_path):
"""A modal dialog mapping in a background tab takes its tab: the
dialog client opens its modal 1.5s after boot (past settle), and the
@ -1761,7 +1766,7 @@ def test_standalone_headless_output_mgmt_apply(tmp_path):
"%s '#ff0000'" % WL)
try:
s.wait_any_active()
deadline = time.monotonic() + 20.0
deadline = time.monotonic() + 30.0
while time.monotonic() < deadline:
if out.exists() and "after " in out.read_text():
break
@ -1773,7 +1778,7 @@ def test_standalone_headless_output_mgmt_apply(tmp_path):
assert m and (m.group(1), m.group(2)) == ("800", "600"), \
f"no 800x600 after line in: {text!r}"
# A live tab reflows into the new output size.
deadline = time.monotonic() + 15.0
deadline = time.monotonic() + 20.0
geom = None
while time.monotonic() < deadline:
if re.search(r"\[diag\] commit app=\d+ geom=800x600",
@ -1799,6 +1804,7 @@ def _gestures_text(log_path):
return log_path.read_text() if log_path.exists() else ""
@pytest.mark.flaky
def test_standalone_headless_gestures_roundtrip(tmp_path):
"""Swipe/pinch/hold injected through the test feed reach a gesture
client holding pointer focus, with exact fingers/deltas/scales."""
@ -2142,6 +2148,7 @@ def test_dc_bridge_primary_roundtrip(tmp_path):
@pytest.mark.skipif(NEED_WL_CLIPBOARD, reason="wl-clipboard tools not installed")
@pytest.mark.flaky
def test_clipboard_host_steal_cancels_nested_source(session):
"""The host takes the selection while a nested source is mirrored out:
the bridge's host-side source is cancelled (the offering wl-copy exits)
@ -2248,6 +2255,7 @@ def test_standalone_cursor_shape_resets_on_tab_switch():
s.close()
@pytest.mark.flaky
def test_nested_cursor_shape_forwarded():
"""In nested mode a client's cursor-shape request is forwarded to the host
pointer (the host draws the cursor). Deterministic when the host honours
@ -2282,6 +2290,7 @@ def test_standalone_headless_last_tab_exits_cleanly(standalone_session):
assert rc == 0, f"rc={rc} (139 = SIGSEGV); log tail:\n{s.read_log()[-800:]}"
@pytest.mark.flaky
def test_standalone_headless_screen_mode_sets_output_size():
"""--screen-mode WxH sizes the output; on the headless backend it becomes
the add_output() size, observable through the dumped frame dimensions."""
@ -2484,6 +2493,7 @@ def _poll_dc_demand(sock, out_file, token, primary=False, deadline=45.0):
@pytest.mark.skipif(NEED_WL_CLIPBOARD, reason="wl-clipboard tools not installed")
@pytest.mark.flaky
def test_clipboard_nested_to_host(session):
token = f"kick-n2h-{os.getpid()}-{time.monotonic_ns()}"
copy = _dc_setter(session.socket, token)
@ -2495,6 +2505,7 @@ def test_clipboard_nested_to_host(session):
@pytest.mark.skipif(NEED_WL_CLIPBOARD, reason="wl-clipboard tools not installed")
@pytest.mark.flaky
def test_clipboard_host_to_nested(session, tmp_path):
token = f"kick-h2n-{os.getpid()}-{time.monotonic_ns()}"
copy = subprocess.Popen(
@ -2542,6 +2553,7 @@ def test_function_keys_pass_through(session):
assert session.proc.poll() is None
@pytest.mark.flaky
def test_client_fullscreen_round_trip(session):
"""A client's fullscreen request is honored and mirrored onto the host
window; the host configure then propagates fullscreen to every tab.
@ -2561,6 +2573,7 @@ def test_client_fullscreen_round_trip(session):
assert session.proc.poll() is None
@pytest.mark.flaky
def test_alt_f11_toggles_host_fullscreen(session):
"""prefix+F11 toggles the kick window's fullscreen state in the host;
the resulting configure propagates to every tab exactly like a
@ -2577,6 +2590,7 @@ def test_alt_f11_toggles_host_fullscreen(session):
assert session.proc.poll() is None
@pytest.mark.flaky
def test_alt_f11_propagates_to_stack_members():
"""prefix+F11's host fullscreen reaches not just each tab's main window
but every stacked tab-group member too, and leaving fullscreen clears
@ -2629,6 +2643,7 @@ def test_cycle_wraps(session):
assert session.wait_active_change(other) == start
@pytest.mark.flaky
def test_hold_alt_down_repeats(session):
start = session.wait_any_active()
assert start in (0, 1)
@ -2733,6 +2748,7 @@ def test_active_switch_on_client_exit(session):
f"compositor exited with {session.proc.returncode} after last tab closed"
@pytest.mark.flaky
def test_tab_death_activates_left_neighbor(session):
"""With three tabs [0,1,2], killing index 2 must activate its left
neighbor (index 1), not the leftmost tab; killing the first tab (index 0)
@ -2817,6 +2833,10 @@ def test_alt_shift_t_becomes_active(session):
os.environ["TERMINAL"] = f"{WL} '#ffff00'"
try:
session.wait_any_active()
# Quiesce boot churn first: a late-mapping tab activating after the
# snapshot would count as an extra transition below.
session.wait_surface_mapped(1)
session.wait_surface_mapped(0)
before = session.active_count()
session.chord(ALT | SHIFT, 20)
deadline = time.monotonic() + 15.0
@ -2879,6 +2899,7 @@ def test_sub_window_hidden_when_parent_not_active():
s.proc.terminate()
@pytest.mark.flaky
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
@ -2897,6 +2918,7 @@ def test_sub_window_activates_when_parent_active():
s.proc.terminate()
@pytest.mark.flaky
def test_sub_windows_stack_mode_never_claims_a_slot():
"""--tab-groups on: a second toplevel from an already-running client
rides on its owner's tab -- it logs [sub] stack, never claims a slot of
@ -2923,6 +2945,7 @@ def test_sub_windows_stack_mode_never_claims_a_slot():
s.proc.terminate()
@pytest.mark.flaky
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."""
@ -2948,6 +2971,7 @@ def test_sub_windows_stack_mode_focus_follows_tab():
s.proc.terminate()
@pytest.mark.flaky
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
@ -2978,6 +3002,7 @@ def test_sub_windows_stack_mode_unstack_refocuses_then_close():
s.proc.terminate()
@pytest.mark.flaky
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
@ -3005,6 +3030,7 @@ def test_sub_windows_stack_mode_row_title_follows_top():
s.proc.terminate()
@pytest.mark.flaky
def test_alt_delete_closes_active_window_not_tab():
"""--tab-groups on: Alt+Delete on a tab that holds a stacked window closes
only that window -- the tab stays up (the compositor stays alive, no new
@ -3145,6 +3171,7 @@ def test_quit_cancel_keeps_running():
s.proc.terminate()
@pytest.mark.flaky
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."""
@ -3164,6 +3191,7 @@ def test_quit_single_window_no_confirm():
s.proc.terminate()
@pytest.mark.flaky
def test_alt_left_right_cycle_tab_group_members():
"""--tab-groups on: Alt+Left / Alt+Right cycle the active tab's stacked
windows (Left = previous, Right = next) and the tab stays up; the chord is
@ -3255,6 +3283,7 @@ def test_alt_delete_rightmost_falls_left():
s.proc.terminate()
@pytest.mark.flaky
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"):
@ -3275,6 +3304,7 @@ def test_alt_delete_second_tab_right_neighbor_two_tabs():
s.proc.terminate()
@pytest.mark.flaky
def test_alt_shift_arrows_reorder_and_wrap():
"""Alt+Shift+Up/Down move the active tab within the overlay list without
switching the active surface; moving off an end wraps to the other end and
@ -3345,6 +3375,7 @@ def test_alt_shift_arrows_reorder_and_wrap():
os.environ["TERMINAL"] = old_term
@pytest.mark.flaky
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."""
@ -3358,7 +3389,7 @@ def test_modifier_super_flag():
# Super+Down cycles; a plain Alt+Down must not.
s.chord(SUPER, DOWN)
other = s.wait_active_change(start, timeout=5.0)
other = s.wait_active_change(start, timeout=15.0)
assert other == 1 - start, "Super+Down did not cycle the other tab"
s.chord(ALT, DOWN)
time.sleep(0.3)
@ -3439,6 +3470,7 @@ def test_alt_delete_escalates_sigkill(tmp_path):
s.proc.terminate()
@pytest.mark.flaky
def test_plain_delete_not_grabbed(session):
"""Plain Delete (no Alt) must not close the active tab."""
session.wait_any_active()
@ -3503,6 +3535,7 @@ def test_dash_h_prints_usage():
assert b"--tab-groups on|off" in r.stdout
@pytest.mark.flaky
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"):
@ -3551,6 +3584,7 @@ def test_missing_arg_fails():
assert b"not found in PATH" in (r.stdout + r.stderr)
@pytest.mark.flaky
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
@ -3793,6 +3827,7 @@ def test_child_toolkit_backends_not_forced():
s.close()
@pytest.mark.flaky
def test_xwayland_tab_renders_and_cycles(xwayland_session):
"""The X11 tab cycles like any other and its surface actually reaches
the screen: after wrapping to it, the host back buffer shows xmessage's
@ -3816,6 +3851,7 @@ def test_xwayland_tab_renders_and_cycles(xwayland_session):
f"X11 tab rendered black at ({cx},{cy})"
@pytest.mark.flaky
def test_xwayland_alt_delete_closes_x11_tab(xwayland_session):
"""Alt+Delete on the active X11 tab sends WM_DELETE_WINDOW; xmessage
honors it, exits, and the escalation never needs to fire."""
@ -4047,6 +4083,7 @@ def test_mode_autodetect_stays_nested_without_console():
s.close()
@pytest.mark.flaky
def test_x11_leader_window_never_becomes_tab():
"""A GTK-style X11 app (unmapped leader window created first, real frame
second) gets exactly ONE tab -- the mapped frame, rendered live. The
@ -4087,6 +4124,7 @@ def test_x11_leader_window_never_becomes_tab():
s.close()
@pytest.mark.flaky
def test_x11_menu_popup_attaches_as_member():
"""An instantly-mapped managed transient -- GTK's X11 menu shape --
attaches as a tab-group member of its owner through the map-time
@ -4171,6 +4209,7 @@ def test_touch_delivery_to_client():
s.close()
@pytest.mark.flaky
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
@ -4204,6 +4243,7 @@ def _parse_diag_geom(log, app_index):
return tuple(int(v) for v in best) if best else None
@pytest.mark.flaky
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
@ -4256,6 +4296,7 @@ def test_layer_exclusive_reserves_tabs():
s.close()
@pytest.mark.flaky
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."""
@ -5661,13 +5702,17 @@ def test_alt_tab_walks_windows(session):
The session fixture's last tab is startup-active (app 1), so it is the
top of the still-uncommitted MRU stack; one Alt+Tab previews app 0."""
start = session.wait_any_active()
assert start == 1, f"expected startup tab 1 active, got {start}"
session.wait_any_active()
# Settle first: wait_any_active can catch a transient crown while the
# other tab is still pending, so only read the crown once both tabs
# have mapped (no further auto-activations happen after that).
session.wait_surface_mapped(1)
session.wait_surface_mapped(0)
start = session.active()
assert start == 1, f"expected startup tab 1 active, got {start}"
session.chord(ALT, TAB)
session.wait_active(0, timeout=3.0)
session.wait_active(0)
assert session.active() == 0
@ -5677,36 +5722,48 @@ def test_alt_tab_commit_on_release(session):
First Alt+Tab moves from app 1 to app 0 (MRU [1,0]); release commits
app 0 to the front (MRU [0,1]); a second Alt+Tab steps back to app 1."""
start = session.wait_any_active()
assert start == 1
session.wait_any_active()
# Settle first: wait_any_active can catch a transient crown while the
# other tab is still pending, so only read the crown once both tabs
# have mapped (no further auto-activations happen after that).
session.wait_surface_mapped(1)
session.wait_surface_mapped(0)
start = session.active()
assert start == 1
session.chord(ALT, TAB)
session.wait_active(0, timeout=3.0)
session.wait_active(0)
session.chord(ALT, TAB)
session.wait_active(1, timeout=3.0)
session.wait_active(1)
assert session.active() == 1
def test_alt_1_synonymous_with_tab(session):
"""Alt+1 previews the next window just like Alt+Tab."""
start = session.wait_any_active()
assert start == 1
session.wait_any_active()
# Settle first: wait_any_active can catch a transient crown while the
# other tab is still pending, so only read the crown once both tabs
# have mapped (no further auto-activations happen after that).
session.wait_surface_mapped(1)
session.wait_surface_mapped(0)
start = session.active()
assert start == 1
session.chord(ALT, ONE)
session.wait_active(0, timeout=3.0)
session.wait_active(0)
assert session.active() == 0
def test_alt_tab_hold_repeats(session):
"""Holding Alt+Tab keeps walking the MRU stack via synthesized repeats,
without reshuffling the stack mid-hold."""
start = session.wait_any_active()
assert start == 1
session.wait_any_active()
# Settle first: wait_any_active can catch a transient crown while the
# other tab is still pending, so only read the crown once both tabs
# have mapped (no further auto-activations happen after that).
session.wait_surface_mapped(1)
session.wait_surface_mapped(0)
start = session.active()
assert start == 1
base = session.active_count()
session.mods(ALT)
session.key(TAB, KEY_PRESSED)