Research-Stack/5-Applications/nodupe/tests/conftest.py
2026-05-05 21:15:26 -05:00

448 lines
14 KiB
Python

"""Test fixtures and configuration for NoDupeLabs test suite.
This module provides comprehensive test fixtures for database operations,
file system operations, tool system mocking, and various utility functions
for testing the NoDupeLabs duplicate detection system.
"""
# NoDupeLabs Test Fixtures and Configuration
# Comprehensive test fixtures for database operations, file system operations, tool system mocking, etc.
import os
import tempfile
import shutil
from pathlib import Path
from typing import Generator, Dict, Any, Optional, Callable
import pytest # type: ignore
from unittest.mock import MagicMock, patch
import nodupe.core.container as container_module
from nodupe.core.container import ServiceContainer
import nodupe.core.config as config
from nodupe.core.tool_system.registry import ToolRegistry
from nodupe.core.tool_system.loader import ToolLoader
from nodupe.tools.databases.connection import DatabaseConnection
## Mock Database Connection for Testing
class MockDatabaseConnection:
"""Mock database connection for testing purposes."""
def __init__(self, db_path: str = ":memory:"):
"""Initialize the mock database connection.
Args:
db_path: Path to the database file. Defaults to in-memory database.
"""
self.db_path = db_path
self.closed = False
def get_connection(self):
"""Return a mock connection."""
import sqlite3
return sqlite3.connect(self.db_path)
def close(self):
"""Close the connection."""
self.closed = True
@pytest.fixture(scope="function")
def temp_db_connection() -> Generator[MockDatabaseConnection, None, None]:
"""Create a temporary in-memory SQLite database connection for testing."""
conn = MockDatabaseConnection(":memory:")
try:
yield conn
finally:
conn.close()
@pytest.fixture(scope="function")
def temp_db_file() -> Generator[str, None, None]:
"""Create a temporary database file that gets cleaned up after tests."""
temp_file = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
temp_file.close()
try:
yield temp_file.name
finally:
if os.path.exists(temp_file.name):
os.unlink(temp_file.name)
## File System Fixtures
@pytest.fixture(scope="function")
def temp_dir() -> Generator[Path, None, None]:
"""Create a temporary directory that gets cleaned up after tests."""
temp_dir = Path(tempfile.mkdtemp())
try:
yield temp_dir
finally:
if temp_dir.exists():
shutil.rmtree(temp_dir)
@pytest.fixture(scope="function")
def temp_file(temp_dir: Path) -> Generator[Path, None, None]:
"""Create a temporary file in the temp directory."""
temp_file = temp_dir / "test_file.txt"
temp_file.write_text("This is a test file content.")
yield temp_file
@pytest.fixture(scope="function")
def sample_files(temp_dir: Path) -> Generator[Dict[str, Path], None, None]:
"""Create a set of sample files for duplicate detection testing."""
files = {
"small.txt": temp_dir / "small.txt",
"medium.txt": temp_dir / "medium.txt",
"large.txt": temp_dir / "large.txt",
"duplicate_small.txt": temp_dir / "duplicate_small.txt",
"binary.dat": temp_dir / "binary.dat"
}
# Create text files with different content
files["small.txt"].write_text("Small file content")
files["medium.txt"].write_text("Medium file content " * 100)
files["large.txt"].write_text("Large file content " * 1000)
files["duplicate_small.txt"].write_text("Small file content") # Duplicate of small.txt
files["binary.dat"].write_bytes(os.urandom(1024))
yield files
@pytest.fixture(scope="function")
def file_system_with_duplicates(temp_dir: Path) -> Generator[Dict[str, Path], None, None]:
"""Create a file system structure with duplicates for testing."""
# Create directory structure
dirs = {
"documents": temp_dir / "documents",
"images": temp_dir / "images",
"backups": temp_dir / "backups"
}
for dir_path in dirs.values():
dir_path.mkdir()
# Create duplicate files in different directories
original_content = "This is original content for duplicate testing."
files = {
"original": dirs["documents"] / "original.txt",
"duplicate1": dirs["images"] / "copy.txt",
"duplicate2": dirs["backups"] / "backup.txt",
"unique1": dirs["documents"] / "unique1.txt",
"unique2": dirs["images"] / "unique2.txt"
}
# Write content
files["original"].write_text(original_content)
files["duplicate1"].write_text(original_content) # Exact duplicate
files["duplicate2"].write_text(original_content) # Exact duplicate
files["unique1"].write_text("Unique content 1")
files["unique2"].write_text("Unique content 2")
yield files
## Configuration Fixtures
@pytest.fixture(scope="function")
def mock_config() -> Generator[Dict[str, Any], None, None]:
"""Provide a mock configuration dictionary for testing."""
yield {
"database": {
"path": ":memory:",
"timeout": 10.0,
"journal_mode": "WAL"
},
"scan": {
"min_file_size": "1KB",
"max_file_size": "10MB",
"default_extensions": ["txt", "pdf", "jpg"],
"exclude_dirs": [".git", ".venv"]
},
"performance": {
"max_workers": 4,
"batch_size": 100,
"chunk_size": "1MB"
},
"logging": {
"level": "DEBUG",
"file": "test.log",
"max_size": "5MB",
"backup_count": 3
}
}
@pytest.fixture(scope="function")
def temp_config_file(temp_dir: Path, mock_config: Dict[str, Any]) -> Generator[Path, None, None]:
"""Create a temporary configuration file."""
config_file = temp_dir / "config.toml"
import tomlkit as toml
with open(config_file, "w") as f:
toml.dump(mock_config, f)
yield config_file
@pytest.fixture(scope="function")
def loaded_config(temp_config_file: Path) -> Generator[config.ConfigManager, None, None]:
"""Load and provide a configuration object from the temp config file."""
cfg = config.ConfigManager(str(temp_config_file))
yield cfg
## Tool System Fixtures
@pytest.fixture(scope="function")
def reset_tool_registry() -> Generator[None, None, None]:
"""Reset ToolRegistry singleton between tests for test isolation.
This fixture ensures each test starts with a clean ToolRegistry state,
preventing test pollution from the singleton pattern.
"""
try:
yield
finally:
# Reset the singleton instance after each test
ToolRegistry._reset_instance()
@pytest.fixture(scope="function")
def mock_tool_registry(reset_tool_registry: None) -> Generator[ToolRegistry, None, None]:
"""Create a mock tool registry for testing.
Uses reset_tool_registry fixture to ensure test isolation.
"""
registry = ToolRegistry()
yield registry
@pytest.fixture(scope="function")
def mock_tool_loader(mock_tool_registry: ToolRegistry) -> Generator[ToolLoader, None, None]:
"""Create a mock tool loader for testing."""
loader = ToolLoader(mock_tool_registry)
yield loader
@pytest.fixture(scope="function")
def mock_tool() -> Generator[MagicMock, None, None]:
"""Create a mock tool object for testing."""
mock = MagicMock()
mock.name = "test_tool"
mock.version = "1.0.0"
mock.author = "Test Author"
mock.description = "A test tool for NoDupeLabs"
mock.initialize = MagicMock()
mock.execute = MagicMock()
mock.cleanup = MagicMock()
yield mock
@pytest.fixture(scope="function")
def registered_mock_tool(mock_tool_registry: ToolRegistry, mock_tool: MagicMock) -> Generator[MagicMock, None, None]:
"""Register a mock tool and yield it."""
mock_tool_registry.register_tool(mock_tool)
yield mock_tool
## Resource Management Fixtures
@pytest.fixture(scope="function")
def memory_mapped_file(temp_file: Path) -> Generator[Any, None, None]:
"""Create a memory-mapped file for testing."""
import mmap
with open(temp_file, "r+b") as f:
# Write some content first
f.write(b"This is test content for memory mapping")
f.flush()
# Create memory map
mmap_obj = mmap.mmap(f.fileno(), 0)
try:
yield mmap_obj
finally:
mmap_obj.close()
@pytest.fixture(scope="function")
def resource_pool() -> Generator[Any, None, None]:
"""Create a test resource pool.
Note: ResourcePool module not available, using MagicMock.
"""
from unittest.mock import MagicMock
pool = MagicMock()
pool.max_size = 5
try:
yield pool
finally:
pass # Mock doesn't need cleanup
## Container and Dependency Injection Fixtures
@pytest.fixture(scope="function")
def test_container() -> Generator[ServiceContainer, None, None]:
"""Create a test container with basic services."""
cont = ServiceContainer()
# Register basic services
cont.register_service("config", mock_config())
cont.register_service("db_connection", DatabaseConnection(":memory:"))
cont.register_service("tool_registry", ToolRegistry())
yield cont
@pytest.fixture(scope="function")
def container_with_mocks(test_container: ServiceContainer) -> Generator[ServiceContainer, None, None]:
"""Create a container with mock services for testing."""
# Add mock services
test_container.register_service("mock_service", MagicMock())
test_container.register_service("file_system", MagicMock())
test_container.register_service("logger", MagicMock())
yield test_container
## Error and Exception Fixtures
@pytest.fixture(scope="function")
def mock_error_condition() -> Generator[Callable[[Exception], None], None, None]:
"""Fixture to simulate error conditions.
Returns:
A function that raises a specified error type with a given message.
"""
def raise_error(error_type: Exception = Exception, message: str = "Test error") -> Callable:
"""Create a function that raises a specified error.
Args:
error_type: The type of exception to raise.
message: The error message.
Returns:
A function that when called raises the specified error.
"""
def _inner():
"""Helper function that raises the specified error."""
raise error_type(message)
return _inner
yield raise_error
@pytest.fixture(scope="function")
def mock_timeout() -> Generator[Callable[[float], None], None, None]:
"""Fixture to simulate timeout conditions.
Returns:
A function that sleeps longer than the specified timeout duration.
"""
def timeout_after(seconds: float) -> Callable:
"""Create a function that sleeps longer than the specified timeout.
Args:
seconds: The timeout duration in seconds.
Returns:
A function that when called sleeps longer than the timeout.
"""
import time
def _inner():
"""Helper function that raises the specified error."""
time.sleep(seconds + 0.1)
return _inner
yield timeout_after
## Performance Testing Fixtures
@pytest.fixture(scope="function")
def performance_monitor() -> Generator[Callable, None, None]:
"""Fixture for monitoring test performance."""
import time
def monitor(func: Callable, *args, **kwargs) -> Dict[str, float]:
"""Monitor function execution time and memory usage.
Args:
func: The function to execute and monitor.
*args: Positional arguments for the function.
**kwargs: Keyword arguments for the function.
Returns:
Dictionary containing execution_time, memory_increase, and result.
"""
start_time = time.time()
start_mem = get_memory_usage()
result = func(*args, **kwargs)
end_time = time.time()
end_mem = get_memory_usage()
return {
"execution_time": end_time - start_time,
"memory_increase": end_mem - start_mem,
"result": result
}
def get_memory_usage() -> float:
"""Get the current memory usage of the process.
Returns:
Memory usage in megabytes.
"""
import psutil
import os
process = psutil.Process(os.getpid())
return process.memory_info().rss / 1024 / 1024 # MB
yield monitor
## Helper Functions
def create_test_file(path: Path, size: int = 1024, content: str = None) -> Path:
"""Helper function to create test files."""
if content is None:
content = "A" * size
path.write_text(content)
return path
def create_file_structure(base_dir: Path, structure: Dict[str, Any]) -> Dict[str, Path]:
"""Helper function to create complex file structures."""
created_files = {}
for name, item in structure.items():
if isinstance(item, dict):
# It's a directory
dir_path = base_dir / name
dir_path.mkdir(exist_ok=True)
created_files.update(create_file_structure(dir_path, item))
else:
# It's a file
file_path = base_dir / name
if isinstance(item, str):
file_path.write_text(item)
else:
file_path.write_bytes(item)
created_files[name] = file_path
return created_files
## Test Configuration Hooks
def pytest_configure(config):
"""Pytest configuration hook."""
# Add custom markers
config.addinivalue_line(
"markers",
"performance: mark test as performance test"
)
config.addinivalue_line(
"markers",
"stress: mark test as stress test"
)
def pytest_collection_modifyitems(items):
"""Modify test collection order."""
# Run performance tests last
performance_tests = [item for item in items if "performance" in item.keywords]
other_tests = [item for item in items if "performance" not in item.keywords]
items[:] = other_tests + performance_tests
def pytest_runtest_setup(item):
"""Test setup hook."""
# Add test timing information
if "performance" in item.keywords:
item.user_properties.append(("test_type", "performance"))
elif "stress" in item.keywords:
item.user_properties.append(("test_type", "stress"))
else:
item.user_properties.append(("test_type", "standard"))