pig.py/tests/unit/test_storage.py
Russell Ballestrini 1847a2fee6 Add comprehensive unit tests for async modules
Tests for:
- async_web_fetcher: URL handling, media extraction, crawl modes, scoring
- domain_vault: VaultManager, HTML/Media/Linkpeek vaults
- screenshot: ScreenshotCapture configuration and availability
- storage: ImageVault with MD5 deduplication

185 tests total, all passing.
2025-12-22 20:40:58 -05:00

330 lines
9.8 KiB
Python

"""
Tests for storage module (ImageVault).
Tests async content-addressable storage with MD5 hashing.
"""
import pytest
import os
import tempfile
import shutil
import hashlib
from storage import ImageVault
class TestImageVaultBasics:
"""Test basic ImageVault functionality."""
def setup_method(self):
self.temp_dir = tempfile.mkdtemp()
self.vault_path = os.path.join(self.temp_dir, "test_vault")
def teardown_method(self):
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
@pytest.mark.asyncio
async def test_vault_initialization(self):
"""Test ImageVault initializes correctly."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
assert vault._initialized is True
assert os.path.exists(self.vault_path)
@pytest.mark.asyncio
async def test_vault_creates_subdirectories(self):
"""Test that vault creates hash bucket subdirectories."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
# Should create 256 subdirectories (00-ff)
subdirs = os.listdir(self.vault_path)
assert len(subdirs) == 256
# Check some specific ones
assert "00" in subdirs
assert "ff" in subdirs
assert "a5" in subdirs
@pytest.mark.asyncio
async def test_vault_double_init(self):
"""Test that double initialization is safe."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
await vault.init() # Should not raise
assert vault._initialized is True
class TestImageVaultStorage:
"""Test ImageVault storage operations."""
def setup_method(self):
self.temp_dir = tempfile.mkdtemp()
self.vault_path = os.path.join(self.temp_dir, "test_vault")
def teardown_method(self):
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
@pytest.mark.asyncio
async def test_store_and_retrieve(self):
"""Test storing and retrieving data."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
# Create test data
data = b"Hello, World! This is test image data."
md5_hash = hashlib.md5(data).hexdigest()
# Store
path = await vault.store(md5_hash, data, extension="jpg")
assert path is not None
# Retrieve
retrieved = await vault.get(md5_hash)
assert retrieved == data
@pytest.mark.asyncio
async def test_store_with_extension(self):
"""Test storing with file extension."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
data = b"PNG image data here"
md5_hash = hashlib.md5(data).hexdigest()
path = await vault.store(md5_hash, data, extension="png")
assert path.endswith(".png")
@pytest.mark.asyncio
async def test_store_without_extension(self):
"""Test storing without file extension."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
data = b"Binary data without extension"
md5_hash = hashlib.md5(data).hexdigest()
path = await vault.store(md5_hash, data)
# Should just be the hash
assert md5_hash in path
@pytest.mark.asyncio
async def test_exists_true(self):
"""Test exists returns True for stored data."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
data = b"Test data for exists check"
md5_hash = hashlib.md5(data).hexdigest()
await vault.store(md5_hash, data)
assert await vault.exists(md5_hash) is True
@pytest.mark.asyncio
async def test_exists_false(self):
"""Test exists returns False for missing data."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
fake_hash = "0" * 32
assert await vault.exists(fake_hash) is False
@pytest.mark.asyncio
async def test_get_missing_returns_none(self):
"""Test get returns None for missing data."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
fake_hash = "0" * 32
result = await vault.get(fake_hash)
assert result is None
class TestImageVaultDeduplication:
"""Test ImageVault deduplication behavior."""
def setup_method(self):
self.temp_dir = tempfile.mkdtemp()
self.vault_path = os.path.join(self.temp_dir, "test_vault")
def teardown_method(self):
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
@pytest.mark.asyncio
async def test_same_data_same_path(self):
"""Test that same data stored twice goes to same path."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
data = b"Duplicate test data"
md5_hash = hashlib.md5(data).hexdigest()
path1 = await vault.store(md5_hash, data, extension="jpg")
path2 = await vault.store(md5_hash, data, extension="jpg")
# Should be the same path
assert path1 == path2
@pytest.mark.asyncio
async def test_different_data_different_path(self):
"""Test that different data goes to different paths."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
data1 = b"First unique data"
data2 = b"Second unique data"
hash1 = hashlib.md5(data1).hexdigest()
hash2 = hashlib.md5(data2).hexdigest()
path1 = await vault.store(hash1, data1)
path2 = await vault.store(hash2, data2)
assert path1 != path2
class TestImageVaultDeletion:
"""Test ImageVault deletion operations."""
def setup_method(self):
self.temp_dir = tempfile.mkdtemp()
self.vault_path = os.path.join(self.temp_dir, "test_vault")
def teardown_method(self):
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
@pytest.mark.asyncio
async def test_delete_existing(self):
"""Test deleting existing file."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
data = b"Data to be deleted"
md5_hash = hashlib.md5(data).hexdigest()
await vault.store(md5_hash, data)
assert await vault.exists(md5_hash) is True
result = await vault.delete(md5_hash)
assert result is True
assert await vault.exists(md5_hash) is False
@pytest.mark.asyncio
async def test_delete_missing(self):
"""Test deleting non-existent file."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
fake_hash = "0" * 32
result = await vault.delete(fake_hash)
assert result is False
class TestImageVaultPath:
"""Test ImageVault path operations."""
def setup_method(self):
self.temp_dir = tempfile.mkdtemp()
self.vault_path = os.path.join(self.temp_dir, "test_vault")
def teardown_method(self):
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
@pytest.mark.asyncio
async def test_get_path_existing(self):
"""Test getting path for existing file."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
data = b"Test data for path"
md5_hash = hashlib.md5(data).hexdigest()
await vault.store(md5_hash, data, extension="bin")
path = await vault.get_path(md5_hash)
assert path is not None
assert path.exists()
assert md5_hash in str(path)
@pytest.mark.asyncio
async def test_get_path_missing(self):
"""Test getting path for missing file."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
fake_hash = "0" * 32
path = await vault.get_path(fake_hash)
assert path is None
@pytest.mark.asyncio
async def test_path_uses_hash_prefix(self):
"""Test that path uses first 2 chars as subdirectory."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
data = b"Test data for subdir check"
md5_hash = hashlib.md5(data).hexdigest()
prefix = md5_hash[:2]
path = await vault.store(md5_hash, data)
# Path should contain the prefix subdirectory
assert f"/{prefix}/" in path or f"\\{prefix}\\" in path
class TestImageVaultStats:
"""Test ImageVault statistics."""
def setup_method(self):
self.temp_dir = tempfile.mkdtemp()
self.vault_path = os.path.join(self.temp_dir, "test_vault")
def teardown_method(self):
if os.path.exists(self.temp_dir):
shutil.rmtree(self.temp_dir)
@pytest.mark.asyncio
async def test_stats_empty_vault(self):
"""Test stats on empty vault."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
stats = await vault.stats()
assert stats['count'] == 0
assert stats['total_size_bytes'] == 0
@pytest.mark.asyncio
async def test_stats_with_files(self):
"""Test stats with files in vault."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
# Store some files
for i in range(3):
data = f"Test data {i}".encode()
md5_hash = hashlib.md5(data).hexdigest()
await vault.store(md5_hash, data)
stats = await vault.stats()
assert stats['count'] == 3
assert stats['total_size_bytes'] > 0
@pytest.mark.asyncio
async def test_stats_backend_type(self):
"""Test stats reports correct backend."""
vault = ImageVault(vault_path=self.vault_path)
await vault.init()
stats = await vault.stats()
# Should be 'directory' when filevault is not available
assert 'backend' in stats
if __name__ == '__main__':
pytest.main([__file__, '-v'])