Files
camoufox/pythonlib/tests/test_version_floor_upgrade.py
T
Jake WriterandClaude Opus 5 ce87cf7dab fix(python): report an unsatisfiable version floor instead of recursing
camoufox_path() ended in `return camoufox_path()` after a fetch. When the
newest published build is still below CONSTRAINTS.MIN_VERSION, install() is a
no-op ("already installed") and that tail recursed ~1000 times -- each
iteration firing another GitHub API call, which exhausts the unauthenticated
rate limit (60/hr) long before the RecursionError lands.

That is precisely the state a library published ahead of its browser release
puts every user in, and it is reachable now that the floor is raised. It also
hits permanently for anyone using a repos.yml source that does not carry the
required build.

Re-check after the fetch instead, and raise UnsupportedVersion naming the
required minimum.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 15:06:48 -06:00

122 lines
4.7 KiB
Python

"""Regression coverage for raising the supported browser floor.
Raising ``CONSTRAINTS.MIN_VERSION`` is how the library forces an existing
install to upgrade when it can no longer talk to the old browser. That path
had been dead since the floor was dropped to ``alpha.1``, and it hid a bug:
``camoufox_path`` probed ``INSTALL_DIR/version.json``, which only the
pre-multiversion flat layout ever wrote, so a below-floor versioned install
raised ``FileNotFoundError`` instead of falling through to a fetch.
"""
import json
import pytest
from camoufox import multiversion, pkgman
from camoufox.exceptions import UnsupportedVersion
def _install(tmp_path, monkeypatch, layout, build, floor):
"""Build an install dir in the given layout and point the library at it."""
root = tmp_path / "cache"
root.mkdir()
(root / ".0.5_FLAG").write_text("")
(root / "repo_cache.json").write_text("{}")
if layout == "versioned":
relative = f"browsers/official/152.0.4-{build}"
(root / "config.json").write_text(json.dumps({"active_version": relative}))
version_dir = root / relative
version_dir.mkdir(parents=True)
else:
(root / "config.json").write_text("{}")
version_dir = root
(version_dir / "version.json").write_text(
json.dumps({"version": "152.0.4", "build": build})
)
for module in (pkgman, multiversion):
monkeypatch.setattr(module, "INSTALL_DIR", root)
monkeypatch.setattr(multiversion, "BROWSERS_DIR", root / "browsers")
monkeypatch.setattr(multiversion, "CONFIG_FILE", root / "config.json")
monkeypatch.setattr(multiversion, "COMPAT_FLAG", root / ".0.5_FLAG")
monkeypatch.setattr(pkgman, "VERSION_MIN", pkgman.Version(build=floor))
return root
@pytest.mark.parametrize("layout", ["versioned", "legacy"])
def test_below_floor_install_is_reported_as_outdated(tmp_path, monkeypatch, layout):
"""A build under the floor must report as outdated, never as missing."""
_install(tmp_path, monkeypatch, layout, build="beta.29", floor="beta.30")
with pytest.raises(UnsupportedVersion):
pkgman.camoufox_path(download_if_missing=False)
@pytest.mark.parametrize("layout", ["versioned", "legacy"])
def test_at_floor_install_is_kept(tmp_path, monkeypatch, layout):
"""A build at the floor is still served, in either layout."""
root = _install(tmp_path, monkeypatch, layout, build="beta.30", floor="beta.30")
resolved = pkgman.camoufox_path(download_if_missing=False)
expected = root if layout == "legacy" else root / "browsers/official/152.0.4-beta.30"
assert resolved == expected
def test_below_floor_install_triggers_a_fetch(tmp_path, monkeypatch):
"""The default path upgrades the install instead of raising."""
root = _install(tmp_path, monkeypatch, "versioned", build="beta.29", floor="beta.30")
installed = []
class StubFetcher:
def install(self):
installed.append(True)
relative = "browsers/official/152.0.4-beta.30"
version_dir = root / relative
version_dir.mkdir(parents=True)
(version_dir / "version.json").write_text(
json.dumps({"version": "152.0.4", "build": "beta.30"})
)
(root / "config.json").write_text(json.dumps({"active_version": relative}))
monkeypatch.setattr(pkgman, "CamoufoxFetcher", StubFetcher)
resolved = pkgman.camoufox_path()
assert installed == [True]
assert resolved == root / "browsers/official/152.0.4-beta.30"
def test_root_probe_tolerates_the_versioned_layout(tmp_path, monkeypatch):
"""The root probe reports False, rather than raising, with no root file."""
root = tmp_path / "cache"
root.mkdir()
monkeypatch.setattr(pkgman, "INSTALL_DIR", root)
assert pkgman._root_install_supported() is False
def test_unsatisfiable_floor_reports_instead_of_recursing(tmp_path, monkeypatch):
"""A floor no published build satisfies must report, not spin.
install() is a no-op when the newest release is already installed, so the
old `return camoufox_path()` tail recursed ~1000 times -- each iteration
firing another GitHub API call, which exhausts the unauthenticated rate
limit long before the RecursionError lands. That is the state a library
published ahead of its browser release puts every user in.
"""
_install(tmp_path, monkeypatch, "versioned", build="beta.29", floor="beta.30")
attempts = []
class StubFetcher:
def install(self):
attempts.append(True) # newest published build is still below the floor
monkeypatch.setattr(pkgman, "CamoufoxFetcher", StubFetcher)
with pytest.raises(UnsupportedVersion):
pkgman.camoufox_path()
assert attempts == [True], "should fetch once, not spin"