tests: cover overlay icon desktop/PNG/SVG/absolute/missing paths

This commit is contained in:
Andrew Hamilton 2026-09-04 20:20:44 +10:00
parent 3446b79bb3
commit 7d2bda5800

View file

@ -4742,6 +4742,174 @@ def test_standalone_alt_delete_sigkill_ignoring_client(tmp_path):
s.close()
_SVG_RED = ('<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48">'
'<rect width="48" height="48" fill="#ff0000"/></svg>')
def _png_rgba(w, h, px):
"""Minimal valid RGBA8 PNG (no external imaging deps)."""
import zlib
raw = b"".join(b"\x00" + bytes(px) * w for _ in range(h))
def chunk(tag, data):
return (struct.pack(">I", len(data)) + tag + data +
struct.pack(">I", zlib.crc32(tag + data) & 0xffffffff))
return (b"\x89PNG\r\n\x1a\n" +
chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0)) +
chunk(b"IDAT", zlib.compress(raw)) +
chunk(b"IEND", b""))
def _icon_desktop(apps_dir, app_id, icon_value):
"""A .desktop whose Icon= line has a localized decoy first (skipped)
and trailing whitespace (trimmed)."""
(apps_dir / (app_id + ".desktop")).write_text(
"[Desktop Entry]\nName=Kick Test\nIcon[en_US]=wrong-name\n"
"Icon=%s \nType=Application\n" % icon_value)
def _icon_theme_file(home, name, png_bytes=None, svg_text=None):
d = home / "icons" / "hicolor" / "48x48" / "apps"
d.mkdir(parents=True, exist_ok=True)
if png_bytes is not None:
(d / (name + ".png")).write_bytes(png_bytes)
if svg_text is not None:
(d / (name + ".svg")).write_text(svg_text)
apps = home / "applications"
apps.mkdir(parents=True, exist_ok=True)
return apps
def _wait_overlay_icon(s, app_id, timeout=15.0):
"""Hold Alt (overlay visible, icons resolve on draw) until the icon
load is logged; each dump forces another overlay frame."""
s.mods(ALT)
try:
rx = re.compile(r"\[overlay\] icon %s" % re.escape(app_id))
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if rx.search(s.read_log()):
# Two more frames so the resolved texture itself draws.
s.dump_host()
s.dump_host()
return
s.dump_host()
pytest.fail(f"overlay never resolved an icon for {app_id}")
finally:
s.mods(0)
def test_standalone_overlay_icon_png_from_desktop(tmp_path, monkeypatch):
"""A PNG icon named by a .desktop Icon= value resolves in the overlay:
desktop-file parse, theme lookup and PNG decode all run."""
home = tmp_path / "xdghome"
apps = _icon_theme_file(home, "testicon-png",
png_bytes=_png_rgba(8, 8, (255, 0, 0, 255)))
_icon_desktop(apps, "icontest", "testicon-png")
monkeypatch.setenv("XDG_DATA_HOME", str(home))
s = _spawn_standalone({}, "%s --app-id icontest '#ff0000'" % WL)
try:
s.wait_app_active(0)
s.wait_surface_mapped(0)
_wait_overlay_icon(s, "icontest")
assert s.proc.poll() is None, \
f"compositor died resolving a PNG icon; log tail:\n{s.read_log()[-400:]}"
finally:
s.close()
def test_standalone_overlay_icon_svg_from_desktop(tmp_path, monkeypatch):
"""An SVG icon named by a .desktop Icon= value resolves in the overlay
via the librsvg decode path."""
home = tmp_path / "xdghome"
apps = _icon_theme_file(home, "testicon-svg", svg_text=_SVG_RED)
_icon_desktop(apps, "icontest", "testicon-svg")
monkeypatch.setenv("XDG_DATA_HOME", str(home))
s = _spawn_standalone({}, "%s --app-id icontest '#ff0000'" % WL)
try:
s.wait_app_active(0)
s.wait_surface_mapped(0)
_wait_overlay_icon(s, "icontest")
assert s.proc.poll() is None, \
f"compositor died resolving an SVG icon; log tail:\n{s.read_log()[-400:]}"
finally:
s.close()
def test_standalone_overlay_icon_absolute_path_via_dirs(tmp_path, monkeypatch):
"""An absolute-path Icon= value loads directly, with the .desktop found
via XDG_DATA_DIRS (not XDG_DATA_HOME)."""
home = tmp_path / "xdghome"
home.mkdir()
(home / "applications").mkdir()
dirs = tmp_path / "xdgdirs"
apps = dirs / "applications"
apps.mkdir(parents=True)
abs_icon = tmp_path / "absicon.png"
abs_icon.write_bytes(_png_rgba(8, 8, (0, 255, 0, 255)))
_icon_desktop(apps, "icontest", str(abs_icon))
monkeypatch.setenv("XDG_DATA_HOME", str(home))
monkeypatch.setenv("XDG_DATA_DIRS", str(dirs))
s = _spawn_standalone({}, "%s --app-id icontest '#ff0000'" % WL)
try:
s.wait_app_active(0)
s.wait_surface_mapped(0)
_wait_overlay_icon(s, "icontest")
assert s.proc.poll() is None, \
f"compositor died resolving an absolute-path icon; log tail:\n{s.read_log()[-400:]}"
finally:
s.close()
def test_standalone_overlay_icon_missing_and_corrupt(tmp_path, monkeypatch):
"""No desktop entry means the placeholder (theme/pixmap misses); a
corrupt PNG fails decode the same way. Neither may disturb the
compositor."""
home = tmp_path / "xdghome"
home.mkdir()
(home / "applications").mkdir()
monkeypatch.setenv("XDG_DATA_HOME", str(home))
# Missing: no desktop file for this app_id at all.
s = _spawn_standalone({}, "%s --app-id noiconapp '#ff0000'" % WL)
try:
s.wait_app_active(0)
s.wait_surface_mapped(0)
s.mods(ALT)
try:
deadline = time.monotonic() + 5.0
while time.monotonic() < deadline:
s.dump_host()
assert "[overlay] icon noiconapp" not in s.read_log(), \
"phantom icon resolved with no desktop entry"
finally:
s.mods(0)
assert s.proc.poll() is None
finally:
s.close()
# Corrupt: desktop entry points at undecodable bytes.
apps = _icon_theme_file(home, "badicon",
png_bytes=b"not a png at all")
_icon_desktop(apps, "badiconapp", "badicon")
t = _spawn_standalone({}, "%s --app-id badiconapp '#ff0000'" % WL)
try:
t.wait_app_active(0)
t.wait_surface_mapped(0)
t.mods(ALT)
try:
deadline = time.monotonic() + 5.0
while time.monotonic() < deadline:
t.dump_host()
assert "[overlay] icon badiconapp" not in t.read_log(), \
"phantom icon resolved from corrupt PNG"
finally:
t.mods(0)
assert t.proc.poll() is None, \
f"compositor died on a corrupt icon; log tail:\n{t.read_log()[-400:]}"
finally:
t.close()
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