mirror of
https://github.com/feder-cr/invisible_playwright.git
synced 2026-06-07 08:35:12 +02:00
test(profile): add 43 unit tests for Profile dataclass and pin system
Covers _validate_pin_key (all groups + negatives), _apply_pins_to_raw (fonts list/tuple/typeerror, multi-pin, no-mutation, unknown-key guard), and generate_profile (determinism, seed coercion, pin propagation through to_prefs_dict, frozen-instance, dark_theme bool coercion, fonts list roundtrip, int31 boundary). Includes a guard test that every dotted pin key has a _PIN_TO_RAW mapping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3286123b11
commit
38ae41289d
1 changed files with 348 additions and 0 deletions
348
tests/test_profile.py
Normal file
348
tests/test_profile.py
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
"""Unit tests for `_fpforge/profile.py`.
|
||||
|
||||
Covers `_validate_pin_key`, `_apply_pins_to_raw`, and `generate_profile`.
|
||||
Test cases derived via ECP/BVA/error guessing.
|
||||
"""
|
||||
from dataclasses import FrozenInstanceError
|
||||
|
||||
import pytest
|
||||
|
||||
from invisible_playwright._fpforge import generate_profile
|
||||
from invisible_playwright._fpforge.profile import (
|
||||
Profile,
|
||||
_PIN_GROUPS,
|
||||
_PIN_TO_RAW,
|
||||
_apply_pins_to_raw,
|
||||
_validate_pin_key,
|
||||
)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# _validate_pin_key
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_validate_pin_key_top_level_fonts():
|
||||
"""VK1 — `fonts` is a known top-level key."""
|
||||
_validate_pin_key("fonts")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_validate_pin_key_top_level_dark_theme():
|
||||
"""VK2 — `dark_theme` is a known top-level key."""
|
||||
_validate_pin_key("dark_theme")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_validate_pin_key_dotted_screen_width():
|
||||
"""VK3 — valid dotted path `screen.width`."""
|
||||
_validate_pin_key("screen.width")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_validate_pin_key_dotted_gpu_renderer():
|
||||
"""VK4 — valid dotted path `gpu.renderer`."""
|
||||
_validate_pin_key("gpu.renderer")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_validate_pin_key_dotted_webgl_msaa_samples():
|
||||
"""VK5 — valid dotted path `webgl.msaa_samples`."""
|
||||
_validate_pin_key("webgl.msaa_samples")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_validate_pin_key_no_dot_not_top_level_raises():
|
||||
"""VK6 — bare key not in top-level set raises with hint."""
|
||||
with pytest.raises(ValueError, match="group.field"):
|
||||
_validate_pin_key("bogus")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_validate_pin_key_unknown_group_raises():
|
||||
"""VK7 — unknown group prefix."""
|
||||
with pytest.raises(ValueError, match="unknown group"):
|
||||
_validate_pin_key("network.port")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_validate_pin_key_unknown_field_in_valid_group_raises():
|
||||
"""VK8 — known group, unknown field."""
|
||||
with pytest.raises(ValueError, match="unknown field"):
|
||||
_validate_pin_key("screen.brightness")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_validate_pin_key_empty_string_raises():
|
||||
"""VK9 — empty key fails the dotted-form check."""
|
||||
with pytest.raises(ValueError):
|
||||
_validate_pin_key("")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("group,fields", sorted(_PIN_GROUPS.items()))
|
||||
def test_validate_pin_key_all_groups_first_field(group, fields):
|
||||
"""VK10 — every defined group accepts its sorted-first field."""
|
||||
first = sorted(fields)[0]
|
||||
_validate_pin_key(f"{group}.{first}")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# _apply_pins_to_raw
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _raw_baseline():
|
||||
"""A minimal raw dict for pin tests — only the keys we care about."""
|
||||
return {
|
||||
"screen_w": 1920,
|
||||
"screen_h": 1080,
|
||||
"webgl_vendor": "Google Inc. (Intel)",
|
||||
"webgl_renderer": "ANGLE (Intel)",
|
||||
"font_whitelist": "arial,calibri",
|
||||
"dark_theme": 0,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_apply_pins_to_raw_screen_width():
|
||||
"""AP1 — `screen.width` rewrites `screen_w` in raw."""
|
||||
out = _apply_pins_to_raw(_raw_baseline(), {"screen.width": 2560})
|
||||
assert out["screen_w"] == 2560
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_apply_pins_to_raw_fonts_list():
|
||||
"""AP2 — list pin joined into comma-separated whitelist."""
|
||||
out = _apply_pins_to_raw(_raw_baseline(), {"fonts": ["Arial", "Verdana"]})
|
||||
assert out["font_whitelist"] == "Arial,Verdana"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_apply_pins_to_raw_fonts_tuple():
|
||||
"""AP3 — tuple pin is also accepted."""
|
||||
out = _apply_pins_to_raw(_raw_baseline(), {"fonts": ("Arial",)})
|
||||
assert out["font_whitelist"] == "Arial"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_apply_pins_to_raw_fonts_string_raises():
|
||||
"""AP4 — bare string is not a list/tuple, must raise."""
|
||||
with pytest.raises(TypeError, match="list/tuple"):
|
||||
_apply_pins_to_raw(_raw_baseline(), {"fonts": "Arial"})
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_apply_pins_to_raw_fonts_int_raises():
|
||||
"""AP5 — int is also rejected."""
|
||||
with pytest.raises(TypeError):
|
||||
_apply_pins_to_raw(_raw_baseline(), {"fonts": 42})
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_apply_pins_to_raw_multiple_pins():
|
||||
"""AP6 — multiple pins all land in raw."""
|
||||
pin = {"gpu.vendor": "X", "gpu.renderer": "Y"}
|
||||
out = _apply_pins_to_raw(_raw_baseline(), pin)
|
||||
assert out["webgl_vendor"] == "X"
|
||||
assert out["webgl_renderer"] == "Y"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_apply_pins_to_raw_returns_copy_not_mutation():
|
||||
"""AP7 — input dict is not mutated."""
|
||||
raw = _raw_baseline()
|
||||
snapshot = dict(raw)
|
||||
_apply_pins_to_raw(raw, {"screen.width": 9999})
|
||||
assert raw == snapshot
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_apply_pins_to_raw_unknown_key_silent():
|
||||
"""AP8 — key not in `_PIN_TO_RAW` (and not 'fonts') is ignored.
|
||||
|
||||
Validation happens upstream in `generate_profile`; the inner helper
|
||||
guards defensively but does not raise.
|
||||
"""
|
||||
raw = _raw_baseline()
|
||||
out = _apply_pins_to_raw(raw, {"some.unknown": 123})
|
||||
# No change to known fields
|
||||
assert out["screen_w"] == raw["screen_w"]
|
||||
# No new key added
|
||||
assert "some.unknown" not in out
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# generate_profile
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_happy_path():
|
||||
"""GP1 — returns a fully populated Profile."""
|
||||
p = generate_profile(seed=42)
|
||||
assert isinstance(p, Profile)
|
||||
assert p.seed == 42
|
||||
assert p.gpu.vendor
|
||||
assert p.gpu.renderer
|
||||
assert p.gpu.class_tier in _PIN_GROUPS["gpu"].union({"low_end", "mid_range",
|
||||
"high_end", "integrated_old", "integrated_modern", "workstation"})
|
||||
assert p.screen.width > 0
|
||||
assert p.screen.height > 0
|
||||
assert p.hardware.concurrency > 0
|
||||
assert p.audio.sample_rate > 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_deterministic():
|
||||
"""GP2 — same seed → identical Profile (equality on frozen dataclass)."""
|
||||
a = generate_profile(seed=42)
|
||||
b = generate_profile(seed=42)
|
||||
assert a == b
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_seed_float_coerced():
|
||||
"""GP3 — float seed is coerced to int (truncated)."""
|
||||
a = generate_profile(seed=42.7)
|
||||
b = generate_profile(seed=42)
|
||||
assert a == b
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_seed_string_coerced():
|
||||
"""GP4 — numeric string seed works via int() coercion."""
|
||||
a = generate_profile(seed="42")
|
||||
b = generate_profile(seed=42)
|
||||
assert a == b
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_no_pin_samples_freely():
|
||||
"""GP5 — no pin: every field is sampler-derived (sanity: 2 seeds differ)."""
|
||||
a = generate_profile(seed=1)
|
||||
b = generate_profile(seed=2)
|
||||
assert a != b
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_pin_overrides_screen_width():
|
||||
"""GP6 — pinned width visible on the Profile dataclass."""
|
||||
p = generate_profile(seed=42, pin={"screen.width": 9999})
|
||||
assert p.screen.width == 9999
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_pin_visible_in_prefs_dict():
|
||||
"""GP7 — pinned values flow through to to_prefs_dict()."""
|
||||
p = generate_profile(seed=42, pin={"screen.width": 9999})
|
||||
assert p.to_prefs_dict()["screen_w"] == 9999
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_invalid_pin_raises():
|
||||
"""GP8 — bad pin key surfaces ValueError from validation."""
|
||||
with pytest.raises(ValueError):
|
||||
generate_profile(seed=42, pin={"bogus": 1})
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_empty_pin_equals_no_pin():
|
||||
"""GP9 — empty pin dict is a no-op."""
|
||||
a = generate_profile(seed=42, pin={})
|
||||
b = generate_profile(seed=42)
|
||||
assert a == b
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_is_frozen():
|
||||
"""GP10 — Profile dataclass is immutable."""
|
||||
p = generate_profile(seed=42)
|
||||
with pytest.raises(FrozenInstanceError):
|
||||
p.seed = 99 # type: ignore[misc]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_fonts_is_list_of_strings():
|
||||
"""GP11 — fonts is a non-empty list of stripped strings."""
|
||||
p = generate_profile(seed=42)
|
||||
assert isinstance(p.fonts, list)
|
||||
assert len(p.fonts) > 0
|
||||
assert all(isinstance(f, str) and f.strip() == f for f in p.fonts)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_to_prefs_dict_flat_and_matches_raw():
|
||||
"""GP12 — to_prefs_dict() returns a flat dict containing core sampler keys."""
|
||||
p = generate_profile(seed=42)
|
||||
d = p.to_prefs_dict()
|
||||
assert isinstance(d, dict)
|
||||
for key in ("screen_w", "screen_h", "webgl_vendor", "webgl_renderer",
|
||||
"hw_concurrency", "stealth_seed"):
|
||||
assert key in d
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_seed_zero():
|
||||
"""GP13 — seed=0 is a valid lowest-value boundary."""
|
||||
p = generate_profile(seed=0)
|
||||
assert p.seed == 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_seed_max_int31():
|
||||
"""GP14 — seed at int31 upper bound works."""
|
||||
seed = (1 << 31) - 1
|
||||
p = generate_profile(seed=seed)
|
||||
assert p.seed == seed
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_dark_theme_is_bool():
|
||||
"""GP15 — dark_theme is coerced to bool on the dataclass."""
|
||||
p = generate_profile(seed=42)
|
||||
assert isinstance(p.dark_theme, bool)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Additional pin coverage (recheck pass)
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_pin_dark_theme_true():
|
||||
"""Pinning dark_theme=True flows through coercion to bool."""
|
||||
p = generate_profile(seed=42, pin={"dark_theme": True})
|
||||
assert p.dark_theme is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_pin_dark_theme_false():
|
||||
p = generate_profile(seed=42, pin={"dark_theme": False})
|
||||
assert p.dark_theme is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_pin_fonts_list_visible_on_profile():
|
||||
"""fonts pin: list → joined raw string → split back to list on Profile."""
|
||||
p = generate_profile(seed=42, pin={"fonts": ["Arial", "Verdana"]})
|
||||
assert p.fonts == ["Arial", "Verdana"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_pin_gpu_renderer_propagates():
|
||||
p = generate_profile(seed=42, pin={"gpu.renderer": "FORCED_RENDERER"})
|
||||
assert p.gpu.renderer == "FORCED_RENDERER"
|
||||
assert p.to_prefs_dict()["webgl_renderer"] == "FORCED_RENDERER"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_generate_profile_pin_to_raw_keymap_complete():
|
||||
"""Every dotted pin key (besides 'fonts') has a `_PIN_TO_RAW` mapping.
|
||||
|
||||
Guards against silently-ignored pins if someone adds a key to `_PIN_GROUPS`
|
||||
but forgets the raw-key mapping.
|
||||
"""
|
||||
dotted = {f"{group}.{field}" for group, fields in _PIN_GROUPS.items()
|
||||
for field in fields}
|
||||
# 'dark_theme' is top-level and present in _PIN_TO_RAW; 'fonts' is handled
|
||||
# specially and intentionally absent.
|
||||
missing = dotted - set(_PIN_TO_RAW.keys())
|
||||
assert missing == set(), f"pin keys without raw mapping: {sorted(missing)}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue