"""浏览器池主动回收逻辑的单元测试(不启动真实 Chromium)。""" from __future__ import annotations import asyncio import pytest from app.core.config import Settings from app.services.browser_pool import BrowserPool class _FakeBrowser: """最小化的浏览器替身,仅实现连接状态查询。""" def __init__(self, connected: bool = True): self._connected = connected def is_connected(self) -> bool: return self._connected def _make_pool(**overrides) -> BrowserPool: settings = Settings(**overrides) pool = BrowserPool(settings) pool._browser = _FakeBrowser() pool._started = True return pool @pytest.mark.asyncio async def test_recycle_due_by_context_count(): pool = _make_pool(browser_recycle_after_contexts=10, browser_recycle_after_seconds=0) pool._browser_started_at = asyncio.get_running_loop().time() pool._contexts_since_start = 9 assert pool._recycle_due() is False pool._contexts_since_start = 10 assert pool._recycle_due() is True @pytest.mark.asyncio async def test_recycle_due_by_age(): pool = _make_pool(browser_recycle_after_contexts=0, browser_recycle_after_seconds=100) now = asyncio.get_running_loop().time() pool._browser_started_at = now - 99 assert pool._recycle_due() is False pool._browser_started_at = now - 101 assert pool._recycle_due() is True @pytest.mark.asyncio async def test_recycle_disabled_when_thresholds_zero(): pool = _make_pool(browser_recycle_after_contexts=0, browser_recycle_after_seconds=0) pool._browser_started_at = asyncio.get_running_loop().time() - 99999 pool._contexts_since_start = 99999 assert pool._recycle_due() is False @pytest.mark.asyncio async def test_maybe_recycle_rebuilds_when_idle(monkeypatch): pool = _make_pool(browser_recycle_after_contexts=10, browser_recycle_after_seconds=0) pool._browser_started_at = asyncio.get_running_loop().time() pool._contexts_since_start = 10 pool._active_contexts = 0 calls: list[str] = [] async def fake_close(): calls.append("close") async def fake_start(): calls.append("start") pool._browser = _FakeBrowser() pool._started = True pool._contexts_since_start = 0 # start() 正常会重置计数 monkeypatch.setattr(pool, "close", fake_close) monkeypatch.setattr(pool, "start", fake_start) await pool._maybe_recycle() assert calls == ["close", "start"] @pytest.mark.asyncio async def test_maybe_recycle_skips_when_busy(monkeypatch): pool = _make_pool(browser_recycle_after_contexts=10, browser_recycle_after_seconds=0) pool._browser_started_at = asyncio.get_running_loop().time() pool._contexts_since_start = 10 pool._active_contexts = 1 # 仍有会话在用,不应回收 calls: list[str] = [] monkeypatch.setattr(pool, "close", lambda: calls.append("close")) monkeypatch.setattr(pool, "start", lambda: calls.append("start")) await pool._maybe_recycle() assert calls == [] @pytest.mark.asyncio async def test_maybe_recycle_noop_when_not_due(monkeypatch): pool = _make_pool(browser_recycle_after_contexts=10, browser_recycle_after_seconds=0) pool._browser_started_at = asyncio.get_running_loop().time() pool._contexts_since_start = 3 calls: list[str] = [] monkeypatch.setattr(pool, "close", lambda: calls.append("close")) monkeypatch.setattr(pool, "start", lambda: calls.append("start")) await pool._maybe_recycle() assert calls == []