Research-Stack/4-Infrastructure/NoDupeLabs/tests/hashing/test_hash_cache.py

467 lines
14 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2025 NoDupeLabs
"""Tests to achieve 100% coverage on hash_cache.py module.
This test file targets the missing coverage in:
- hash_cache.py: HashCache class methods
"""
import time
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from nodupe.tools.hashing.hash_cache import (
HashCache,
HashCacheError,
create_hash_cache,
)
class TestHashCacheInit:
"""Tests for HashCache initialization."""
def test_init_default_values(self):
"""Test HashCache with default values."""
cache = HashCache()
assert cache.max_size == 1000
assert cache.ttl_seconds == 3600
assert cache.enable_persistence is False
def test_init_custom_values(self):
"""Test HashCache with custom values."""
cache = HashCache(max_size=500, ttl_seconds=1800, enable_persistence=True)
assert cache.max_size == 500
assert cache.ttl_seconds == 1800
assert cache.enable_persistence is True
def test_init_stats_initialized(self):
"""Test stats are initialized correctly."""
cache = HashCache()
stats = cache._stats
assert stats['hits'] == 0
assert stats['misses'] == 0
assert stats['evictions'] == 0
assert stats['insertions'] == 0
class TestGetHash:
"""Tests for get_hash method."""
def test_get_hash_miss(self):
"""Test get_hash when not cached."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
mock_path.__str__ = MagicMock(return_value="/test/path")
result = cache.get_hash(mock_path)
assert result is None
def test_get_hash_hit(self):
"""Test get_hash when cached."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
mock_path.stat.return_value.st_mtime = 1000.0
# Manually add to cache
cache._cache[path_str] = ("hash_value", 1000.0, time.monotonic())
result = cache.get_hash(mock_path)
assert result == "hash_value"
def test_get_hash_expired(self):
"""Test get_hash when entry is expired."""
cache = HashCache(ttl_seconds=1)
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
mock_path.stat.return_value.st_mtime = 1000.0
# Add to cache with old timestamp
old_timestamp = time.monotonic() - 10 # 10 seconds ago
cache._cache[path_str] = ("hash_value", 1000.0, old_timestamp)
result = cache.get_hash(mock_path)
assert result is None
def test_get_hash_file_modified(self):
"""Test get_hash when file has been modified."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
# Current mtime differs from stored mtime
mock_path.stat.return_value.st_mtime = 2000.0
# Add to cache with old mtime
cache._cache[path_str] = ("hash_value", 1000.0, time.monotonic())
result = cache.get_hash(mock_path)
assert result is None
def test_get_hash_file_not_found(self):
"""Test get_hash when file no longer exists."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
mock_path.stat.side_effect = OSError("File not found")
# Add to cache
cache._cache[path_str] = ("hash_value", 1000.0, time.monotonic())
result = cache.get_hash(mock_path)
assert result is None
class TestSetHash:
"""Tests for set_hash method."""
def test_set_hash_basic(self):
"""Test basic set_hash."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
mock_path.stat.return_value.st_mtime = 1000.0
cache.set_hash(mock_path, "hash_value")
assert path_str in cache._cache
assert cache._cache[path_str][0] == "hash_value"
def test_set_hash_updates_stats(self):
"""Test set_hash updates insertion stats."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
mock_path.stat.return_value.st_mtime = 1000.0
cache.set_hash(mock_path, "hash_value")
assert cache._stats['insertions'] == 1
def test_set_hash_eviction_when_full(self):
"""Test set_hash evicts oldest when cache is full."""
cache = HashCache(max_size=2)
mock_path1 = MagicMock(spec=Path)
mock_path1.__str__ = MagicMock(return_value="/path1")
mock_path1.stat.return_value.st_mtime = 1000.0
mock_path2 = MagicMock(spec=Path)
mock_path2.__str__ = MagicMock(return_value="/path2")
mock_path2.stat.return_value.st_mtime = 1000.0
mock_path3 = MagicMock(spec=Path)
mock_path3.__str__ = MagicMock(return_value="/path3")
mock_path3.stat.return_value.st_mtime = 1000.0
cache.set_hash(mock_path1, "hash1")
cache.set_hash(mock_path2, "hash2")
cache.set_hash(mock_path3, "hash3")
assert len(cache._cache) == 2
assert cache._stats['evictions'] == 1
def test_set_hash_nonexistent_file(self):
"""Test set_hash with non-existent file."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
mock_path.stat.side_effect = OSError("File not found")
cache.set_hash(mock_path, "hash_value")
assert len(cache._cache) == 0
class TestInvalidate:
"""Tests for invalidate method."""
def test_invalidate_existing(self):
"""Test invalidating existing entry."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
mock_path.stat.return_value.st_mtime = 1000.0
cache.set_hash(mock_path, "hash_value")
result = cache.invalidate(mock_path)
assert result is True
assert path_str not in cache._cache
def test_invalidate_nonexisting(self):
"""Test invalidating non-existing entry."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
result = cache.invalidate(mock_path)
assert result is False
class TestInvalidateAll:
"""Tests for invalidate_all method."""
def test_invalidate_all(self):
"""Test invalidating all entries."""
cache = HashCache(max_size=10)
mock_path1 = MagicMock(spec=Path)
mock_path1.__str__ = MagicMock(return_value="/path1")
mock_path1.stat.return_value.st_mtime = 1000.0
mock_path2 = MagicMock(spec=Path)
mock_path2.__str__ = MagicMock(return_value="/path2")
mock_path2.stat.return_value.st_mtime = 1000.0
cache.set_hash(mock_path1, "hash1")
cache.set_hash(mock_path2, "hash2")
cache.invalidate_all()
assert len(cache._cache) == 0
assert cache._stats['evictions'] == 2
class TestValidateCache:
"""Tests for validate_cache method."""
def test_validate_cache_removes_expired(self):
"""Test validate_cache removes expired entries."""
cache = HashCache(ttl_seconds=1)
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
mock_path.stat.return_value.st_mtime = 1000.0
# Add with old timestamp
old_timestamp = time.monotonic() - 10
cache._cache[path_str] = ("hash_value", 1000.0, old_timestamp)
removed = cache.validate_cache()
assert removed == 1
assert path_str not in cache._cache
def test_validate_cache_removes_modified(self):
"""Test validate_cache removes entries with modified files."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
mock_path.stat.return_value.st_mtime = 2000.0 # Different from stored
cache._cache[path_str] = ("hash_value", 1000.0, time.monotonic())
removed = cache.validate_cache()
assert removed == 1
def test_validate_cache_removes_missing_files(self):
"""Test validate_cache removes entries for missing files."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
mock_path.stat.side_effect = OSError("File not found")
cache._cache[path_str] = ("hash_value", 1000.0, time.monotonic())
removed = cache.validate_cache()
assert removed == 1
class TestGetStats:
"""Tests for get_stats method."""
def test_get_stats_basic(self):
"""Test basic get_stats."""
cache = HashCache()
stats = cache.get_stats()
assert 'hits' in stats
assert 'misses' in stats
assert 'evictions' in stats
assert 'insertions' in stats
assert 'size' in stats
assert 'capacity' in stats
assert 'hit_rate' in stats
def test_get_stats_with_hits(self):
"""Test get_stats with cache hits."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
mock_path.stat.return_value.st_mtime = 1000.0
cache.set_hash(mock_path, "hash_value")
cache.get_hash(mock_path) # Hit
cache.get_hash(mock_path) # Hit
stats = cache.get_stats()
assert stats['hits'] == 2
assert stats['hit_rate'] == 1.0
def test_get_stats_hit_rate_no_hits_or_misses(self):
"""Test hit rate calculation with no activity."""
cache = HashCache()
stats = cache.get_stats()
assert stats['hit_rate'] == 0.0
class TestGetCacheSize:
"""Tests for get_cache_size method."""
def test_get_cache_size_empty(self):
"""Test get_cache_size when empty."""
cache = HashCache()
assert cache.get_cache_size() == 0
def test_get_cache_size_with_entries(self):
"""Test get_cache_size with entries."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
mock_path.stat.return_value.st_mtime = 1000.0
cache.set_hash(mock_path, "hash_value")
assert cache.get_cache_size() == 1
class TestIsCached:
"""Tests for is_cached method."""
def test_is_cached_true(self):
"""Test is_cached when cached."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
mock_path.stat.return_value.st_mtime = 1000.0
cache.set_hash(mock_path, "hash_value")
assert cache.is_cached(mock_path) is True
def test_is_cached_false(self):
"""Test is_cached when not cached."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
assert cache.is_cached(mock_path) is False
class TestCleanupExpired:
"""Tests for cleanup_expired method."""
def test_cleanup_expired(self):
"""Test cleanup_expired."""
cache = HashCache(ttl_seconds=1)
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
mock_path.stat.return_value.st_mtime = 1000.0
old_timestamp = time.monotonic() - 10
cache._cache[path_str] = ("hash_value", 1000.0, old_timestamp)
removed = cache.cleanup_expired()
assert removed == 1
class TestResize:
"""Tests for resize method."""
def test_resize_smaller(self):
"""Test resizing to smaller size."""
cache = HashCache(max_size=10)
mock_path1 = MagicMock(spec=Path)
mock_path1.__str__ = MagicMock(return_value="/path1")
mock_path1.stat.return_value.st_mtime = 1000.0
mock_path2 = MagicMock(spec=Path)
mock_path2.__str__ = MagicMock(return_value="/path2")
mock_path2.stat.return_value.st_mtime = 1000.0
cache.set_hash(mock_path1, "hash1")
cache.set_hash(mock_path2, "hash2")
cache.resize(1)
assert cache.max_size == 1
assert len(cache._cache) == 1
def test_resize_larger(self):
"""Test resizing to larger size."""
cache = HashCache(max_size=2)
cache.resize(10)
assert cache.max_size == 10
class TestGetMemoryUsage:
"""Tests for get_memory_usage method."""
def test_get_memory_usage_empty(self):
"""Test get_memory_usage when empty."""
cache = HashCache()
usage = cache.get_memory_usage()
assert usage == 0
def test_get_memory_usage_with_entries(self):
"""Test get_memory_usage with entries."""
cache = HashCache()
mock_path = MagicMock(spec=Path)
path_str = "/test/path"
mock_path.__str__ = MagicMock(return_value=path_str)
mock_path.stat.return_value.st_mtime = 1000.0
cache.set_hash(mock_path, "hash_value")
usage = cache.get_memory_usage()
assert usage > 0
class TestCreateHashCache:
"""Tests for create_hash_cache factory function."""
def test_create_hash_cache_default(self):
"""Test create_hash_cache with defaults."""
cache = create_hash_cache()
assert isinstance(cache, HashCache)
assert cache.max_size == 1000
assert cache.ttl_seconds == 3600
def test_create_hash_cache_custom(self):
"""Test create_hash_cache with custom values."""
cache = create_hash_cache(max_size=500, ttl_seconds=1800)
assert cache.max_size == 500
assert cache.ttl_seconds == 1800
class TestHashCacheError:
"""Tests for HashCacheError exception."""
def test_hash_cache_error_creation(self):
"""Test HashCacheError can be created."""
error = HashCacheError("Test error")
assert str(error) == "Test error"
def test_hash_cache_error_inheritance(self):
"""Test HashCacheError inherits from Exception."""
error = HashCacheError("Test")
assert isinstance(error, Exception)