mirror of
https://github.com/daijro/camoufox.git
synced 2026-10-07 00:00:33 +00:00
fix(pythonlib): identity draws that match real machines and stay stable
Launcher-side fixes found by comparing camoufox against stock Firefox 152.0.4 on Linux, Windows 11 and macOS hosts: - DNT / GPC: BrowserForge draws doNotTrack "1" on most Firefox samples, but a stock Firefox 152 reports "unspecified" and globalPrivacyControl false; the stock defaults are used unless the caller sets them, and both are applied as prefs so the API, the worker and the DNT / Sec-GPC headers agree (#760). - Timezone and geolocation: the timezone is passed to the browser, and a configured position sets permissions.default.geo so permissions.query agrees with the auto-grant (#769, #773). - hardwareConcurrency: the reported count is the fingerprint's and the browser is pinned to that many cores (cpu_affinity.py, Linux/Windows), so worker timing agrees with it; otherwise the host count snapped into the core counts real machines ship with (never 2, Firefox's resistFingerprinting value). - Fonts: the OS base is always present in full, OS-version variants are drawn all-or-nothing, co-shipped groups stay together, Cascadia is never claimed off Windows, a native macOS/Windows identity claims only the real OS base, and gfx.font_rendering.fallback.async is off on Linux so per-character fallback does not depend on cmap-load timing. - Speech voices: a per-OS installed-voice model (voice-manifests.json) with the voiceURI formats each backend really produces (voice-uris.json); no default voice where stock has none. - WebGL: extensions a release Firefox never exposes are filtered, but OVR_multiview2 stays for Windows D3D11 renderers, which expose it. - Media devices: a seeded draw of common per-OS devices with OS-style labels. - Windows scrollbars follow the drawn Windows version (overlay on 11). - Glyph-advance perturbation (fonts:spacing_seed) defaults to off: it moved every measureText width off the value the same font gives on a real machine. - Launcher prefs are also exported as CAMOU_PREFS_1..N so camoufox.cfg applies them at startup, and the browser UI locale follows the spoofed locale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8c7eec556f
commit
3f0f850bf3
@@ -224,15 +224,136 @@ class TestClampWindowPosition:
|
||||
|
||||
|
||||
class TestSetMediaDevicesDefaults:
|
||||
def test_sets_one_mic_one_cam(self):
|
||||
c = {}
|
||||
def test_draws_common_desktop_devices(self):
|
||||
# A pre-permission page only sees "has a mic" / "has a camera"; the draw
|
||||
# is seeded by the identity and follows the common desktop population.
|
||||
c = {"navigator.userAgent": "ua", "navigator.platform": "Win32"}
|
||||
set_media_devices_defaults(c)
|
||||
assert c["mediaDevices:enabled"] is True
|
||||
assert c["mediaDevices:micros"] == 1
|
||||
assert c["mediaDevices:webcams"] == 1
|
||||
assert c["mediaDevices:speakers"] == 0
|
||||
# counts and the post-grant label/group lists agree
|
||||
for kind, key in (("micros", "microphone"), ("webcams", "webcam"), ("speakers", "speaker")):
|
||||
n = c[f"mediaDevices:{kind}"]
|
||||
assert n >= 0
|
||||
assert len(c[f"mediaDevices:{key}Labels"]) == n
|
||||
assert len(c[f"mediaDevices:{key}Groups"]) == n
|
||||
# a Windows machine always has an output, in WASAPI label form
|
||||
assert c["mediaDevices:speakers"] >= 1
|
||||
assert all("(" in s for s in c["mediaDevices:speakerLabels"])
|
||||
again = {"navigator.userAgent": "ua", "navigator.platform": "Win32"}
|
||||
set_media_devices_defaults(again)
|
||||
assert again == c
|
||||
# over many identities laptops dominate Windows: most have both
|
||||
mics = cams = 0
|
||||
for i in range(400):
|
||||
d = {"navigator.userAgent": f"ua{i}", "navigator.platform": "Win32"}
|
||||
set_media_devices_defaults(d)
|
||||
mics += d["mediaDevices:micros"] > 0
|
||||
cams += d["mediaDevices:webcams"] > 0
|
||||
assert 0.8 < mics / 400 < 1.0
|
||||
assert 0.55 < cams / 400 < 0.95
|
||||
|
||||
def test_per_os_label_style(self):
|
||||
from camoufox.fingerprints import draw_media_devices
|
||||
|
||||
mac = draw_media_devices("mac", 7)
|
||||
assert all(not s.startswith("Microphone (") for s in mac["mediaDevices:microphoneLabels"])
|
||||
lin = draw_media_devices("lin", 7)
|
||||
# PulseAudio lists a monitor source per output as a capture device
|
||||
outs = lin["mediaDevices:speakerLabels"]
|
||||
assert all(f"Monitor of {o}" in lin["mediaDevices:microphoneLabels"] for o in outs)
|
||||
# a sound card's mic and speakers share a group, like a real groupId
|
||||
win = draw_media_devices("win", 11)
|
||||
if win["mediaDevices:micros"] and win["mediaDevices:speakers"]:
|
||||
assert win["mediaDevices:microphoneGroups"][0] == win["mediaDevices:speakerGroups"][0]
|
||||
# never the fake engine's names
|
||||
for os_key in ("win", "mac", "lin"):
|
||||
for seed in range(50):
|
||||
d = draw_media_devices(os_key, seed)
|
||||
mics = d["mediaDevices:microphoneLabels"]
|
||||
cams = d["mediaDevices:webcamLabels"]
|
||||
assert "Default Audio Device" not in mics
|
||||
assert "Default Video Device" not in cams
|
||||
# distinct within a kind (macOS names a webcam and its mic alike)
|
||||
assert len(set(mics)) == len(mics)
|
||||
assert len(set(cams)) == len(cams)
|
||||
|
||||
def test_respects_user_set_media_devices(self):
|
||||
c = {"mediaDevices:webcams": 5}
|
||||
set_media_devices_defaults(c)
|
||||
assert c == {"mediaDevices:webcams": 5}
|
||||
|
||||
|
||||
class TestFixHardwareConcurrency:
|
||||
def test_keeps_a_plausible_draw_the_host_can_be_pinned_to(self, monkeypatch):
|
||||
# The fingerprint's value survives when it is a count a real desktop
|
||||
# ships with AND the browser can be pinned to it (reported == measurable
|
||||
# by construction).
|
||||
from camoufox import cpu_affinity, fingerprints as fp
|
||||
|
||||
monkeypatch.setattr(cpu_affinity, "supported", lambda: True)
|
||||
monkeypatch.setattr(fp, "host_cpu_count", lambda: 16)
|
||||
for drawn in (4, 6, 8, 10, 12, 14, 16):
|
||||
c = {"navigator.hardwareConcurrency": drawn}
|
||||
fp.fix_hardware_concurrency(c)
|
||||
assert c["navigator.hardwareConcurrency"] == drawn
|
||||
|
||||
def test_snaps_implausible_draws_down_into_the_table(self, monkeypatch):
|
||||
# browserforge draws counts no desktop ships with: over 400 linux draws
|
||||
# 8.0% were < 4 cores and 4.2% were exactly 2. hardwareConcurrency == 2
|
||||
# is what Firefox reports under resistFingerprinting, so CreepJS-style
|
||||
# heuristics label the browser "Firefox resistFingerprinting"; odd counts
|
||||
# (5, 7, 9, ...) are equally synthetic. They snap DOWN into
|
||||
# PLAUSIBLE_CORE_COUNTS, with the table floor for anything below it.
|
||||
from camoufox import cpu_affinity, fingerprints as fp
|
||||
|
||||
monkeypatch.setattr(cpu_affinity, "supported", lambda: True)
|
||||
monkeypatch.setattr(fp, "host_cpu_count", lambda: 16)
|
||||
for drawn, expected in ((1, 4), (2, 4), (3, 4), (5, 4), (7, 6), (9, 8),
|
||||
(11, 10), (13, 12), (15, 14), (32, 16)):
|
||||
c = {"navigator.hardwareConcurrency": drawn}
|
||||
fp.fix_hardware_concurrency(c)
|
||||
assert c["navigator.hardwareConcurrency"] == expected, drawn
|
||||
# never above what the host can be pinned to
|
||||
monkeypatch.setattr(fp, "host_cpu_count", lambda: 4)
|
||||
c = {"navigator.hardwareConcurrency": 2}
|
||||
fp.fix_hardware_concurrency(c)
|
||||
assert c["navigator.hardwareConcurrency"] == 4
|
||||
|
||||
def test_snaps_host_parallelism_when_it_cannot_pin(self, monkeypatch):
|
||||
# The host count, snapped DOWN into the
|
||||
# counts real machines ship with; the tails report 24 / 4. Used when the
|
||||
# draw exceeds the host or the host cannot pin (macOS).
|
||||
# 24 is in the table (recorded on real Windows/Linux devices); 2 is NOT,
|
||||
# although it is recorded, because 2 is the resistFingerprinting value.
|
||||
from camoufox import cpu_affinity, fingerprints as fp
|
||||
|
||||
monkeypatch.setattr(cpu_affinity, "supported", lambda: False)
|
||||
for host, expected in (
|
||||
(16, 16),
|
||||
(10, 10),
|
||||
(24, 24),
|
||||
(32, 24),
|
||||
(64, 24),
|
||||
(7, 6),
|
||||
(5, 4),
|
||||
(2, 4),
|
||||
(9, 8),
|
||||
):
|
||||
monkeypatch.setattr(fp, "host_cpu_count", lambda host=host: host)
|
||||
c = {"navigator.hardwareConcurrency": 2}
|
||||
fp.fix_hardware_concurrency(c)
|
||||
assert c["navigator.hardwareConcurrency"] == expected, host
|
||||
# a draw above the host count cannot be honoured even where pinning works
|
||||
monkeypatch.setattr(cpu_affinity, "supported", lambda: True)
|
||||
monkeypatch.setattr(fp, "host_cpu_count", lambda: 8)
|
||||
c = {"navigator.hardwareConcurrency": 32}
|
||||
fp.fix_hardware_concurrency(c)
|
||||
assert c["navigator.hardwareConcurrency"] == 8
|
||||
|
||||
def test_no_host_count_leaves_the_draw(self, monkeypatch):
|
||||
from camoufox import fingerprints as fp
|
||||
|
||||
monkeypatch.setattr(fp, "host_cpu_count", lambda: None)
|
||||
c = {"navigator.hardwareConcurrency": 8}
|
||||
fp.fix_hardware_concurrency(c)
|
||||
assert c["navigator.hardwareConcurrency"] == 8
|
||||
|
||||
@@ -74,3 +74,181 @@ def test_virtual_display_does_not_mutate_caller_environment(
|
||||
assert options["env"]["GDK_BACKEND"] == "x11"
|
||||
assert "WAYLAND_DISPLAY" not in options["env"]
|
||||
assert options["env"]["MOZ_ENABLE_WAYLAND"] == "0"
|
||||
|
||||
|
||||
class TestFontFallbackAsyncPref:
|
||||
"""Per-character font fallback must not skip families whose
|
||||
charmap is not loaded yet.
|
||||
|
||||
Gecko's GlobalFontFallback walks the shared font list for a family covering
|
||||
the character; in a content process with async fallback on it hits the
|
||||
`!family.IsFullyInitialized()` branch, schedules a cmap load and SKIPS the
|
||||
family, so the first measurement of a character only one bundled family
|
||||
provides returns the primary family's .notdef. Linux takes that path for
|
||||
every fallback (UseCmapsDuringSystemFallback), so the font-hijacker change
|
||||
that restored this on macOS cannot reach it there.
|
||||
|
||||
macOS must NOT get the pref: it uses the CoreText fallback, and forcing the
|
||||
synchronous scan changed the face picked for U+1E9E in Futura (stock 21.733
|
||||
-> 27.267), measured on a real Mac mini.
|
||||
"""
|
||||
|
||||
PREF = "gfx.font_rendering.fallback.async"
|
||||
|
||||
def _prefs_for(self, ua, isolated):
|
||||
return utils.launch_options(
|
||||
config={"navigator.userAgent": ua},
|
||||
i_know_what_im_doing=True,
|
||||
)["firefox_user_prefs"]
|
||||
|
||||
def test_linux_disables_async_font_fallback(self, isolated_launch_dependencies):
|
||||
prefs = self._prefs_for("Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0", None)
|
||||
assert prefs[self.PREF] is False
|
||||
|
||||
def test_macos_keeps_async_font_fallback(self, isolated_launch_dependencies):
|
||||
prefs = self._prefs_for(
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:152.0) Gecko/20100101 Firefox/152.0", None
|
||||
)
|
||||
assert self.PREF not in prefs
|
||||
|
||||
def test_windows_keeps_async_font_fallback(self, isolated_launch_dependencies):
|
||||
prefs = self._prefs_for(
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0", None
|
||||
)
|
||||
assert self.PREF not in prefs
|
||||
|
||||
def test_caller_pref_wins(self, isolated_launch_dependencies):
|
||||
prefs = utils.launch_options(
|
||||
config={"navigator.userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0"},
|
||||
firefox_user_prefs={"gfx.font_rendering.fallback.async": True},
|
||||
i_know_what_im_doing=True,
|
||||
)["firefox_user_prefs"]
|
||||
assert prefs[self.PREF] is True
|
||||
|
||||
|
||||
class TestUiLocaleFollowsIntlLocale:
|
||||
"""The packaged browser locale must follow the spoofed Intl locale.
|
||||
|
||||
With locale="fr-FR" and the browser left on en-US, Intl formatting went
|
||||
French while input.validationMessage and XML parse errors stayed English --
|
||||
a mix no real Firefox produces. Packages now bake in the language packs;
|
||||
intl.locale.requested selects one, and must always be set because an empty
|
||||
value would follow the host OS locale.
|
||||
"""
|
||||
|
||||
PREF = "intl.locale.requested"
|
||||
UA = "Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0"
|
||||
|
||||
def _prefs(self, **kwargs):
|
||||
return utils.launch_options(
|
||||
config={"navigator.userAgent": self.UA, **kwargs.pop("config", {})},
|
||||
i_know_what_im_doing=True,
|
||||
**kwargs,
|
||||
)["firefox_user_prefs"]
|
||||
|
||||
def test_default_identity_pins_en_us(self, isolated_launch_dependencies):
|
||||
assert self._prefs()[self.PREF] == "en-US"
|
||||
|
||||
def test_spoofed_locale_selects_matching_ui_locale(self, isolated_launch_dependencies):
|
||||
# handle_locale adds the likely script; Gecko's filtering negotiation
|
||||
# treats a packaged "fr" as a range, so fr-Latn-FR still selects fr.
|
||||
assert self._prefs(locale="fr-FR")[self.PREF] == "fr-Latn-FR"
|
||||
assert self._prefs(locale="pt-BR")[self.PREF] == "pt-Latn-BR"
|
||||
|
||||
def test_first_of_several_locales_wins(self, isolated_launch_dependencies):
|
||||
assert self._prefs(locale="de-DE, en-US")[self.PREF] == "de-Latn-DE"
|
||||
|
||||
def test_geoip_style_config_locale_is_used(self, isolated_launch_dependencies):
|
||||
config = {"locale:language": "ja", "locale:region": "JP"}
|
||||
assert self._prefs(config=config)[self.PREF] == "ja-JP"
|
||||
|
||||
def test_script_subtag_is_kept(self, isolated_launch_dependencies):
|
||||
config = {"locale:language": "zh", "locale:script": "Hant", "locale:region": "TW"}
|
||||
assert self._prefs(config=config)[self.PREF] == "zh-Hant-TW"
|
||||
|
||||
def test_caller_pref_wins(self, isolated_launch_dependencies):
|
||||
prefs = self._prefs(locale="fr-FR", firefox_user_prefs={self.PREF: "de"})
|
||||
assert prefs[self.PREF] == "de"
|
||||
|
||||
|
||||
class TestPrefsReachStartup:
|
||||
"""Launcher prefs must be readable by camoufox.cfg at startup.
|
||||
|
||||
Playwright's non-persistent launch writes no user.js, so firefox_user_prefs
|
||||
only arrived via juggler after startup; intl.locale.requested lost the race
|
||||
against Gecko's pre-created string bundles on Windows.
|
||||
"""
|
||||
|
||||
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0"
|
||||
|
||||
def test_prefs_are_passed_as_env(self, isolated_launch_dependencies):
|
||||
import orjson
|
||||
|
||||
opts = utils.launch_options(config={"navigator.userAgent": self.UA}, locale="fr-FR", i_know_what_im_doing=True)
|
||||
chunks = sorted((k for k in opts["env"] if k.startswith("CAMOU_PREFS_")), key=lambda k: int(k.rsplit("_", 1)[1]))
|
||||
assert chunks and chunks[0] == "CAMOU_PREFS_1"
|
||||
prefs = orjson.loads("".join(opts["env"][k] for k in chunks))
|
||||
assert prefs == opts["firefox_user_prefs"]
|
||||
assert prefs["intl.locale.requested"] == "fr-Latn-FR"
|
||||
|
||||
def test_large_prefs_are_chunked_in_order(self, monkeypatch):
|
||||
import orjson
|
||||
|
||||
monkeypatch.setattr(utils, "OS_NAME", "win")
|
||||
prefs = {f"camoufox.test.pref{i}": "x" * 50 for i in range(200)}
|
||||
env = utils.get_pref_env_vars(prefs)
|
||||
assert len(env) > 1 and all(len(v) <= 2047 for v in env.values())
|
||||
joined = "".join(env[f"CAMOU_PREFS_{i}"] for i in range(1, len(env) + 1))
|
||||
assert orjson.loads(joined) == prefs
|
||||
|
||||
def test_no_prefs_no_env(self):
|
||||
assert utils.get_pref_env_vars({}) == {}
|
||||
|
||||
|
||||
class TestWindowsScrollbarsFollowVersion:
|
||||
"""Windows 11 draws overlay scrollbars by default (stock 152.0.4 on Win11: 0 px),
|
||||
Windows 10 classic 17 px ones; the identity's font draw decides the version."""
|
||||
|
||||
PREF = "ui.useOverlayScrollbars"
|
||||
WIN_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0"
|
||||
|
||||
def _prefs(self, fonts, ua=None):
|
||||
return utils.launch_options(
|
||||
config={"navigator.userAgent": ua or self.WIN_UA, "fonts": fonts},
|
||||
i_know_what_im_doing=True,
|
||||
)["firefox_user_prefs"]
|
||||
|
||||
def test_windows_11_fonts_get_overlay(self, isolated_launch_dependencies):
|
||||
assert self._prefs(["Arial", "Segoe UI Variable Text"])[self.PREF] == 1
|
||||
|
||||
def test_windows_10_fonts_get_classic(self, isolated_launch_dependencies):
|
||||
assert self._prefs(["Arial", "Segoe UI", "Calibri"])[self.PREF] == 0
|
||||
|
||||
def test_other_oses_overlay(self, isolated_launch_dependencies):
|
||||
ua = "Mozilla/5.0 (X11; Linux x86_64; rv:152.0) Gecko/20100101 Firefox/152.0"
|
||||
assert self._prefs(["DejaVu Sans"], ua=ua)[self.PREF] == 1
|
||||
|
||||
def test_marker_set_is_the_font_model(self):
|
||||
from camoufox.fingerprints import _BASE_VARIANT_FONTS_WINDOWS, WINDOWS_11_MARKER_FONTS
|
||||
assert WINDOWS_11_MARKER_FONTS == frozenset(_BASE_VARIANT_FONTS_WINDOWS[1])
|
||||
|
||||
|
||||
class TestNativeWindowsVariantFonts:
|
||||
"""A native Windows identity claims the Win11 font variant iff the host has it."""
|
||||
|
||||
def test_native_windows_11_host_claims_variant(self, monkeypatch):
|
||||
from camoufox import fingerprints as fp
|
||||
monkeypatch.setattr(fp, "_host_has_variant_fonts", lambda target_os: True)
|
||||
fonts = fp._generate_random_font_subset("windows", seed=1, native=True)
|
||||
assert fp.WINDOWS_11_MARKER_FONTS <= set(fonts)
|
||||
|
||||
def test_native_windows_10_host_does_not(self, monkeypatch):
|
||||
from camoufox import fingerprints as fp
|
||||
monkeypatch.setattr(fp, "_host_has_variant_fonts", lambda target_os: False)
|
||||
fonts = fp._generate_random_font_subset("windows", seed=1, native=True)
|
||||
assert not (fp.WINDOWS_11_MARKER_FONTS & set(fonts))
|
||||
|
||||
def test_non_windows_hosts_never_report_variant(self):
|
||||
from camoufox import fingerprints as fp
|
||||
assert fp._host_has_variant_fonts("linux") is False
|
||||
assert fp._host_has_variant_fonts("macos") is False
|
||||
|
||||
@@ -44,15 +44,23 @@ def test_entries_are_full_objects(target_os):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("target_os", ["macos", "windows", "linux"])
|
||||
def test_exactly_one_default(target_os):
|
||||
def test_no_default_voice(target_os):
|
||||
# Stock Firefox 152 marks no SpeechSynthesisVoice.default on any OS.
|
||||
voices = _generate_random_voice_subset(target_os, "en-US")
|
||||
assert sum(1 for v in voices if v["isDefault"]) == 1
|
||||
assert voices and not any(v["isDefault"] for v in voices)
|
||||
|
||||
|
||||
def test_default_matches_spoofed_locale_prefix():
|
||||
de = _generate_random_voice_subset("linux", "de-DE")
|
||||
default = next(v for v in de if v["isDefault"])
|
||||
assert default["lang"].split("-")[0] == "de"
|
||||
def test_windows_display_language_pack():
|
||||
# A German display language gets the German OneCore pack first; en-US is
|
||||
# the pack every stock en-US box reports (David / Mark / Zira + Desktop).
|
||||
de = _generate_random_voice_subset("windows", "de-DE", seed=1)
|
||||
assert de[0]["lang"] == "de-DE"
|
||||
en = _generate_random_voice_subset("windows", "en-US", seed=1)
|
||||
assert [v["name"] for v in en[:3]] == [
|
||||
"Microsoft David - English (United States)",
|
||||
"Microsoft Mark - English (United States)",
|
||||
"Microsoft Zira - English (United States)",
|
||||
]
|
||||
|
||||
|
||||
class TestLinuxSpeechdUris:
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Which sampled WebGL extensions reach the page, per OS."""
|
||||
|
||||
import sqlite3
|
||||
|
||||
import orjson
|
||||
|
||||
from camoufox.webgl import sample
|
||||
from camoufox.webgl.sample import DB_PATH, _load_webgl_data
|
||||
|
||||
|
||||
def _row_with(ext, key="webGl2:supportedExtensions", os="win"):
|
||||
con = sqlite3.connect(DB_PATH)
|
||||
try:
|
||||
for (data,) in con.execute(f"SELECT data FROM webgl_fingerprints WHERE {os} > 0"): # nosec
|
||||
if ext in (orjson.loads(data).get(key) or []):
|
||||
return data
|
||||
finally:
|
||||
con.close()
|
||||
raise AssertionError(f"no {os} row carries {ext}")
|
||||
|
||||
|
||||
def test_windows_keeps_ovr_multiview2_on_webgl2():
|
||||
data = _load_webgl_data(_row_with("OVR_multiview2"), "win")
|
||||
assert "OVR_multiview2" in data["webGl2:supportedExtensions"]
|
||||
|
||||
|
||||
def test_linux_filters_ovr_multiview2():
|
||||
data = _load_webgl_data(_row_with("OVR_multiview2", os="lin"), "lin")
|
||||
assert "OVR_multiview2" not in data["webGl2:supportedExtensions"]
|
||||
|
||||
|
||||
def test_ovr_multiview2_never_on_webgl1_in_corpus():
|
||||
con = sqlite3.connect(DB_PATH)
|
||||
try:
|
||||
for (data,) in con.execute("SELECT data FROM webgl_fingerprints"):
|
||||
assert "OVR_multiview2" not in (orjson.loads(data).get("webGl:supportedExtensions") or [])
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
|
||||
def test_draft_extensions_filtered_on_every_os():
|
||||
blob = orjson.dumps(
|
||||
{
|
||||
"webGl:supportedExtensions": ["ANGLE_instanced_arrays", "WEBGL_multi_draw"],
|
||||
"webGl2:supportedExtensions": ["EXT_texture_norm16", "WEBGL_clip_cull_distance", "OVR_multiview2"],
|
||||
}
|
||||
)
|
||||
for os in ("win", "mac", "lin"):
|
||||
data = _load_webgl_data(blob, os)
|
||||
assert data["webGl:supportedExtensions"] == ["ANGLE_instanced_arrays"]
|
||||
assert "EXT_texture_norm16" not in data["webGl2:supportedExtensions"]
|
||||
assert "WEBGL_clip_cull_distance" not in data["webGl2:supportedExtensions"]
|
||||
|
||||
|
||||
def test_sampled_windows_identities_can_carry_it():
|
||||
hits = sum(
|
||||
"OVR_multiview2" in (sample.sample_webgl("win", seed=s).get("webGl2:supportedExtensions") or [])
|
||||
for s in range(200)
|
||||
)
|
||||
assert hits > 0
|
||||
@@ -153,20 +153,19 @@ def test_hardware_is_not_mistaken_for_software(monkeypatch):
|
||||
assert not is_software_renderer(renderer)
|
||||
|
||||
|
||||
def test_software_first_draw_is_never_resampled(monkeypatch):
|
||||
"""The strongest reason this sampler must not be a plain reject loop.
|
||||
|
||||
Rejecting hardware draws while accepting every software one renormalises
|
||||
the pool onto llvmpipe / WARP / SwiftShader. On a sub-floor screen that
|
||||
turned a 1.5% software rate into ~40%, trading a weak incoherence for the
|
||||
strongest VM/headless tell there is. So the first draw settles the class.
|
||||
"""
|
||||
def test_software_first_draw_is_resampled_to_hardware(monkeypatch):
|
||||
"""A presented llvmpipe / WARP / SwiftShader is the first thing every
|
||||
consumer-hardware check flags (measured 2026-09-14 with sundial), so a
|
||||
software first draw is retried until a hardware renderer that fits the
|
||||
screen comes up, and only kept when the pool offers nothing else."""
|
||||
draws = iter([{"webGl:renderer": _LLVMPIPE}, {"webGl:renderer": _INTEL}])
|
||||
monkeypatch.setattr(fingerprints, "sample_webgl", lambda *a, **kw: next(draws))
|
||||
|
||||
# 1024x600 would reject a discrete GPU, but llvmpipe is plausible there and
|
||||
# must be returned as drawn rather than swapped for the Intel part.
|
||||
assert sample_webgl_for_screen("lin", 1024, 600)["webGl:renderer"] == _LLVMPIPE
|
||||
assert sample_webgl_for_screen("lin", 1024, 600)["webGl:renderer"] == _INTEL
|
||||
|
||||
only_software = iter([{"webGl:renderer": _LLVMPIPE}] * 40)
|
||||
monkeypatch.setattr(fingerprints, "sample_webgl", lambda *a, **kw: next(only_software))
|
||||
assert sample_webgl_for_screen("lin", 1920, 1080)["webGl:renderer"] == _LLVMPIPE
|
||||
|
||||
|
||||
def test_software_draws_are_skipped_when_resampling(monkeypatch):
|
||||
|
||||
Reference in New Issue
Block a user