Skip to main content

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.