test(prefs, headless): add 16 Linux-specific tests for Phase 6 + 10

Cover the Linux branches in prefs.py that previously had no tests
(font metrics, GPU spoofing, MSAA from profile, canvas noise mask
per renderer, WebGL extension preservation, Xvfb workarounds,
virtual_display no-op) and add construction smoke tests for
_LinuxVirtualDisplay. Also fix two host-platform-dependent tests
so the suite stays green on both Windows and Linux.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chrissbaumann 2026-05-14 13:21:17 +02:00
parent 4f6254469e
commit d392ca2971
2 changed files with 215 additions and 2 deletions

View file

@ -13,6 +13,8 @@ does no I/O — Xvfb is only spawned in ``start()``, which we never call.
"""
from __future__ import annotations
import sys
import pytest
import invisible_playwright._headless as headless
@ -85,11 +87,65 @@ def test_windows_desktop_initial_state_is_clean():
@pytest.mark.unit
@pytest.mark.skipif(sys.platform != "win32", reason="exercises Win32 ctypes")
def test_windows_desktop_stop_is_idempotent_without_start():
"""``stop()`` after never calling ``start()`` must be a no-op, so
``__exit__`` from a failed launch can call it unconditionally."""
``__exit__`` from a failed launch can call it unconditionally.
Skipped off Windows because ``stop()`` unconditionally resolves
``ctypes.windll.user32`` at the top of the function that symbol
only exists on Windows. The early-return logic is safe because
callers only instantiate this class via ``make_virtual_display()``
which already routes on ``sys.platform == 'win32'``.
"""
vd = _WindowsVirtualDesktop()
vd.stop()
vd.stop()
assert vd._desktop is None
assert vd._original_handle == 0
# ──────────────────────────────────────────────────────────────────────
# _LinuxVirtualDisplay — construction-only smoke tests. ``start()`` is
# E2E because it spawns Xvfb; ``stop()`` is safe to call when no Xvfb
# was ever started, so we exercise that path explicitly.
# ──────────────────────────────────────────────────────────────────────
@pytest.mark.unit
def test_linux_virtual_display_initial_state_is_clean():
"""Construction must not spawn Xvfb or mutate the environment — only
``start()`` does. Mirrors the Windows construction-state test."""
vd = _LinuxVirtualDisplay()
assert vd._proc is None
assert vd._display is None
assert vd._saved_env == {}
@pytest.mark.unit
def test_linux_virtual_display_geometry_default():
"""Default geometry is 1920x1080x24 — matches the profile sampler's
default screen and avoids the Xvfb default of 1280x1024 which the
fingerprint pipeline never produces."""
vd = _LinuxVirtualDisplay()
assert vd._geometry == "1920x1080x24"
@pytest.mark.unit
def test_linux_virtual_display_custom_geometry():
"""Caller-supplied width/height feed straight into the Xvfb geometry
spec; the depth is always 24 (Firefox/ANGLE assume true-color)."""
vd = _LinuxVirtualDisplay(width=2560, height=1440)
assert vd._geometry == "2560x1440x24"
@pytest.mark.unit
def test_linux_virtual_display_stop_without_start_is_safe():
"""``stop()`` before ``start()`` must be a no-op — supports the
``__exit__`` path on a launcher that failed before Xvfb was spawned.
Verifies no AttributeError on env restore (saved_env is empty)."""
vd = _LinuxVirtualDisplay()
vd.stop()
vd.stop()
assert vd._proc is None
assert vd._display is None

View file

@ -5,6 +5,7 @@ import pytest
from invisible_playwright._fpforge import generate_profile
from invisible_playwright.prefs import (
_LINUX_GENERIC_FONT_FACTORS,
_accept_language,
_font_metrics_for_platform,
_WIN_LIGHT_COLORS,
@ -13,8 +14,9 @@ from invisible_playwright.prefs import (
@pytest.mark.unit
def test_translate_includes_gpu_renderer_windows():
def test_translate_includes_gpu_renderer_windows(monkeypatch):
"""On Windows, renderer/vendor are cleared so ANGLE reports native hardware."""
monkeypatch.setattr(sys, "platform", "win32")
p = generate_profile(seed=42)
prefs = translate_profile_to_prefs(p)
assert prefs["zoom.stealth.webgl.renderer"] == ""
@ -356,3 +358,158 @@ def test_lan_ip_seed_zero_has_no_zero_octets():
assert octets[1] == "168"
assert int(octets[2]) >= 1
assert int(octets[3]) >= 1
# ──────────────────────────────────────────────────────────────────────
# Linux-specific tests — exercise the branches that only fire when
# ``sys.platform.startswith("linux")``. Patched via ``monkeypatch`` so
# these run on any host CI environment.
# ──────────────────────────────────────────────────────────────────────
@pytest.mark.unit
def test_font_metrics_linux_prepends_generic_factors(monkeypatch):
# FM1: Linux prepends the GTK/DejaVu compensation block to the
# per-font metrics string sampled from the profile.
monkeypatch.setattr(sys, "platform", "linux")
out = _font_metrics_for_platform("Arial|1.0,Verdana|0.9,")
assert out.startswith(_LINUX_GENERIC_FONT_FACTORS)
assert out.endswith("Arial|1.0,Verdana|0.9,")
@pytest.mark.unit
def test_font_metrics_linux_empty_input_returns_empty(monkeypatch):
# FM1b: even on Linux, empty profile metrics short-circuits before
# the prepend so we never emit a metrics pref containing only the
# generic block (which would surface as a tampering signal).
monkeypatch.setattr(sys, "platform", "linux")
assert _font_metrics_for_platform("") == ""
@pytest.mark.unit
def test_font_metrics_linux2_variant_uses_linux_branch(monkeypatch):
# FM1c: ``sys.platform`` can be ``linux2`` on older Pythons / odd
# WSL builds. ``startswith("linux")`` accepts both.
monkeypatch.setattr(sys, "platform", "linux2")
out = _font_metrics_for_platform("Verdana|0.9,")
assert out.startswith(_LINUX_GENERIC_FONT_FACTORS)
@pytest.mark.unit
def test_gpu_renderer_set_from_profile_on_linux(monkeypatch):
# PG1: on Linux we spoof to the profile's Windows-ANGLE renderer
# string so cross-platform sessions present a consistent Windows GPU.
monkeypatch.setattr(sys, "platform", "linux")
p = generate_profile(seed=42)
prefs = translate_profile_to_prefs(p)
assert prefs["zoom.stealth.webgl.renderer"] == p.gpu.renderer
assert prefs["zoom.stealth.webgl.vendor"] == p.gpu.vendor
assert prefs["zoom.stealth.webgl.renderer"] # non-empty
@pytest.mark.unit
def test_msaa_from_profile_on_linux(monkeypatch):
# PG3: on Linux, MSAA comes from the profile's sampled value rather
# than being pinned to 4 (which is the Windows ANGLE default).
monkeypatch.setattr(sys, "platform", "linux")
p = generate_profile(seed=42, pin={"webgl.msaa_samples": 8})
prefs = translate_profile_to_prefs(p)
assert prefs["zoom.stealth.webgl.msaa"] == 8
assert prefs["webgl.msaa-samples"] == 8
assert prefs["webgl.msaa-force"] is True
@pytest.mark.unit
def test_msaa_zero_disables_force_on_linux(monkeypatch):
# PG3b: MSAA=0 means "no MSAA" so ``webgl.msaa-force`` must be False.
# Verifies the ``> 0`` guard on the force flag.
monkeypatch.setattr(sys, "platform", "linux")
p = generate_profile(seed=42, pin={"webgl.msaa_samples": 0})
prefs = translate_profile_to_prefs(p)
assert prefs["zoom.stealth.webgl.msaa"] == 0
assert prefs["webgl.msaa-force"] is False
@pytest.mark.unit
def test_canvas_noise_mask_intel_on_linux(monkeypatch):
# CN1: Intel renderer → 1/16 noise (mask=15). Pinning the renderer
# exercises the live ``_renderer_lo`` branch on Linux (where the
# value is read from the profile rather than hardcoded as on Windows).
monkeypatch.setattr(sys, "platform", "linux")
p = generate_profile(
seed=42,
pin={
"gpu.renderer": "ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)",
"gpu.vendor": "Google Inc. (Intel)",
},
)
prefs = translate_profile_to_prefs(p)
assert prefs["zoom.stealth.canvas.noise_skip_mask"] == 15
@pytest.mark.unit
def test_canvas_noise_mask_nvidia_on_linux(monkeypatch):
# CN2: NVIDIA/AMD renderer → 1/8 noise (mask=7). The "intel" substring
# check must NOT match here.
monkeypatch.setattr(sys, "platform", "linux")
p = generate_profile(
seed=42,
pin={
"gpu.renderer": "ANGLE (NVIDIA, NVIDIA GeForce RTX 4090 Direct3D11 vs_5_0 ps_5_0, D3D11)",
"gpu.vendor": "Google Inc. (NVIDIA)",
},
)
prefs = translate_profile_to_prefs(p)
assert prefs["zoom.stealth.canvas.noise_skip_mask"] == 7
@pytest.mark.unit
def test_webgl_extensions_preserved_on_linux(monkeypatch):
# WE1: on Linux the curated WebGL1/2 extension lists from _BASELINE
# remain in the prefs dict so the patched binary publishes them
# instead of native Mesa's set.
monkeypatch.setattr(sys, "platform", "linux")
p = generate_profile(seed=42)
prefs = translate_profile_to_prefs(p)
assert prefs["zoom.stealth.webgl.extensions"]
assert prefs["zoom.stealth.webgl2.extensions"]
# Spot-check a canonical Windows ANGLE extension is in the list.
assert "ANGLE_instanced_arrays" in prefs["zoom.stealth.webgl.extensions"]
assert "OVR_multiview2" in prefs["zoom.stealth.webgl2.extensions"]
@pytest.mark.unit
def test_xvfb_workarounds_applied_on_linux(monkeypatch):
# XW1: Linux Firefox under Xvfb can't run WebRender, so we force the
# software path. These are added via ``setdefault`` so callers can
# still override them via ``extra_prefs``.
monkeypatch.setattr(sys, "platform", "linux")
p = generate_profile(seed=42)
prefs = translate_profile_to_prefs(p)
assert prefs["gfx.webrender.all"] is False
assert prefs["gfx.webrender.force-disabled"] is True
assert prefs["webgl.force-enabled"] is True
@pytest.mark.unit
def test_xvfb_workarounds_caller_can_override(monkeypatch):
# XW1b: the workarounds are added with ``setdefault``, so a user-
# supplied ``extra_prefs`` value wins. Verifies the override path
# doesn't get clobbered by the platform branch.
monkeypatch.setattr(sys, "platform", "linux")
p = generate_profile(seed=42)
prefs = translate_profile_to_prefs(
p, extra_prefs={"webgl.force-enabled": False}
)
assert prefs["webgl.force-enabled"] is False
@pytest.mark.unit
def test_virtual_display_no_op_on_linux(monkeypatch):
# VD3: ``virtual_display`` is a Windows-only concept (CreateDesktop
# alt-desktop GPU sandbox workaround). Even when True, Linux must
# not pick up ``security.sandbox.gpu.level``.
monkeypatch.setattr(sys, "platform", "linux")
p = generate_profile(seed=42)
prefs = translate_profile_to_prefs(p, virtual_display=True)
assert "security.sandbox.gpu.level" not in prefs