Skip to content

Testing

This document provides an overview of the test suite for Arandu.

The test suite follows the project structure, with tests mirroring the source code organization:

tests/
├── conftest.py # Shared pytest fixtures
├── test_config.py # Configuration module tests
├── test_schemas.py # Pydantic schema tests
└── core/
├── test_hardware.py # Hardware detection tests
├── test_llm_client.py # LLM client tests
├── test_media.py # Media file utility tests
├── test_checkpoint.py # Checkpoint management tests
└── test_io.py # File I/O tests
Terminal window
pytest tests/
Terminal window
pytest tests/ -v
Terminal window
pytest tests/ --cov=arandu --cov-report=term
Terminal window
pytest tests/core/test_hardware.py -v
Terminal window
pytest tests/test_config.py::TestQAConfig -v
Terminal window
pytest tests/core/test_llm_client.py::TestLLMClient::test_generate_basic -v

Current coverage: ~80-85% (1105+/1379 statements)

  • core/hardware.py - Device detection and quantization
  • core/llm_client.py - Unified LLM client
  • core/checkpoint.py - Batch processing checkpoints
  • core/io.py - File I/O operations
  • utils/console.py - Console instance management
  • __init__.py - Package initialization
  • config.py - 95% (Configuration classes)
  • schemas.py - 98% (Pydantic data models)
  • utils/logger.py - 90% (Logger setup and utilities)
  • utils/ui.py - 85% (UI components and progress bars)
  • core/media.py - 75% (Media file utilities)
  • core/engine.py - 70% (Whisper ASR engine)
  • core/drive.py - 65% (Google Drive integration)
  • core/batch.py - 45% (Batch processing)
  • main.py - 30% (CLI entry points)

Tests for configuration classes that load from environment variables:

  • Default initialization
  • Environment variable overrides
  • Field validation (boundaries, constraints)
  • Invalid input handling

Example:

def test_questions_per_document_boundary_max() -> None:
"""Test maximum boundary for questions_per_document."""
config = QAConfig(questions_per_document=50)
assert config.questions_per_document == 50

Tests for Pydantic models that validate data structures:

  • Valid/invalid initialization
  • Field validators
  • Computed fields
  • Save/load round-trips

Example:

def test_start_time_greater_than_end_time() -> None:
"""Test validation error when start_time >= end_time."""
with pytest.raises(ValidationError) as exc_info:
QAPair(
question="Test?",
answer="Answer",
context="Context",
question_type="temporal",
confidence=0.9,
start_time=5.0,
end_time=3.0,
)
assert "start_time must be less than end_time" in str(exc_info.value)

Tests for device detection and configuration:

  • CUDA, MPS, CPU device selection
  • Quantization configuration
  • Error handling for unsupported architectures

Mocking Example:

from pytest_mock import MockerFixture
def test_cuda_available_modern_architecture(mocker: MockerFixture) -> None:
"""Test CUDA device selection with modern architecture (sm_70+)."""
mock_cuda = mocker.patch("torch.cuda")
mock_cuda.is_available.return_value = True
mock_cuda.get_device_capability.return_value = (7, 5) # sm_75
hw_config = get_device_and_dtype(force_cpu=False)
assert hw_config.device == "cuda:0"
assert hw_config.dtype == torch.float16

Tests for the unified LLM client supporting OpenAI, Ollama, and custom providers:

  • Provider initialization
  • API availability checks
  • Text generation with retry logic
  • Error handling

Mocking Example:

def test_generate_retry_on_failure(mocker: MockerFixture) -> None:
"""Test that generate retries on failure (tenacity decorator)."""
mock_openai = mocker.patch("arandu.core.llm_client.OpenAI")
mock_client = Mock()
# First two calls fail, third succeeds
mock_response = Mock()
mock_response.choices = [Mock()]
mock_response.choices[0].message.content = "Success"
mock_client.chat.completions.create.side_effect = [
Exception("API Error 1"),
Exception("API Error 2"),
mock_response,
]
client = LLMClient(LLMProvider.OLLAMA, "llama3.1:8b")
response = client.generate("Test prompt")
assert response == "Success"
assert mock_client.chat.completions.create.call_count == 3

Tests for media file processing utilities:

  • Audio stream detection
  • Duration extraction using ffprobe
  • Custom exceptions
  • Error handling for corrupted files

Mocking Example:

def test_has_audio_stream_success(mocker: MockerFixture) -> None:
"""Test detecting audio stream in media file."""
mock_result = Mock()
mock_result.stdout = json.dumps({"streams": [{"codec_type": "audio"}]})
mock_run = mocker.patch("subprocess.run", return_value=mock_result)
result = has_audio_stream("test.mp4")
assert result is True
assert "ffprobe" in mock_run.call_args[0][0]

Tests for batch processing checkpoint management:

  • State persistence
  • Progress tracking
  • Corrupted checkpoint recovery
  • File completion tracking

Example:

def test_mark_completed_removes_from_failed(tmp_path: Path) -> None:
"""Test that marking as completed removes from failed list."""
manager = CheckpointManager(tmp_path / "checkpoint.json")
manager.mark_failed("file1", "Some error")
manager.mark_completed("file1")
assert "file1" in manager.state.completed_files
assert "file1" not in manager.state.failed_files

Tests for file operations and temporary file management:

  • Temporary directory creation
  • Temporary file creation
  • EnrichedRecord saving
  • MIME type detection
  • Cleanup operations

Example:

def test_cleanup_temp_files_ignores_other_files(tmp_path: Path) -> None:
"""Test that cleanup only removes arandu_ files."""
(tmp_path / "arandu_file1.txt").touch()
(tmp_path / "other_file.txt").touch()
success, failure = cleanup_temp_files(str(tmp_path))
assert success == 1
assert (tmp_path / "other_file.txt").exists()

Always mock external services to avoid real API calls:

  • Google Drive API
  • OpenAI/Ollama APIs
  • System commands (ffprobe, ffmpeg)
  • PyTorch CUDA functions

Don’t just test the happy path. Test error conditions:

  • Invalid inputs
  • Network failures
  • Permission errors
  • Corrupted data

Test names should describe what they test:

def test_questions_per_document_above_max() -> None:
"""Test validation error when questions_per_document is above maximum."""

Use pytest fixtures for common setup:

@pytest.fixture
def mock_torch_cuda(mocker: MockerFixture) -> MagicMock:
"""Mock torch.cuda module for hardware detection tests."""
mock_cuda = mocker.patch("torch.cuda")
mock_cuda.is_available.return_value = False
return mock_cuda

For validated fields, test:

  • Minimum valid value
  • Maximum valid value
  • Below minimum (should fail)
  • Above maximum (should fail)

Provides a temporary directory unique to each test function:

def test_example(tmp_path: Path) -> None:
file = tmp_path / "test.txt"
file.write_text("content")

Provides mocking functionality:

def test_example(mocker: MockerFixture) -> None:
mock_func = mocker.patch("module.function")
mock_func.return_value = "mocked"

Captures log messages:

def test_example(caplog: pytest.LogCaptureFixture) -> None:
function_that_logs()
assert "expected message" in caplog.text

Tests are run automatically on:

  • Every commit
  • Every pull request
  • Before merging to main

Pre-commit checklist:

Terminal window
# Format code
ruff format src/ tests/
# Check linting
ruff check --fix src/ tests/
# Run tests
pytest tests/
# Check coverage
pytest tests/ --cov=arandu

When adding a new test:

  1. Place it in the correct location: Mirror the source structure
  2. Name it descriptively: Use test_ prefix and describe what it tests
  3. Add docstring: Explain what the test validates
  4. Mock external dependencies: No real API calls or file system operations (except in tmp_path)
  5. Test error paths: Not just happy paths
  6. Run locally before committing: Ensure all tests pass

Example template:

def test_function_name_scenario(mocker: MockerFixture) -> None:
"""Test that function_name handles scenario correctly."""
# Arrange: Set up test data and mocks
mock_dependency = mocker.patch("module.dependency")
mock_dependency.return_value = "expected"
# Act: Execute the function
result = function_name(param="value")
# Assert: Verify expectations
assert result == "expected"
mock_dependency.assert_called_once()
  1. Ensure all dependencies are installed: pip install pytest pytest-cov pytest-mock
  2. Check Python version: Requires Python 3.13+
  3. Clear pytest cache: pytest --cache-clear
  1. Delete .coverage file
  2. Run with --cov-report=term to see live results
  3. Ensure you’re testing the right modules
  1. Ensure src/ is in PYTHONPATH (configured in pyproject.toml)
  2. Check that __init__.py files exist in test directories
  3. Use absolute imports: from arandu.module import function