mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-28 13:01:38 +00:00
* merge branch develop with main * bump desktop version * fix the merge * sync Cargo.lock to desktop 2.0.0 * fix cli conftest.py --------- Co-authored-by: deeleeramone <>
192 lines
6.3 KiB
Python
192 lines
6.3 KiB
Python
"""Tests for the Deribit response cache."""
|
|
|
|
import pytest
|
|
|
|
from openbb_deribit.utils import cache
|
|
|
|
|
|
class _Store:
|
|
"""A stand-in for the backend's response store."""
|
|
|
|
def __init__(self, keys):
|
|
self._keys = list(keys)
|
|
self.deleted: list = []
|
|
|
|
async def keys(self):
|
|
"""Yield every stored key."""
|
|
for key in self._keys:
|
|
yield key
|
|
|
|
async def size(self):
|
|
"""Return how many responses are stored."""
|
|
return len(self._keys)
|
|
|
|
async def bulk_delete(self, keys):
|
|
"""Forget the named responses."""
|
|
self.deleted.append(set(keys))
|
|
self._keys = [key for key in self._keys if key not in keys]
|
|
|
|
|
|
class _Backend:
|
|
"""A stand-in for the SQLite backend."""
|
|
|
|
def __init__(self, keys=()):
|
|
self.responses = _Store(keys)
|
|
self.expired = 0
|
|
|
|
async def delete_expired_responses(self):
|
|
"""Mark the expired responses for deletion."""
|
|
self.expired += 1
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_sweep():
|
|
"""Let each test sweep once, as the first caller in a process would."""
|
|
cache._swept = False
|
|
yield
|
|
cache._swept = False
|
|
|
|
|
|
class TestConfiguration:
|
|
"""The cache is bounded by a TTL per endpoint and a size cap."""
|
|
|
|
def test_path_lives_under_the_user_cache(self):
|
|
"""The cache file is kept with the other OpenBB HTTP caches."""
|
|
assert cache.cache_path().endswith("/http/deribit")
|
|
|
|
def test_backend_carries_the_ttl_map(self, tmp_path, monkeypatch):
|
|
"""The backend is built with the per-endpoint expiries."""
|
|
monkeypatch.setattr(cache, "cache_path", lambda: str(tmp_path / "deribit"))
|
|
backend = cache.get_cache_backend()
|
|
|
|
assert backend.expire_after == cache.CACHE_TTL_DEFAULT
|
|
assert backend.urls_expire_after
|
|
|
|
def test_live_endpoints_are_never_cached(self):
|
|
"""A quote or a book is read fresh every time."""
|
|
for pattern in (
|
|
"*/public/get_order_book*",
|
|
"*/public/ticker*",
|
|
"*/public/get_time*",
|
|
"*/public/status*",
|
|
):
|
|
assert cache.URL_CACHE_TTL[pattern] == 0
|
|
|
|
def test_reference_endpoints_are_held_longest(self):
|
|
"""A listing that barely changes is held for a day."""
|
|
assert cache.URL_CACHE_TTL["*/public/get_currencies*"] == cache.DAY
|
|
|
|
|
|
class TestSweep:
|
|
"""The sweep drops what expired, then trims what is still too big."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_deletes_expired_and_vacuums(self, monkeypatch, tmp_path):
|
|
"""A file under the cap is swept once and left alone."""
|
|
stored = tmp_path / "deribit.sqlite"
|
|
stored.write_bytes(b"x")
|
|
monkeypatch.setattr(cache, "cache_path", lambda: str(tmp_path / "deribit"))
|
|
vacuums: list = []
|
|
|
|
async def _vacuum(backend):
|
|
vacuums.append(backend)
|
|
|
|
monkeypatch.setattr(cache, "_vacuum", _vacuum)
|
|
backend = _Backend(["a", "b"])
|
|
await cache.sweep_cache(backend)
|
|
|
|
assert backend.expired == 1
|
|
assert len(vacuums) == 1
|
|
assert backend.responses.deleted == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trims_by_the_share_that_overshoots(self, monkeypatch, tmp_path):
|
|
"""A file over the cap loses the oldest share of its rows."""
|
|
stored = tmp_path / "deribit.sqlite"
|
|
stored.write_bytes(b"x" * 400)
|
|
monkeypatch.setattr(cache, "cache_path", lambda: str(tmp_path / "deribit"))
|
|
monkeypatch.setattr(cache, "CACHE_MAX_BYTES", 100)
|
|
backend = _Backend([f"key-{index}" for index in range(8)])
|
|
|
|
async def _vacuum(_):
|
|
if backend.responses.deleted:
|
|
stored.write_bytes(b"x" * 50)
|
|
|
|
monkeypatch.setattr(cache, "_vacuum", _vacuum)
|
|
await cache.sweep_cache(backend)
|
|
|
|
assert backend.responses.deleted
|
|
assert len(backend.responses.deleted[0]) == 6
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stops_when_nothing_is_left_to_drop(self, monkeypatch, tmp_path):
|
|
"""A file still too big with nothing stored gives up."""
|
|
stored = tmp_path / "deribit.sqlite"
|
|
stored.write_bytes(b"x" * 400)
|
|
monkeypatch.setattr(cache, "cache_path", lambda: str(tmp_path / "deribit"))
|
|
monkeypatch.setattr(cache, "CACHE_MAX_BYTES", 100)
|
|
|
|
async def _vacuum(backend):
|
|
return None
|
|
|
|
monkeypatch.setattr(cache, "_vacuum", _vacuum)
|
|
backend = _Backend([])
|
|
await cache.sweep_cache(backend)
|
|
|
|
assert backend.responses.deleted == []
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_missing_file(self, monkeypatch, tmp_path):
|
|
"""A cache that was never written is swept without error."""
|
|
monkeypatch.setattr(cache, "cache_path", lambda: str(tmp_path / "deribit"))
|
|
|
|
async def _vacuum(backend):
|
|
return None
|
|
|
|
monkeypatch.setattr(cache, "_vacuum", _vacuum)
|
|
backend = _Backend([])
|
|
await cache.sweep_cache(backend)
|
|
|
|
assert backend.expired == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sweeps_once_per_process(self, monkeypatch, tmp_path):
|
|
"""A second caller does not sweep again."""
|
|
monkeypatch.setattr(cache, "cache_path", lambda: str(tmp_path / "deribit"))
|
|
|
|
async def _vacuum(backend):
|
|
return None
|
|
|
|
monkeypatch.setattr(cache, "_vacuum", _vacuum)
|
|
backend = _Backend([])
|
|
await cache.sweep_cache(backend)
|
|
await cache.sweep_cache(backend)
|
|
|
|
assert backend.expired == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_oldest_stops_at_the_count(self):
|
|
"""The trim reads only as many keys as it means to drop."""
|
|
backend = _Backend([f"key-{index}" for index in range(10)])
|
|
|
|
assert len(await cache._oldest(backend, 3)) == 3
|
|
|
|
|
|
class TestVacuum:
|
|
"""Vacuuming gives back the pages the deleted responses held."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skips_a_foreign_backend(self):
|
|
"""A store that is not SQLite is left alone."""
|
|
assert await cache._vacuum(_Backend([])) is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_runs_against_sqlite(self, tmp_path, monkeypatch):
|
|
"""A real SQLite store is vacuumed."""
|
|
monkeypatch.setattr(cache, "cache_path", lambda: str(tmp_path / "deribit"))
|
|
backend = cache.get_cache_backend()
|
|
await backend.responses.write("key", b"value")
|
|
await cache._vacuum(backend)
|
|
|
|
assert (tmp_path / "deribit.sqlite").exists()
|