mirror of
https://github.com/daijro/camoufox.git
synced 2026-10-04 08:00:19 +00:00
Several draws swallowed their failure and used something else, so an
identity could ship with values the rest of it was not drawn to match and
nobody would hear about it:
- from_preset(): a failed font or voice draw used the preset's recorded
list, or nothing, on any exception.
- generate_context_fingerprint(): a failed font, voice or WebGL draw was
`except Exception: pass`, leaving the browser's launch-time values.
- _load_font_groups() / _load_font_bases(): an unreadable file became {},
i.e. no font additions or no OS-version base.
- launch_options(): a failed font draw used every font in fonts.json, a
failed voice draw used no voices, and a preset GPU missing from
webgl_data.db was silently swapped for a drawn one (36 of the 397
bundled presets).
Each site now catches only the errors its data can raise (OSError and
ValueError for an unreadable or corrupt file, KeyError for a manifest with
no entry for the OS, sqlite3.Error for the WebGL database) and emits a
FallbackWarning. The text names what failed and what the identity uses
instead, then gives a block to paste into an issue (camoufox, browser, OS
and Python versions, the error, and the identity's user agent or GPU),
asking the user to report it on GitHub. It shares LeakWarning's
caller-frame attribution and its template lives in warnings.yml.
The broad excepts had also been hiding a broken fixture:
test_launch_environment's font and voice stubs did not accept `seed`, so
every draw there raised and was swallowed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
3.5 KiB
Python
97 lines
3.5 KiB
Python
"""Every place an identity falls back to a substitute value says so.
|
|
|
|
A substitute is a value the rest of the identity was not drawn to match, which
|
|
a page can see. Each site warns with a report block the user can paste into a
|
|
GitHub issue, so the failure reaches us instead of shipping silently.
|
|
"""
|
|
|
|
import sqlite3
|
|
|
|
import pytest
|
|
from test_identity_salt import launch
|
|
|
|
from camoufox import fingerprints as fp
|
|
from camoufox import utils
|
|
from camoufox._warnings import FallbackWarning
|
|
|
|
REPORT = r"Please report this at https://github\.com/daijro/camoufox/issues/new"
|
|
|
|
|
|
def _fail(error):
|
|
def raiser(*_args, **_kwargs):
|
|
raise error
|
|
|
|
return raiser
|
|
|
|
|
|
def _preset():
|
|
preset = fp.load_presets("152")["presets"]["windows"][0]
|
|
return {**preset, "fonts": ["Arial"]}
|
|
|
|
|
|
def _report(record):
|
|
(warning,) = [w for w in record if w.category is FallbackWarning]
|
|
text = str(warning.message)
|
|
assert "camoufox:" in text and "python:" in text and "os:" in text
|
|
return text
|
|
|
|
|
|
def test_preset_font_draw(monkeypatch):
|
|
monkeypatch.setattr(fp, "_generate_random_font_subset", _fail(OSError("fonts.json missing")))
|
|
with pytest.warns(FallbackWarning, match=REPORT) as record:
|
|
config = fp.from_preset(_preset(), "152")
|
|
assert "OSError: fonts.json missing" in _report(record)
|
|
assert "Arial" in config["fonts"]
|
|
|
|
|
|
def test_preset_voice_draw(monkeypatch):
|
|
monkeypatch.setattr(fp, "_generate_random_voice_subset", _fail(ValueError("bad manifest")))
|
|
with pytest.warns(FallbackWarning, match=REPORT) as record:
|
|
fp.from_preset(_preset(), "152")
|
|
assert "ValueError: bad manifest" in _report(record)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"target, error, key",
|
|
[
|
|
("_generate_random_font_subset", OSError("fonts.json missing"), "fonts"),
|
|
("_generate_random_voice_subset", ValueError("bad manifest"), "voices"),
|
|
("sample_webgl_for_screen", sqlite3.OperationalError("no such table"), "webGl:renderer"),
|
|
],
|
|
)
|
|
def test_context_draws(monkeypatch, target, error, key):
|
|
monkeypatch.setattr(fp, target, _fail(error))
|
|
with pytest.warns(FallbackWarning, match=REPORT) as record:
|
|
context = fp.generate_context_fingerprint(os="linux")
|
|
assert f"{type(error).__name__}: {error}" in _report(record)
|
|
assert key not in context["config"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"loader, cache", [("_load_font_groups", "_FONT_GROUPS_CACHE"), ("_load_font_bases", "_FONT_BASES_CACHE")]
|
|
)
|
|
def test_font_data_loaders(monkeypatch, tmp_path, loader, cache):
|
|
monkeypatch.setattr(fp, cache, None)
|
|
monkeypatch.setattr(fp, "__file__", str(tmp_path / "fingerprints.py"))
|
|
with pytest.warns(FallbackWarning, match=REPORT) as record:
|
|
assert getattr(fp, loader)() == {}
|
|
assert "FileNotFoundError" in _report(record)
|
|
|
|
|
|
def test_launch_font_draw(monkeypatch):
|
|
monkeypatch.setattr(utils, "_generate_random_font_subset", _fail(OSError("font-bases.json missing")))
|
|
with pytest.warns(FallbackWarning, match=REPORT):
|
|
config = launch()
|
|
assert config["fonts"]
|
|
|
|
|
|
def test_preset_gpu_not_in_webgl_data():
|
|
# Windows reports every GPU through ANGLE, so only an ANGLE string reaches the lookup.
|
|
GPU = "ANGLE (Acme, Acme GPU 9000 Direct3D11 vs_5_0 ps_5_0)"
|
|
preset = fp.load_presets("152")["presets"]["windows"][0]
|
|
preset = {**preset, "webgl": {"unmaskedVendor": "Google Inc. (Acme)", "unmaskedRenderer": GPU}}
|
|
with pytest.warns(FallbackWarning, match=REPORT) as record:
|
|
config = launch(os="windows", fingerprint_preset=preset)
|
|
assert GPU in _report(record)
|
|
assert config["webGl:renderer"] != GPU
|