manager: add missing test cases to get complete coverage
- ID
0246dc9- date
2026-11-06 21:57:19+00:00- author
Alex Chan <alexc@tailscale.com>- parent
522bdc4- message
manager: add missing test cases to get complete coverage- changed files
2 files, 151 additions, 5 deletions
Changed files
src/funkycache/manager.py (10676 → 10753)
diff --git a/src/funkycache/manager.py b/src/funkycache/manager.py
index 7621e02..8058a6b 100644
--- a/src/funkycache/manager.py
+++ b/src/funkycache/manager.py
@@ -11,6 +11,7 @@ from functools import wraps
import inspect
import json
from pathlib import Path
+import typing
from typing import Any, cast, Literal, overload, ParamSpec, Protocol, TypeVar
import warnings
@@ -97,6 +98,9 @@ class FunkyCache:
if storage == "memory" and root is not None:
raise TypeError("Do not specify a root path with memory storage")
+ if storage not in typing.get_args(StorageType):
+ raise TypeError(f"Unrecognised storage: {storage!r}")
+
if root is None:
root = Path(".funkycache")
@@ -168,16 +172,16 @@ class FunkyCache:
return storage.get(key)
except KeyError:
pass
- except Exception as e: # pragma: no cover
+ except Exception as e:
warnings.warn(f"unexpected error looking up key in cache: {e}")
new_value = func(*args, **kwargs)
try:
storage.set(key, new_value)
- except CannotOverwriteValueException: # pragma: no cover
+ except CannotOverwriteValueException:
raise
- except Exception as e: # pragma: no cover
+ except Exception as e:
warnings.warn(f"unexpected error inserting value into cache: {e}")
pass
tests/test_manager.py (13658 → 18240)
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 9b73f20..c6e3bda 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -3,16 +3,19 @@ Tests for the FunkyCache cache manager.
"""
from abc import ABC, abstractmethod
-from collections.abc import Iterator
+from collections.abc import Iterator, Mapping
from collections import Counter
import inspect
from pathlib import Path
+import secrets
import subprocess
import textwrap
+from typing import Any, TypeVar
import pytest
-from funkycache import FunkyCache
+from funkycache import CannotOverwriteValueException, FunkyCache
+from funkycache.storage import CacheStorage, MemoryCacheStorage
class FunkyCacheTestSuite(ABC):
@@ -291,6 +294,145 @@ class FunkyCacheTestSuite(ABC):
assert double(5) == 10
+R = TypeVar("R")
+
+
+class TestFunkyCache:
+ """
+ Tests for FunkyCache that aren't based on the behaviour of
+ a specific storage implementation.
+ """
+
+ def test_cache_depends_on_python_file(self, tmp_path: Path) -> None:
+ """
+ The cache depends on the source file of the cached function.
+ """
+ p = tmp_path / "example.py"
+
+ def run():
+ cmd = ["python3", str(p)]
+ return subprocess.check_output(cmd, cwd=tmp_path, text=True).strip()
+
+ # Write the initial version of the file, and check the first call
+ # is a cache miss, subsequent calls are cache hits
+ p.write_text(
+ textwrap.dedent("""
+ from funkycache import FunkyCache
+
+ GREETING = "hello world"
+
+ cache = FunkyCache(storage="sqlite", root=".cache")
+
+ @cache.register
+ def greet() -> str:
+ print("calling original function")
+ return GREETING
+
+ print(greet())
+ """)
+ )
+
+ assert run() == "calling original function\nhello world"
+ assert run() == "hello world"
+ assert run() == "hello world"
+
+ # Update the file, and check the next call is a cache miss
+ new_text = p.read_text().replace('"hello world"', '"bonjour monde"')
+ p.write_text(new_text)
+
+ assert run() == "calling original function\nbonjour monde"
+
+ def test_cache_get_error_is_ignored(self) -> None:
+ """
+ An error reading from the cache is a warning and falls back
+ to the underlying function.
+ """
+
+ class BrokenGetCacheStorage[V](MemoryCacheStorage[V]):
+ def get(self, key: Mapping[str, Any]) -> V:
+ raise ValueError("BOOM!")
+
+ class BrokenGetFunkyCache(FunkyCache):
+ def _create_storage(
+ self, func_name: str, data_type: R, input_sig: str
+ ) -> CacheStorage[R]:
+ return BrokenGetCacheStorage[R]()
+
+ cache = BrokenGetFunkyCache(storage="memory")
+
+ @cache.register
+ def initial(s: str) -> str:
+ return s[0].upper()
+
+ with pytest.warns(
+ UserWarning, match="unexpected error looking up key in cache"
+ ):
+ assert initial("apple") == "A"
+
+ def test_cache_set_error_is_ignored(self) -> None:
+ """
+ An error writing to the cache is a warning and falls back
+ to the underlying function.
+ """
+
+ class BrokenGetCacheStorage[V](MemoryCacheStorage[V]):
+ def set(self, key: Mapping[str, Any], value: V) -> None:
+ raise ValueError("BOOM!")
+
+ class BrokenGetFunkyCache(FunkyCache):
+ def _create_storage(
+ self, func_name: str, data_type: R, input_sig: str
+ ) -> CacheStorage[R]:
+ return BrokenGetCacheStorage[R]()
+
+ cache = BrokenGetFunkyCache(storage="memory")
+
+ @cache.register
+ def initial(s: str) -> str:
+ return s[0].upper()
+
+ with pytest.warns(
+ UserWarning, match="unexpected error inserting value into cache"
+ ):
+ assert initial("apple") == "A"
+
+ def test_cache_overwrite_value_error_is_raised(self) -> None:
+ """
+ An error where you overwrite the value of an existing key
+ is re-raised.
+ """
+
+ # Create a broken implementation of CacheStorage which always
+ # returns a KeyError when looking up keys.
+ class BrokenGetCacheStorage[V](MemoryCacheStorage[V]):
+ def get(self, key: Mapping[str, Any]) -> V:
+ raise KeyError(key)
+
+ class BrokenGetFunkyCache(FunkyCache):
+ def _create_storage(
+ self, func_name: str, data_type: R, input_sig: str
+ ) -> CacheStorage[R]:
+ return BrokenGetCacheStorage[R]()
+
+ cache = BrokenGetFunkyCache(storage="memory")
+
+ @cache.register
+ def get_token(n: int) -> str:
+ return secrets.token_hex(n)
+
+ assert len(get_token(16)) == 32
+
+ with pytest.raises(CannotOverwriteValueException):
+ get_token(16)
+
+ def test_invalid_storage(self) -> None:
+ """
+ Passing an invalid storage type is a TypeError.
+ """
+ with pytest.raises(TypeError, match="Unrecognised storage"):
+ FunkyCache(storage="does_not_exist") # type: ignore
+
+
class TestMemoryFunkyCache(FunkyCacheTestSuite):
"""
Tests for a FunkyCache which is backed by in-memory storage.