Files
OpenBB/openbb_platform/providers/cboe/tests/test_shared_cache.py
dlee-bq 7f49354884 [BugFix] openbb-cboe: Fix cache lock so that concurrent callers in separate processes don't hang (#7796)
* fix cboe's cache lock so that concurrent callers in separate processes don't hang

* fix test coverage and ty check

* more test failure

* more test failure

* more Windows Python 3.10 test failure

* Move lint path filter into a job so required checks always report

lint-openbb-platform.yml used `on.pull_request.paths` to skip PRs that only
touch api/extensions/providers/obbject_extensions. A workflow skipped by a
trigger filter never reports its checks, so the required "General Code
Linting" and "Markdown Linting" checks stayed pending forever on those PRs.

Detect relevant changes in a `changes` job instead and gate the lint jobs with
a job-level `if:`. Skipped jobs report success, so required checks resolve.
The gate fails closed: if detection fails, the lint jobs still run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: dlee-bq <325002271+dlee-bq@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:50:40 +00:00

109 lines
3.8 KiB
Python

"""Tests for openbb_cboe.utils.shared_cache."""
import asyncio
from unittest.mock import AsyncMock
import pytest
from openbb_cboe.utils.shared_cache import SharedCache, SharedStore
async def _gather(iterable):
"""Collect everything an async iterable yields."""
return [item async for item in iterable]
class TestSharedStore:
"""A store hands every operation to ``call``, whichever loop it is used on."""
@pytest.mark.parametrize(
("method", "args", "returned"),
[
("contains", ("key",), True),
("read", ("key",), "item"),
("size", (), 7),
("write", ("key", "item"), None),
("delete", ("key",), None),
("bulk_delete", ({"key"},), None),
("clear", (), None),
],
)
def test_an_operation_is_run_where_the_connection_is(self, method, args, returned):
"""The table, the method and its arguments go over, and the result comes back."""
call = AsyncMock(return_value=returned)
store = SharedStore("responses", call)
result = asyncio.run(getattr(store, method)(*args))
assert result == returned
call.assert_awaited_once_with("responses", method, *args)
@pytest.mark.parametrize("method", ["keys", "values"])
def test_what_is_yielded_is_yielded_here(self, method):
"""Keys and values arrive gathered, and are yielded on the caller's loop."""
call = AsyncMock(return_value=["a", "b"])
store = SharedStore("redirects", call)
assert asyncio.run(_gather(getattr(store, method)())) == ["a", "b"]
call.assert_awaited_once_with("redirects", method)
def test_closing_leaves_the_connection_to_the_cache(self):
"""Other requests are still using the connection, so a session may not close it."""
call = AsyncMock()
asyncio.run(SharedStore("responses", call).close())
call.assert_not_awaited()
def test_pop_reads_then_deletes(self):
"""The base class's ``pop`` works through the same two operations."""
call = AsyncMock(side_effect=lambda table, method, *args: "item")
assert asyncio.run(SharedStore("responses", call).pop("key")) == "item"
assert [step.args[1] for step in call.await_args_list] == ["read", "delete"]
def test_an_error_where_the_connection_is_reaches_the_caller(self):
"""A failed read is the failure of the request that made it."""
call = AsyncMock(side_effect=OSError("disk"))
with pytest.raises(OSError):
asyncio.run(SharedStore("responses", call).read("key"))
class TestSharedCache:
"""The backend decides expiry and keys itself, and stores through ``call``."""
def test_its_tables_are_the_responses_and_the_redirects(self):
"""Each table of the backend is a store of the same name."""
call = AsyncMock()
cache = SharedCache(call)
asyncio.run(cache.responses.write("key", "item"))
asyncio.run(cache.redirects.write("alias", "key"))
assert [step.args[:2] for step in call.await_args_list] == [
("responses", "write"),
("redirects", "write"),
]
def test_it_keeps_the_settings_it_is_given(self):
"""Expiry is applied on the caller's loop, from the backend's own settings."""
cache = SharedCache(
AsyncMock(),
expire_after=60,
urls_expire_after={"*/book/*": 5},
autoclose=False,
)
assert cache.expire_after == 60
assert cache.urls_expire_after == {"*/book/*": 5}
assert cache.autoclose is False
def test_closing_it_does_not_close_the_shared_connection(self):
"""Closing the backend is not closing the connection other requests use."""
call = AsyncMock()
asyncio.run(SharedCache(call).close())
call.assert_not_awaited()