# Fenrir Testing Guide

Complete guide to running and writing tests for the Fenrir screen reader.

## Quick Start

### 1. Install Test Dependencies

```bash
# Install test requirements
pip install -r tests/requirements.txt

# Or install individually
pip install pytest pytest-cov pytest-mock pytest-timeout
```

### 2. Run Tests

```bash
# Run all tests (unit + integration)
pytest tests/

# Run only unit tests (fastest)
pytest tests/unit/ -v

# Run only integration tests
pytest tests/integration/ -v

# Run with coverage report
pytest tests/ --cov=src/fenrirscreenreader --cov-report=html
# Then open htmlcov/index.html in a browser

# Run specific test file
pytest tests/unit/test_settings_validation.py -v

# Run specific test class
pytest tests/unit/test_settings_validation.py::TestSpeechSettingsValidation -v

# Run specific test
pytest tests/unit/test_settings_validation.py::TestSpeechSettingsValidation::test_speech_rate_valid_range -v
```

### 3. Useful Test Options

```bash
# Stop on first failure
pytest tests/ -x

# Show test output (print statements, logging)
pytest tests/ -s

# Run tests in parallel (faster, requires: pip install pytest-xdist)
pytest tests/ -n auto

# Show slowest 10 tests
pytest tests/ --durations=10

# Run only tests matching a keyword
pytest tests/ -k "remote"

# Run tests with specific markers
pytest tests/ -m unit          # Only unit tests
pytest tests/ -m integration   # Only integration tests
pytest tests/ -m "not slow"    # Skip slow tests
```

## Test Structure

```
tests/
├── README.md                       # Test overview and strategy
├── TESTING_GUIDE.md               # This file - detailed usage guide
├── requirements.txt               # Test dependencies
├── conftest.py                    # Shared fixtures and pytest config
├── unit/                          # Unit tests (fast, isolated)
│   ├── __init__.py
│   ├── test_settings_validation.py  # Settings validation tests
│   ├── test_cursor_utils.py         # Cursor calculation tests
│   └── test_text_utils.py           # Text processing tests
├── integration/                   # Integration tests (require mocking)
│   ├── __init__.py
│   ├── test_remote_control.py       # Remote control functionality
│   ├── test_command_manager.py      # Command loading/execution
│   └── test_event_manager.py        # Event queue processing
└── drivers/                       # Driver tests (require root)
    ├── __init__.py
    ├── test_vcsa_driver.py          # TTY screen reading
    └── test_evdev_driver.py         # Keyboard input capture
```

## Writing New Tests

### Unit Test Example

```python
"""tests/unit/test_my_feature.py"""
import pytest

@pytest.mark.unit
def test_speech_rate_calculation():
    """Test that speech rate is calculated correctly."""
    rate = calculate_speech_rate(0.5)
    assert 0.0 <= rate <= 1.0
    assert rate == 0.5
```

### Integration Test Example

```python
"""tests/integration/test_my_integration.py"""
import pytest

@pytest.mark.integration
def test_remote_command_execution(mock_environment):
    """Test remote command execution flow."""
    manager = RemoteManager()
    manager.initialize(mock_environment)

    result = manager.handle_command_execution_with_response("say test")

    assert result["success"] is True
    mock_environment["runtime"]["OutputManager"].speak_text.assert_called_once()
```

### Using Fixtures

Common fixtures are defined in `conftest.py`:

```python
def test_with_mock_environment(mock_environment):
    """Use the shared mock environment fixture."""
    # mock_environment provides mocked runtime managers
    assert "runtime" in mock_environment
    assert "DebugManager" in mock_environment["runtime"]

def test_with_temp_config(temp_config_file):
    """Use a temporary config file."""
    # temp_config_file is a Path object to a valid test config
    assert temp_config_file.exists()
    content = temp_config_file.read_text()
    assert "[speech]" in content
```

## Test Markers

Tests can be marked to categorize them:

```python
@pytest.mark.unit           # Fast, isolated unit test
@pytest.mark.integration    # Integration test with mocking
@pytest.mark.driver         # Requires root access (skipped by default)
@pytest.mark.slow           # Takes > 1 second
@pytest.mark.remote         # Tests remote control functionality
@pytest.mark.settings       # Tests settings/configuration
@pytest.mark.commands       # Tests command system
@pytest.mark.vmenu          # Tests VMenu system
```

Run tests by marker:
```bash
pytest tests/ -m unit              # Only unit tests
pytest tests/ -m "unit or integration"  # Unit and integration
pytest tests/ -m "not slow"        # Skip slow tests
```

## Code Coverage

### View Coverage Report

```bash
# Generate HTML coverage report
pytest tests/ --cov=src/fenrirscreenreader --cov-report=html

# Open report in browser
firefox htmlcov/index.html    # Or your preferred browser

# Terminal coverage report
pytest tests/ --cov=src/fenrirscreenreader --cov-report=term-missing
```

### Coverage Goals

- **Unit Tests**: 80%+ coverage on utility functions and validation logic
- **Integration Tests**: 60%+ coverage on core managers
- **Overall**: 70%+ coverage on non-driver code

Driver code is excluded from coverage as it requires hardware interaction.

## Testing Best Practices

### 1. Test One Thing

```python
# Good - tests one specific behavior
def test_speech_rate_rejects_negative():
    with pytest.raises(ValueError):
        validate_rate(-1.0)

# Bad - tests multiple unrelated things
def test_speech_settings():
    validate_rate(0.5)      # Rate validation
    validate_pitch(1.0)     # Pitch validation
    validate_volume(0.8)    # Volume validation
```

### 2. Use Descriptive Names

```python
# Good - clear what's being tested
def test_speech_rate_rejects_values_above_three():
    ...

# Bad - unclear purpose
def test_rate():
    ...
```

### 3. Arrange-Act-Assert Pattern

```python
def test_remote_command_parsing():
    # Arrange - set up test data
    manager = RemoteManager()
    command = "say Hello World"

    # Act - execute the code being tested
    result = manager.parse_command(command)

    # Assert - verify the result
    assert result["action"] == "say"
    assert result["text"] == "Hello World"
```

### 4. Mock External Dependencies

```python
def test_clipboard_export(mock_environment, tmp_path):
    """Test clipboard export without real file operations."""
    # Use mock environment instead of real Fenrir runtime
    manager = RemoteManager()
    manager.initialize(mock_environment)

    # Use temporary path instead of /tmp
    clipboard_path = tmp_path / "clipboard"
    mock_environment["runtime"]["SettingsManager"].get_setting = Mock(
        return_value=str(clipboard_path)
    )

    manager.export_clipboard()

    assert clipboard_path.exists()
```

### 5. Test Error Paths

```python
def test_remote_command_handles_invalid_input():
    """Test that invalid commands are handled gracefully."""
    manager = RemoteManager()

    # Test with various invalid inputs
    result1 = manager.handle_command_execution_with_response("")
    result2 = manager.handle_command_execution_with_response("invalid")
    result3 = manager.handle_command_execution_with_response("command unknown")

    # All should return error results, not crash
    assert all(not r["success"] for r in [result1, result2, result3])
```

## Debugging Tests

### Run with More Verbosity

```bash
# Show test names and outcomes
pytest tests/ -v

# Show test names, outcomes, and print statements
pytest tests/ -v -s

# Show local variables on failure
pytest tests/ --showlocals

# Show full diff on assertion failures
pytest tests/ -vv
```

### Use pytest.set_trace() for Debugging

```python
def test_complex_logic():
    result = complex_function()
    pytest.set_trace()  # Drop into debugger here
    assert result == expected
```

### Run Single Test Repeatedly

```bash
# Useful for debugging flaky tests
pytest tests/unit/test_my_test.py::test_specific_test --count=100
```

## Continuous Integration

### GitHub Actions Example

```yaml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: '3.9'
      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          pip install -r tests/requirements.txt
      - name: Run tests
        run: pytest tests/ --cov=src/fenrirscreenreader --cov-report=xml
      - name: Upload coverage
        uses: codecov/codecov-action@v3
```

## Common Issues

### ImportError: No module named 'fenrirscreenreader'

**Solution**: Make sure you're running pytest from the project root, or set PYTHONPATH:
```bash
export PYTHONPATH="${PYTHONPATH}:$(pwd)/src"
pytest tests/
```

### Tests hang or timeout

**Solution**: Use the timeout decorator or pytest-timeout:
```bash
pytest tests/ --timeout=30  # Global 30s timeout
```

Or mark specific tests:
```python
@pytest.mark.timeout(5)
def test_that_might_hang():
    ...
```

### Mocks not working as expected

**Solution**: Check that you're patching the right location:
```python
# Good - patch where it's used
@patch('fenrirscreenreader.core.remoteManager.OutputManager')

# Bad - patch where it's defined
@patch('fenrirscreenreader.core.outputManager.OutputManager')
```

## Advanced Topics

### Parametrized Tests

Test multiple inputs with one test:

```python
@pytest.mark.parametrize("rate,expected", [
    (0.0, True),
    (1.5, True),
    (3.0, True),
    (-1.0, False),
    (10.0, False),
])
def test_rate_validation(rate, expected):
    try:
        validate_rate(rate)
        assert expected is True
    except ValueError:
        assert expected is False
```

### Test Fixtures with Cleanup

```python
@pytest.fixture
def temp_fenrir_instance():
    """Start a test Fenrir instance."""
    fenrir = FenrirTestInstance()
    fenrir.start()

    yield fenrir  # Test runs here

    # Cleanup after test
    fenrir.stop()
    fenrir.cleanup()
```

### Testing Async Code

```python
@pytest.mark.asyncio
async def test_async_speech():
    result = await async_speak("test")
    assert result.success
```

## Getting Help

- **Pytest Documentation**: https://docs.pytest.org/
- **Fenrir Issues**: https://github.com/chrys87/fenrir/issues
- **Test Coverage**: Run with `--cov-report=html` and inspect `htmlcov/index.html`

## Contributing Tests

When contributing tests:

1. **Follow naming conventions**: `test_*.py` for files, `test_*` for functions
2. **Add docstrings**: Explain what each test verifies
3. **Use appropriate markers**: `@pytest.mark.unit`, `@pytest.mark.integration`, etc.
4. **Keep tests fast**: Unit tests should complete in <100ms
5. **Test edge cases**: Empty strings, None, negative numbers, etc.
6. **Update this guide**: If you add new test patterns or fixtures

Happy testing! 🧪
