tests: cover overlay icon desktop/PNG/SVG/absolute/missing paths
This commit is contained in:
parent
3446b79bb3
commit
7d2bda5800
1 changed files with 168 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue