import asyncio import pytest from ragas import cacher from ragas.cache import DiskCacheBackend, _generate_cache_key, _make_hashable @pytest.fixture(scope="function") def temp_cache_dir(tmp_path): """Use a temporary directory for caching.""" return str(tmp_path) @pytest.fixture(scope="function") def cache_backend(temp_cache_dir): """Provide a DiskCacheBackend instance with a temporary directory.""" return DiskCacheBackend(cache_dir=temp_cache_dir) def test_make_hashable(): """Test that _make_hashable converts various objects into a hashable structure.""" data = {"tuple": (1, 2), "list": [3, 4], "set": {5, 6}, "dict": {"a": 1, "b": 2}} result = _make_hashable(data) assert isinstance(result, tuple) assert len(result) == len(data) def test_generate_cache_key(): """Test that cache keys change when arguments or kwargs differ.""" def sample_func(a, b): return a + b key1 = _generate_cache_key(sample_func, (1, 2), {}) key2 = _generate_cache_key(sample_func, (2, 2), {}) assert key1 != key2, "Cache keys should differ for different args" key3 = _generate_cache_key(sample_func, (1, 2), {"c": 3}) assert key1 != key3, "Cache keys should differ if kwargs differ" def test_generate_cache_key_bound_method(): """Test that cache keys stay the same, when caching bound methods of different objects.""" class Clazz: def __init__(self, irrelevant): self.irrelevant = irrelevant def sample_func(self, a, b): return a + b object = Clazz(irrelevant=1) object2 = Clazz(irrelevant=2) key1 = _generate_cache_key(object.sample_func, (1, 2), {}) key2 = _generate_cache_key(object2.sample_func, (1, 2), {}) assert key1 == key2, ( "Cache keys should match even if the originating objects the methods are bound to are not the same, as long as the arguments match" ) def test_no_cache_backend(): """Test that if no cache backend is provided, results are not cached.""" call_count = {"count": 0} @cacher(cache_backend=None) def no_cache_func(): call_count["count"] += 1 return call_count["count"] # Each call should increment count since caching is disabled val1 = no_cache_func() val2 = no_cache_func() assert val2 == val1 + 1, "Without a cache backend, calls should not be cached." def test_caching_with_cache_backend(cache_backend): """Test that providing a cache backend enables caching.""" call_count = {"count": 0} @cacher(cache_backend=cache_backend) def expensive_function(): call_count["count"] += 1 return "expensive_result" # First call: should run the function result1 = expensive_function() assert result1 == "expensive_result" assert call_count["count"] == 1 # Second call with same args: should return cached result, not increment call_count result2 = expensive_function() assert result2 == "expensive_result" assert call_count["count"] == 1, "Call count should not increase on cached result" @pytest.mark.asyncio async def test_async_caching_with_cache_backend(cache_backend): """Test that caching works for async functions when a backend is provided.""" call_count = {"count": 0} @cacher(cache_backend=cache_backend) async def async_expensive_function(x): call_count["count"] += 1 await asyncio.sleep(0.1) return x * 2 # First call: should run the function result1 = await async_expensive_function(10) assert result1 == 20 assert call_count["count"] == 1 # Second call with same args: should return cached result result2 = await async_expensive_function(10) assert result2 == 20 assert call_count["count"] == 1, "Should have come from cache" @pytest.mark.filterwarnings("ignore:.*coroutine.*was never awaited:RuntimeWarning") def test_caching_with_different_args(cache_backend): """Test that different arguments produce different cache entries.""" call_count = {"count": 0} @cacher(cache_backend=cache_backend) def multiply(x, y): call_count["count"] += 1 return x * y assert multiply(2, 3) == 6 assert multiply(2, 3) == 6 # Same arguments, should have cached assert call_count["count"] == 1 # Different arguments, cache miss assert multiply(3, 3) == 9 assert call_count["count"] == 2