Port [claim]/[launch] client-PID logging and the 13-test smoke suite
Brings in master's client-PID logging ([claim] ties an app slot to the connected client's PID, [launch] records the spawn PID) and its expanded smoke suite, so the fixed tree can run and pass the full merged suite.
This commit is contained in:
parent
3cc2ab38fd
commit
ea8770c887
3 changed files with 167 additions and 4 deletions
|
|
@ -855,6 +855,17 @@ static void handle_new_toplevel(struct wl_listener *listener, void *data) {
|
|||
/* hidden until it becomes the active tab */
|
||||
wlr_scene_node_set_enabled(&app->tree->node, false);
|
||||
|
||||
/* [claim] ties the app slot to the *connected client's* PID, so tests
|
||||
can kill the tab that is actually active. App slots are claimed in
|
||||
connection order (first free slot), which is NOT the launch order
|
||||
the [launch] lines use when two clients start concurrently. */
|
||||
pid_t cpid = -1;
|
||||
if (toplevel->base->client)
|
||||
wl_client_get_credentials(toplevel->base->client->client,
|
||||
&cpid, NULL, NULL);
|
||||
tabbed_log(TABBED_LOG_INFO, "[claim] app=%d pid=%d\n",
|
||||
app_index(app), (int)cpid);
|
||||
|
||||
app->destroy.notify = handle_toplevel_destroy;
|
||||
wl_signal_add(&toplevel->events.destroy, &app->destroy);
|
||||
|
||||
|
|
@ -2734,8 +2745,11 @@ static void launch_client(int idx) {
|
|||
execlp("/bin/sh", "sh", "-c", server.apps[idx]->cmd, NULL);
|
||||
_exit(127);
|
||||
}
|
||||
if (pid > 0)
|
||||
if (pid > 0) {
|
||||
server.apps[idx]->pid = pid;
|
||||
tabbed_log(TABBED_LOG_INFO, "[launch] app=%d pid=%d cmd=%s\n",
|
||||
idx, pid, server.apps[idx]->cmd);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== Crash handler ===================== */
|
||||
|
|
|
|||
6
test.sh
6
test.sh
|
|
@ -1,4 +1,8 @@
|
|||
#!/bin/sh
|
||||
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
exec pytest -v tests/
|
||||
exec pytest -v "$@"
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import glob
|
|||
import os
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import struct
|
||||
import subprocess
|
||||
import time
|
||||
|
|
@ -48,6 +49,7 @@ CTL_CMD_KEY = 1
|
|||
CTL_CMD_MODS = 2
|
||||
|
||||
ACTIVE_RE = re.compile(r"active app=(\d+)")
|
||||
CLAIM_RE = re.compile(r"\[claim\] app=(\d+) pid=(\d+)")
|
||||
SOCKET_RE = re.compile(r"Wayland socket: (tabbed-wayland-\d+)")
|
||||
|
||||
|
||||
|
|
@ -181,6 +183,38 @@ class Session:
|
|||
time.sleep(0.1)
|
||||
raise AssertionError("F10 produced no 'host back buffer ->' line")
|
||||
|
||||
def dump_scene(self, timeout=5.0):
|
||||
"""F9-request a scene dump; wait until the dump path has run.
|
||||
|
||||
The scene buffer is a dmabuf and never CPU-readable, so no ppm is
|
||||
written; the `[dump] scene` line (either "not CPU-readable" or the
|
||||
stats line) proves the F9 request reached the dump path.
|
||||
"""
|
||||
rx = re.compile(r"\[dump\] scene")
|
||||
before = len(rx.findall(self.read_log()))
|
||||
self.tap(F9)
|
||||
since = time.monotonic()
|
||||
while time.monotonic() - since < timeout:
|
||||
m = rx.findall(self.read_log())
|
||||
if len(m) > before:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
raise AssertionError("F9 produced no '[dump] scene' line")
|
||||
|
||||
def active_count(self):
|
||||
"""How many `active app=` transitions the log has seen so far."""
|
||||
return len(ACTIVE_RE.findall(self.read_log()))
|
||||
|
||||
def launch_pids(self, timeout=10.0):
|
||||
"""Map app index -> connected client PID from [claim] lines."""
|
||||
since = time.monotonic()
|
||||
while time.monotonic() - since < timeout:
|
||||
d = dict((int(a), int(p)) for a, p in CLAIM_RE.findall(self.read_log()))
|
||||
if {0, 1}.issubset(d):
|
||||
return d
|
||||
time.sleep(0.1)
|
||||
pytest.fail("both apps never logged a [claim] line")
|
||||
|
||||
def centre_of(self, path):
|
||||
"""Centre pixel of an 8-bit P6 ppm as an (R, G, B) tuple."""
|
||||
with open(path, "rb") as f:
|
||||
|
|
@ -202,7 +236,7 @@ class Session:
|
|||
except subprocess.TimeoutExpired:
|
||||
self.proc.kill()
|
||||
self.proc.wait()
|
||||
for ppm in glob.glob("/tmp/host_*.ppm"):
|
||||
for ppm in glob.glob("/tmp/host_*.ppm") + glob.glob("/tmp/scene_*.ppm"):
|
||||
os.unlink(ppm)
|
||||
|
||||
|
||||
|
|
@ -345,4 +379,115 @@ def test_sigterm_removes_socket(session):
|
|||
session.proc.terminate()
|
||||
session.proc.wait(timeout=5)
|
||||
assert session.proc.returncode == 0
|
||||
assert not os.path.exists(sock_path), f"socket left behind: {sock_path}"
|
||||
assert not os.path.exists(sock_path), f"socket left behind: {sock_path}"
|
||||
|
||||
|
||||
def test_cycle_wraps(session):
|
||||
start = session.wait_any_active()
|
||||
assert start in (0, 1)
|
||||
session.cycle(+1)
|
||||
other = session.wait_active_change(start)
|
||||
assert other == 1 - start
|
||||
session.cycle(+1)
|
||||
assert session.wait_active_change(other) == start
|
||||
session.cycle(-1)
|
||||
assert session.wait_active_change(start) == other
|
||||
session.cycle(-1)
|
||||
assert session.wait_active_change(other) == start
|
||||
|
||||
|
||||
def test_hold_pagedown_repeats(session):
|
||||
start = session.wait_any_active()
|
||||
assert start in (0, 1)
|
||||
# Press and hold Ctrl+PageDown for ~1.3s so the compositor's own
|
||||
# repeat timer (600ms delay, then 25Hz) kicks in.
|
||||
session.mods(CTRL)
|
||||
session.key(PAGE_DOWN, KEY_PRESSED)
|
||||
base = session.active_count()
|
||||
time.sleep(1.3)
|
||||
transitions = session.active_count() - base
|
||||
# Clean release: let go of the key and the modifier.
|
||||
session.key(PAGE_DOWN, KEY_RELEASED)
|
||||
session.mods(0)
|
||||
assert transitions >= 3, (
|
||||
f"held Ctrl+PgDn produced only {transitions} active transitions "
|
||||
"(expected repeats to cycle several tabs)"
|
||||
)
|
||||
settled = session.active_count()
|
||||
time.sleep(0.3)
|
||||
assert session.active_count() == settled, "repeats continued after release"
|
||||
|
||||
|
||||
def test_non_ctrl_key_ignored(session):
|
||||
start = session.wait_any_active()
|
||||
assert start in (0, 1)
|
||||
base_active = session.active_count()
|
||||
session.key(30, KEY_PRESSED) # 'A'
|
||||
session.key(30, KEY_RELEASED)
|
||||
time.sleep(0.3)
|
||||
assert session.active_count() == base_active, "'A' without ctrl cycled tabs"
|
||||
assert "host fullscreen=" not in session.read_log(), "'A' toggled fullscreen"
|
||||
assert session.proc.poll() is None
|
||||
session.tap(F11)
|
||||
session.wait_for("host fullscreen=1")
|
||||
assert session.proc.poll() is None
|
||||
|
||||
|
||||
def test_f9_scene_dump_runs(session):
|
||||
"""F9 must trigger the scene-dump path. The scene buffer is a dmabuf
|
||||
(never CPU-readable here), so we assert the diagnostic line that the
|
||||
dump path emits regardless of readability, and that F10 still works."""
|
||||
session.wait_for(r"\[diag\] map app=0")
|
||||
session.wait_for(r"\[diag\] map app=1")
|
||||
session.dump_scene()
|
||||
|
||||
host_ppm = session.dump_host()
|
||||
assert session.centre_of(host_ppm) is not None
|
||||
|
||||
# dump_seq increments every dump (even failed scene reads), so F10
|
||||
# still lands a fresh host ppm after an F9.
|
||||
ch = session.centre_of(host_ppm)
|
||||
assert ch[0] + ch[1] + ch[2] > 0, "host dump was all-black"
|
||||
|
||||
|
||||
def test_active_switch_on_client_exit(session):
|
||||
start = session.wait_any_active()
|
||||
assert start in (0, 1)
|
||||
pids = session.launch_pids()
|
||||
other = 1 - start
|
||||
|
||||
# Kill the active tab: the compositor must fall through to the other.
|
||||
# [claim] pid is the connected client (wltest); SIGTERM closes its
|
||||
# Wayland socket, which is what makes the compositor destroy the tab.
|
||||
os.kill(pids[start], signal.SIGTERM)
|
||||
assert session.wait_active_change(start) == other, \
|
||||
"active did not fall through to the other tab after its tab died"
|
||||
assert session.proc.poll() is None, "compositor exited after one tab died"
|
||||
|
||||
# Kill the last tab: the compositor should exit cleanly on its own.
|
||||
os.kill(pids[other], signal.SIGTERM)
|
||||
session.proc.wait(timeout=5)
|
||||
assert session.proc.returncode == 0, \
|
||||
f"compositor exited with {session.proc.returncode} after last tab closed"
|
||||
|
||||
|
||||
def test_sigusr1_dumps(session):
|
||||
scene_rx = re.compile(r"\[dump\] scene")
|
||||
host_rx = re.compile(r"\[dump\] host back buffer -> (\S+\.ppm)")
|
||||
# Warm the dump machinery so both baselines exist, then assert SIGUSR1
|
||||
# produces a *new* scene dump attempt and a *new* host ppm.
|
||||
session.tap(F9)
|
||||
session.tap(F10)
|
||||
session.wait_for(r"\[dump\] host back buffer ->")
|
||||
scene_before = len(scene_rx.findall(session.read_log()))
|
||||
host_before = len(host_rx.findall(session.read_log()))
|
||||
|
||||
os.kill(session.proc.pid, signal.SIGUSR1)
|
||||
since = time.monotonic()
|
||||
while time.monotonic() - since < 5.0:
|
||||
log = session.read_log()
|
||||
if len(scene_rx.findall(log)) > scene_before and \
|
||||
len(host_rx.findall(log)) > host_before:
|
||||
return
|
||||
time.sleep(0.1)
|
||||
pytest.fail("SIGUSR1 produced no new scene+host dump lines")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue