mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-17 19:13:19 +03:00
- Run X11 and Weston tests at 1x and 2x scale. - Accept both libwayland object ID log formats. - Preserve test failures through logging pipelines. - Add shell32 to Windows dependencies in both Premake scripts. - Update SDK platform support and linking documentation.
301 lines
12 KiB
Python
Executable File
301 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Run the explicit Wayland drag checks in Weston inside an isolated Xvfb display.
|
|
|
|
Requires Weston with its X11 backend, Xvfb, xdotool and ImageMagick's import.
|
|
Build eepp-native-dnd-tests first. Normal/manual runs never inject input.
|
|
"""
|
|
|
|
import argparse
|
|
import io
|
|
import os
|
|
import re
|
|
from pathlib import Path
|
|
import selectors
|
|
import shutil
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def read_line(process, prefix, timeout=25):
|
|
deadline = time.monotonic() + timeout
|
|
# Unbuffered binary stdout avoids hidden read-ahead when selecting a pipe.
|
|
with selectors.DefaultSelector() as selector:
|
|
selector.register(process.stdout, selectors.EVENT_READ)
|
|
pending = bytearray()
|
|
while time.monotonic() < deadline:
|
|
if not selector.select(max(0, deadline - time.monotonic())):
|
|
break
|
|
byte = process.stdout.read(1)
|
|
if not byte:
|
|
raise RuntimeError(f"Test process exited with {process.poll()}")
|
|
if byte != b"\n":
|
|
pending += byte
|
|
continue
|
|
line = pending.decode(errors="replace").strip()
|
|
pending.clear()
|
|
print(line, flush=True)
|
|
if line.startswith(prefix):
|
|
return line
|
|
raise RuntimeError(f"Timed out waiting for {prefix}")
|
|
|
|
|
|
def pointer(*arguments):
|
|
subprocess.run(["xdotool", *map(str, arguments)], check=True)
|
|
|
|
|
|
def color_point(color, compact=False):
|
|
# PPM keeps this runner independent of Python image packages.
|
|
deadline = time.monotonic() + 5
|
|
while time.monotonic() < deadline:
|
|
stream = io.BytesIO(
|
|
subprocess.check_output(["import", "-window", "root", "-depth", "8", "ppm:-"])
|
|
)
|
|
assert stream.readline().strip() == b"P6"
|
|
line = stream.readline()
|
|
while line.startswith(b"#"):
|
|
line = stream.readline()
|
|
width, height = map(int, line.split())
|
|
assert stream.readline().strip() == b"255"
|
|
pixels = stream.read()
|
|
wanted = bytes(color)
|
|
# Window centers need room for the drag threshold; badges need only a small solid patch.
|
|
radius = 2 if compact else 24
|
|
candidates = []
|
|
for y in range(60, height - 30, 6):
|
|
for x in range(40, width - 40, 6):
|
|
if all(
|
|
pixels[(yy * width + xx) * 3 : (yy * width + xx) * 3 + 3] == wanted
|
|
for xx, yy in (
|
|
(x, y),
|
|
(x - radius, y),
|
|
(x + radius, y),
|
|
(x, y + min(radius, 20)),
|
|
)
|
|
):
|
|
if compact:
|
|
return x, y
|
|
candidates.append((x, y))
|
|
if candidates:
|
|
# Prefer the middle of the visible window, keeping the preview away from output edges.
|
|
center_x = (min(x for x, _ in candidates) + max(x for x, _ in candidates)) / 2
|
|
center_y = (min(y for _, y in candidates) + max(y for _, y in candidates)) / 2
|
|
return min(candidates, key=lambda p: (p[0] - center_x) ** 2 + (p[1] - center_y) ** 2)
|
|
time.sleep(0.1)
|
|
raise RuntimeError(f"No visible test surface with color {color}")
|
|
|
|
|
|
def run(binary, log_dir, scale):
|
|
runtime = tempfile.mkdtemp(prefix="eepp-weston-")
|
|
environment = dict(
|
|
os.environ,
|
|
XDG_RUNTIME_DIR=runtime,
|
|
WAYLAND_DISPLAY="eepp-dnd-test",
|
|
SDL_VIDEO_DRIVER="wayland",
|
|
SDL_VIDEODRIVER="wayland",
|
|
LIBGL_ALWAYS_SOFTWARE="1",
|
|
SDL_VIDEO_WAYLAND_ALLOW_LIBDECOR="0",
|
|
)
|
|
children = []
|
|
files = []
|
|
|
|
def launch(arguments, name, protocol=False):
|
|
log = (log_dir / (name + ".log")).open("wb")
|
|
files.append(log)
|
|
child_env = dict(environment)
|
|
if protocol:
|
|
child_env["WAYLAND_DEBUG"] = "1"
|
|
# Only Weston uses X11. The applications must operate as pure Wayland clients.
|
|
child_env.pop("DISPLAY", None)
|
|
child_env.pop("XAUTHORITY", None)
|
|
child = subprocess.Popen(
|
|
arguments,
|
|
cwd=ROOT,
|
|
env=child_env,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=log,
|
|
bufsize=0,
|
|
)
|
|
children.append(child)
|
|
return child
|
|
|
|
try:
|
|
weston = launch(
|
|
[
|
|
"weston",
|
|
"--backend=x11",
|
|
"--renderer=pixman",
|
|
f"--width={1240 // scale}",
|
|
f"--height={760 // scale}",
|
|
f"--scale={scale}",
|
|
"--socket=eepp-dnd-test",
|
|
"--no-config",
|
|
"--idle-time=0",
|
|
"--log=" + str(log_dir / "weston.log"),
|
|
],
|
|
"weston-stderr",
|
|
)
|
|
deadline = time.monotonic() + 10
|
|
while not Path(runtime, "eepp-dnd-test").exists():
|
|
if weston.poll() is not None or time.monotonic() > deadline:
|
|
raise RuntimeError("Weston could not start; inspect its log")
|
|
time.sleep(0.1)
|
|
tests = launch([str(binary), "--automated-wayland"], "source-protocol", True)
|
|
for scenario in (
|
|
"snapshot",
|
|
"large",
|
|
"rejected",
|
|
"cancel",
|
|
"escape",
|
|
"destroy",
|
|
"external",
|
|
"instant",
|
|
"stalled-cancel",
|
|
"shutdown",
|
|
):
|
|
ready = read_line(tests, "WAYLAND_DND_READY")
|
|
assert ready.endswith(scenario), ready
|
|
external = None
|
|
if scenario == "external":
|
|
external = launch(
|
|
[str(binary), "--automated-wayland-target"], "target-protocol", True
|
|
)
|
|
read_line(external, "WAYLAND_DND_EXTERNAL_READY")
|
|
time.sleep(0.25)
|
|
source = color_point((180, 30, 50))
|
|
pointer("mousemove", *source, "click", 1)
|
|
time.sleep(0.2)
|
|
source = color_point((180, 30, 50))
|
|
target = color_point((135, 45, 185) if external else (30, 80, 190))
|
|
pointer("mousemove", *source, "mousedown", 1)
|
|
time.sleep(0.1)
|
|
pointer("mousemove", source[0] + 18, source[1] + 4)
|
|
if scenario == "stalled-cancel":
|
|
read_line(tests, "WAYLAND_DND_BEFORE_START")
|
|
# Stop Weston before native setup, ensuring cancellation cannot use a cached reply.
|
|
# Resume it before rendering: swap/frame callbacks may legitimately wait for it.
|
|
weston.send_signal(signal.SIGSTOP)
|
|
try:
|
|
tests.stdin.write(b"begin\n")
|
|
tests.stdin.flush()
|
|
read_line(tests, "WAYLAND_DND_STARTED", timeout=2)
|
|
tests.stdin.write(b"cleanup\n")
|
|
tests.stdin.flush()
|
|
read_line(tests, "WAYLAND_DND_CLEANUP_DONE", timeout=2)
|
|
finally:
|
|
weston.send_signal(signal.SIGCONT)
|
|
else:
|
|
read_line(tests, "WAYLAND_DND_STARTED")
|
|
if scenario == "escape":
|
|
pointer("key", "Escape")
|
|
elif scenario == "rejected":
|
|
pointer("mousemove", 20, 600)
|
|
color_point((195, 45, 55), compact=True)
|
|
elif scenario not in ("cancel", "destroy", "instant", "stalled-cancel", "shutdown"):
|
|
pointer("mousemove", *target)
|
|
time.sleep(0.2)
|
|
color_point((31, 150, 82), compact=True)
|
|
if scenario == "snapshot":
|
|
pointer("mousemove", 20, 600)
|
|
color_point((195, 45, 55), compact=True)
|
|
pointer("mousemove", *target)
|
|
color_point((31, 150, 82), compact=True)
|
|
if scenario in ("snapshot", "external"):
|
|
color_point((40, 220, 100))
|
|
subprocess.run(
|
|
["import", "-window", "root", str(log_dir / "snapshot.png")], check=True
|
|
)
|
|
pointer("mouseup", 1)
|
|
result = read_line(tests, "WAYLAND_DND_RESULT")
|
|
assert " PASS " in result, result
|
|
if external:
|
|
assert read_line(external, "WAYLAND_DND_EXTERNAL ").endswith("PASS")
|
|
assert external.wait(timeout=5) == 0
|
|
time.sleep(0.1)
|
|
if scenario not in ("destroy", "shutdown"):
|
|
tests.stdin.write(b"click\n")
|
|
tests.stdin.flush()
|
|
read_line(tests, "WAYLAND_DND_CLICK_READY")
|
|
pointer("mousemove", *color_point((180, 30, 50)), "click", 1)
|
|
assert read_line(tests, "WAYLAND_DND_CLICK ").endswith("PASS")
|
|
if scenario != "shutdown":
|
|
tests.stdin.write(b"next\n")
|
|
tests.stdin.flush()
|
|
assert tests.wait(timeout=5) == 0
|
|
exiting = launch([str(binary), "--automated-wayland-exit"], "process-exit-protocol", True)
|
|
read_line(exiting, "WAYLAND_DND_READY shutdown")
|
|
time.sleep(0.25)
|
|
source = color_point((180, 30, 50))
|
|
pointer("mousemove", *source, "click", 1)
|
|
time.sleep(0.2)
|
|
source = color_point((180, 30, 50))
|
|
pointer("mousemove", *source, "mousedown", 1)
|
|
time.sleep(0.1)
|
|
pointer("mousemove", source[0] + 18, source[1] + 4)
|
|
read_line(exiting, "WAYLAND_DND_PROCESS_EXIT")
|
|
assert exiting.wait(timeout=5) == 0
|
|
pointer("mouseup", 1)
|
|
print("Wayland process exit with a live backend passed")
|
|
protocol = (log_dir / "source-protocol.log").read_text(errors="replace")
|
|
# Older libwayland logs use '@' for object IDs; newer versions use '#'.
|
|
created = len(re.findall(r"\.data_offer\(new id wl_data_offer[@#]", protocol))
|
|
destroyed = len(re.findall(r"-> wl_data_offer[@#]\d+\.destroy\(\)", protocol))
|
|
assert created > 0 and created == destroyed, (created, destroyed)
|
|
print(f"Wayland offer proxies: {created} created, {destroyed} destroyed")
|
|
print("Wayland drag checks passed; logs:", log_dir)
|
|
except Exception:
|
|
subprocess.run(["import", "-window", "root", str(log_dir / "failure.png")], check=False)
|
|
raise
|
|
finally:
|
|
subprocess.run(
|
|
["xdotool", "mouseup", "1"],
|
|
check=False,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
for child in reversed(children):
|
|
if child.poll() is None:
|
|
child.terminate()
|
|
for child in children:
|
|
try:
|
|
child.wait(timeout=3)
|
|
except subprocess.TimeoutExpired:
|
|
child.kill()
|
|
child.wait()
|
|
for file in files:
|
|
file.close()
|
|
shutil.rmtree(runtime)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--inside-xvfb", action="store_true", help=argparse.SUPPRESS)
|
|
parser.add_argument("--binary", type=Path, default=ROOT / "bin/eepp-native-dnd-tests")
|
|
parser.add_argument("--log-dir", type=Path)
|
|
parser.add_argument("--scale", type=int, choices=(1, 2), default=1)
|
|
args = parser.parse_args()
|
|
if not args.inside_xvfb:
|
|
return subprocess.call(
|
|
[
|
|
str(ROOT / "projects/scripts/xvfb-run-eepp"),
|
|
sys.executable,
|
|
str(Path(__file__).resolve()),
|
|
"--inside-xvfb",
|
|
*sys.argv[1:],
|
|
]
|
|
)
|
|
log_dir = (args.log_dir or Path(tempfile.mkdtemp(prefix="eepp-wayland-dnd-logs-"))).resolve()
|
|
log_dir.mkdir(parents=True, exist_ok=True)
|
|
print("Wayland test logs:", log_dir, flush=True)
|
|
run(args.binary.resolve(), log_dir, args.scale)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|