test: locked abs-pointer pin, rogue second lock surface; fix wltest abs gating

This commit is contained in:
Andrew Hamilton 2026-09-11 08:37:27 +10:00
parent 21d686df5d
commit 16c5ed58e5
3 changed files with 114 additions and 4 deletions

View file

@ -1107,6 +1107,56 @@ def test_standalone_headless_confined_pointer_clamps():
_assert_alive(s)
def test_standalone_headless_locked_pointer_abs_holds():
"""A LOCKED pointer constraint also pins absolute motion: a virtual
pointer warp must not carry the cursor away from the lock position.
Regression: native_ptr_abs skipped the constraint clamp that the
relative path applies, so tablets and virtual-pointer warps escaped
the lock."""
with _run_standalone({}, "%s --constrain locked '#ff0000'" % WL, wait="app0") as s:
_assert_alive(s)
def patch_at(path, cx, cy, r=22):
w, h, px = s.ppm_rgb(path)
py = h - 1 - cy
hits = []
for y in range(max(0, py - r), min(h, py + r + 1)):
row = y * w * 3
for x in range(max(0, cx - r), min(w, cx + r + 1)):
i = row + x * 3
hits.append((px[i], px[i + 1], px[i + 2]))
return hits
def distinct(a, b):
return any(ca != cb for ca, cb in zip(a, b))
lock = (300, 300)
# Far corner of the output, well away from the lock position.
target = (720, 540)
pure_lock = patch_at(s.dump_host(), *lock)
pure_target = patch_at(s.dump_host(), *target)
assert len(set(pure_lock)) == 1, "lock probe should be pure client colour"
assert len(set(pure_target)) == 1, "target probe should be pure client colour"
pos = len(s.read_log())
s.warp(*lock)
s.wait_for_new_after(pos, r"\[constraint\] locked on surface")
s.wait_for_new_after(pos, r"\[cursor\] pos=%d,%d" % lock)
assert distinct(patch_at(s.dump_host(), *lock), pure_lock), \
"cursor never appeared at the lock position"
# An absolute warp must leave the pinned cursor where it is.
pos = len(s.read_log())
_oneshot_virtual_abs(s.socket, 0.9, 0.9)
s.wait_for_new_after(pos, r"\[cursor\] pos=%d,%d" % lock)
path = s.dump_host()
assert distinct(patch_at(path, *lock), pure_lock) and \
not distinct(patch_at(path, *target), pure_target), \
"locked pointer escaped via absolute motion"
_assert_alive(s)
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
@ -4817,6 +4867,27 @@ def test_standalone_headless_session_lock_unlock(tmp_path):
_assert_alive(s, "compositor died under takeover lock")
def test_standalone_headless_session_lock_second_surface(tmp_path):
"""A rogue second lock surface on one lock is survived: the first
surface's protocol object is dropped while the second stays live, then
unlock still tears everything down and the session survives. Covers
the singleton lock-surface bookkeeping against a misbehaving client
(re-adding the listeners must first detach them)."""
out = tmp_path / "lock.out"
with _run_standalone(
{},
"%s --app-id testwin '#ff0000' 320x240" % WL,
"%s --lock --lock-unlock --lock-second-surface --lock-out '%s'"
% (WL, out), wait="any") as s:
# The creation milestone is on stderr (session log): the out file
# only keeps the last note, so unlocked-exit would hide it.
s.wait_for("second lock surface created", timeout=10.0)
assert _wait_out(out, "unlocked-exit", timeout=10.0), \
f"lock client never finished its unlock: " \
f"{(out.read_text().strip() if out.exists() else '(missing)')!r}"
_assert_alive(s, "compositor died on double lock surface")
def test_standalone_headless_session_lock_locker_dies(tmp_path):
"""A lock client that dies without unlocking must not take the session
down: the compositor survives the destroy path and a fresh lock client

View file

@ -1735,10 +1735,16 @@ static void idle_run(void) {
a solid buffer on it (proving the compositor blanked the session and
accepted the surface). Writes "locked" then "surface-config" to --lock-out
as each stage completes; the strongest observable is the locked event the
compositor only sends once it has set up the blanked session. */
compositor only sends once it has set up the blanked session.
--lock-second-surface additionally opens a rogue second lock surface on
the same lock (noting "second-surface"), proving the compositor survives
the extra surface and its teardown. */
static bool lock_mode;
static const char *lock_out;
static bool lock_unlock_after;
static bool lock_unlocked;
static bool lock_second_surface;
static bool lock_second_created;
static struct ext_session_lock_v1 *lock;
static struct ext_session_lock_surface_v1 *lock_surface;
static struct wl_surface *lock_wl_surface;
@ -1817,7 +1823,33 @@ static void lock_surface_configure(void *data,
ext_session_lock_surface_v1_ack_configure(l, serial);
lock_fill_buffer();
lock_note("surface-config");
if (lock_unlock_after) {
/* Rogue-client probe: a second lock surface on the same lock. Created
once, before any unlock, so the compositor must survive the extra
new_surface plus the teardown of both. The first surface's protocol
object is then dropped while the second stays live, so its destroy
runs against a live lock. */
if (lock_second_surface && !lock_second_created) {
lock_second_created = true;
struct wl_surface *s2 = wl_compositor_create_surface(compositor);
if (s2 && wl_output) {
struct ext_session_lock_surface_v1 *ls2 =
ext_session_lock_v1_get_lock_surface(lock, s2, wl_output);
if (ls2) {
ext_session_lock_surface_v1_add_listener(ls2,
&lock_surface_listener, NULL);
lock_note("second-surface");
/* Also on stderr (the session log at KICK_LOG=2): the
out file only keeps the last note, so a later
unlocked-exit would hide this milestone from polls. */
fprintf(stderr, "wltest: second lock surface created\n");
ext_session_lock_surface_v1_destroy(lock_surface);
lock_surface = ls2;
lock_note("first-destroyed");
}
}
}
if (lock_unlock_after && !lock_unlocked) {
lock_unlocked = true;
ext_session_lock_v1_unlock_and_destroy(lock);
/* The unlock reply is the unblanked screen itself
(unlock_and_destroy leaves no proxy for a finished event):
@ -3942,6 +3974,9 @@ int main(int argc, char **argv) {
} else if (strcmp(argv[i], "--lock-unlock") == 0) {
lock_unlock_after = true;
i += 1;
} else if (strcmp(argv[i], "--lock-second-surface") == 0) {
lock_second_surface = true;
i += 1;
} else if (strcmp(argv[i], "--screencopy") == 0) {
screencopy_mode = true;
i += 1;
@ -4166,8 +4201,10 @@ int main(int argc, char **argv) {
return 0;
}
/* Virtual pointer: motion (+optional click) is requested via
--virtual-motion; --virtual-click alone is meaningless. */
if (virtual_dx != 0 || virtual_dy != 0 || virtual_click) {
--virtual-motion, absolute warp via --virtual-abs (either alone
suffices); --virtual-click alone is meaningless. */
if (virtual_dx != 0 || virtual_dy != 0 || virtual_click ||
(virtual_abs_x >= 0.0 && virtual_abs_y >= 0.0)) {
virtual_motion_run();
return 0;
}