# Testing Frameworks & Patterns

**Project**: opendeviationbar-py | **Date**: 2026-03-19 | **Scope**: Rust + Python test infrastructure

---

## Test Execution

### Rust Testing

**Framework**: `cargo nextest` (parallelized test runner)

```bash
# Run all tests (excludes PyO3 crate)
cargo nextest run --workspace --exclude opendeviationbar-py

# Test specific crate
cargo nextest run -p opendeviationbar-core

# Test with feature flags
cargo nextest run -p opendeviationbar-core --features test-utils

# Watch mode
cargo nextest run --watch
```

**Configuration**: `.cargo/nextest.toml` (if present) or default parallelization

**Location**: `crates/opendeviationbar-core/tests/`, `crates/opendeviationbar-hurst/tests/`

---

### Python Testing

**Framework**: `pytest` (with plugins)

```bash
# Run all Python tests
mise run test-py
pytest tests/

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

# Run specific test class/function
pytest tests/test_rust_bindings.py::TestProcessor::test_invalid_threshold -v

# With coverage
pytest --cov=python/opendeviationbar tests/

# Markers
pytest -m benchmark          # Only benchmarks
pytest -m "not benchmark"    # Skip benchmarks
```

**Configuration**: `pyproject.toml`

```toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "--tb=short -q"
asyncio_mode = "auto"
markers = [
    "benchmark: marks tests as performance benchmarks",
    "asyncio: marks tests as async",
    "bootstrap: marks tests as transitional bootstrap",
    "contract: marks tests as alpha-forge contract tests",
]
```

**Location**: `pyproject.toml` lines 86-98 | Tests in `tests/`

---

## Test Directory Structure

### Rust Tests

```
crates/opendeviationbar-core/tests/
├── processor_tests.rs              # 1706 lines: core algorithm (extracted from processor.rs)
├── microstructure_features_test.rs # 136K lines: 10 intra-bar features + edge cases
├── interbar_math_tests.rs          # 71K lines: Tier 2/3 features (lookback window)
├── cross_boundary_validation.rs    # 8797 lines: Day boundary, year-end edge cases
├── cross_year_boundary_test.rs     # 22K lines: Cross-year bar integrity
├── cross_date_real_data_validation.rs # Real trade data validation
├── incomplete_bar_continuation_proof.rs # Orphan bar semantics
├── intrabar_dual_pass_test.rs      # Dual-pass algorithm validation
├── tier3_features_test.rs          # Advanced features (Hurst, entropy, permutation)
├── types_tests.rs                  # Type system invariants (FixedPoint, timestamp)
├── test_agg_trade_id_tracking.rs   # Trade ID continuity (Stathera invariant)
└── arrow_export_tests.rs           # Arrow zero-copy export path

crates/opendeviationbar-hurst/tests/
├── accuracy_validation.rs          # 311 lines: Hurst estimator accuracy
├── real_data_audit.rs              # 879 lines: 7 estimators on real data
└── surrogate_and_aucroc.rs         # 218 lines: Statistical validation

crates/*/tests/
├── (Similar structure in other crates)
```

**Total**: ~450K lines of Rust tests across 12 test files

**Key observation**: Tests are MASSIVE because they validate exact numeric outputs against oracle ClickHouse queries (bit-exact proof).

---

### Python Tests

```
tests/
├── test_rust_bindings.py                # PyO3 binding validation
├── test_rust_bindings_advanced.py       # Checkpoint, threading, streaming
├── test_day_mode_invariant.py           # Cross-midnight bar rejection (Issue #264)
├── test_memory_guards.py                # Memory watermark, OOM protection
├── test_unified_config.py               # pydantic-settings configuration
├── test_exchange_sessions.py            # DST handling, forex session detection
├── test_crypto_minimum_threshold.py     # Symbol registry validation
├── test_volume_overflow.py              # Negative volume detection (Issue #88)
├── test_plugins.py                      # FeatureProvider entry-point discovery
├── test_kintsugi_repair.py              # Gap reconciliation algorithm
├── test_parquet_corruption.py           # Tier 1 cache resilience
├── test_real_data.py                    # Integration with real Binance data
├── test_rectification.py                # Cross-midnight bar splitting
├── test_compat_bootstrap.py             # Version compatibility
├── test_sidecar_watchdog.py             # Streaming sidecar resilience
├── test_checkpoint_oracle.py            # Checkpoint recovery semantics
├── test_phi_accrual.py                  # Phi accrual detector (gap indicator)
├── test_cache_schema_evolution.py       # ClickHouse schema migration
├── test_data_contracts.py               # Arrow schema validation
├── test_e2e_optimized.py                # End-to-end optimization
├── test_get_n_open_deviation_bars.py    # Count-bounded API
└── (additional test files for new features)
```

**Total**: ~25 test files, ~5K-10K lines aggregate

**Key focus**: API contracts, integration with ClickHouse, data invariants, cross-midnight handling

---

## Test Patterns

### Rust: Unit Tests (Inline)

Tests live in the same file as code, in `#[cfg(test)]` modules:

```rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_single_bar_no_breach() {
        let mut processor = OpenDeviationBarProcessor::new(250).unwrap();
        let trades = scenarios::no_breach_sequence(250);
        let bars = processor.process_agg_trade_records(&trades).unwrap();
        assert_eq!(bars.len(), 0);
    }
}
```

**Location**: Within each `.rs` file (e.g., `processor.rs:2600+`)

---

### Rust: Integration Tests (External)

Tests in separate files under `tests/` directory:

```rust
// File: crates/opendeviationbar-core/tests/processor_tests.rs

use opendeviationbar_core::processor::OpenDeviationBarProcessor;
use opendeviationbar_core::test_utils::scenarios;

#[test]
fn test_exact_breach_upward() {
    let mut processor = OpenDeviationBarProcessor::new(250).unwrap();
    let trades = scenarios::exact_breach_upward(250);

    let bars = processor.process_agg_trade_records(&trades).unwrap();
    assert_eq!(bars.len(), 1);

    let bar = &bars[0];
    assert_eq!(bar.open.to_string(), "50000.00000000");
    assert_eq!(bar.close.to_string(), "50125.00000000");
}
```

**Features**:

- Uses public API (forces API stability)
- Can use `#[ignore]` for slow tests
- Runs in release mode by default

**Location**: `crates/*/tests/*.rs`

---

### Python: Pytest Classes

Organized by test class for grouping:

```python
# File: tests/test_day_mode_invariant.py

class TestSplitTradesByDay:
    """Test day boundary splitting logic."""

    def test_empty_list(self):
        assert split_trades_by_day([]) == []

    def test_single_day(self):
        trades = [{"timestamp": 1704067200000 + i * 1000} for i in range(10)]
        groups = split_trades_by_day(trades)
        assert len(groups) == 1
        assert len(groups[0]) == 10

    def test_two_days(self):
        day1 = [{"timestamp": 1704067200000 + i * 1000} for i in range(5)]
        day2 = [{"timestamp": 1704153600000 + i * 1000} for i in range(3)]
        groups = split_trades_by_day(day1 + day2)
        assert len(groups) == 2


class TestValidateBarDayMode:
    """Test day-mode invariant enforcement."""

    def test_valid_bar(self):
        bar = {"open_time_ms": 1704067200000, "close_time_ms": 1704110400000}
        assert validate_bar_day_mode(bar) is True

    def test_cross_midnight_strict(self):
        bar = {"open_time_ms": 1704150000000, "close_time_ms": 1704160000000}
        with pytest.raises(AssertionError, match="Cross-midnight"):
            validate_bar_day_mode(bar, strict=True)
```

**Location**: `tests/test_*.py`

---

### Fixtures

#### Rust (Test Utilities)

```rust
// File: crates/opendeviationbar-core/src/test_utils/scenarios.rs

pub mod scenarios {
    use crate::trade::AggTrade;
    use crate::fixed_point::FixedPoint;

    /// Create a no-breach scenario (trades stay within threshold).
    pub fn no_breach_sequence(threshold_dbps: i32) -> Vec<AggTrade> {
        vec![
            AggTrade {
                agg_trade_id: 1,
                price: FixedPoint::from_str("50000.0").unwrap(),
                volume: FixedPoint::from_str("1.0").unwrap(),
                timestamp: 1640995200000000,
                is_buyer_maker: false,
                ..Default::default()
            },
            // More trades within threshold...
        ]
    }

    /// Create exact-breach scenario (second trade triggers closure).
    pub fn exact_breach_upward(threshold_dbps: i32) -> Vec<AggTrade> {
        // Breach at: 50000 * (1 + 250/1_000_000) = 50125
        vec![
            /* ... */
        ]
    }
}
```

**Location**: `crates/opendeviationbar-core/src/test_utils/`

#### Python (pytest Fixtures)

```python
# File: tests/conftest.py (shared fixtures)

import pytest
from opendeviationbar import get_open_deviation_bars

@pytest.fixture
def btcusdt_cache():
    """Pre-populated BTCUSDT cache for testing."""
    # Populate or assume already exists
    return get_open_deviation_bars(
        "BTCUSDT",
        "2024-01-01",
        "2024-01-31",
        threshold_decimal_bps=250,
    )

@pytest.fixture
def clickhouse_client():
    """Connected ClickHouse client for tests."""
    import clickhouse_connect
    client = clickhouse_connect.get_client(host='localhost')
    yield client
    # Cleanup not needed for read-only tests
```

**Best practice**: Place shared fixtures in `conftest.py` at test root

---

## Test Categories

### 1. Unit Tests (Fast, <1s each)

**Purpose**: Single function/class validation

```python
# File: tests/test_rust_bindings.py

def test_processor_creation():
    """Test basic processor creation."""
    processor = PyOpenDeviationBarProcessor(threshold_decimal_bps=250)
    assert processor.threshold_decimal_bps == 250


def test_invalid_threshold_too_low():
    """Test that threshold validation rejects values that are too low."""
    with pytest.raises(ValueError, match="Invalid threshold"):
        PyOpenDeviationBarProcessor(threshold_decimal_bps=0)


def test_process_empty_trades():
    """Test processing empty trade list."""
    processor = PyOpenDeviationBarProcessor(threshold_decimal_bps=250)
    bars = processor.process_trades([])
    assert bars == []
```

**Run**: `pytest tests/test_rust_bindings.py -v` (0.5s)

---

### 2. Integration Tests (Medium, 1-30s)

**Purpose**: Multi-component workflows (API + cache + Rust)

```python
# File: tests/test_real_data.py

class TestRealBinanceData:
    """Integration: fetch real Binance data and validate bars."""

    def test_fetch_and_process_btcusdt(self):
        """Fetch BTCUSDT from Vision for known date."""
        df = get_open_deviation_bars(
            "BTCUSDT",
            "2024-01-15",
            "2024-01-16",
            threshold_decimal_bps=250,
            use_cache=False,  # Force fetch
        )
        assert len(df) > 0
        assert all(df["Open"] > 0)
        assert (df["High"] >= df["Low"]).all()

    def test_cache_write_and_read(self):
        """Write bars to cache, then read back."""
        symbol = "BTCUSDT"
        start, end = "2024-01-15", "2024-01-16"

        # Populate cache
        populate_cache_resumable(symbol, start, end, threshold_decimal_bps=250)

        # Read back
        df = get_open_deviation_bars(symbol, start, end, use_cache=True)
        assert len(df) > 0
```

**Run**: `pytest tests/test_real_data.py -v` (5-30s, depends on network)

---

### 3. Contract Tests (Validation, ~10-60s)

**Purpose**: Data invariant validation (cross-reference with oracle)

```python
# File: tests/test_data_contracts.py

class TestArrowSchemaContracts:
    """Arrow schema must match Rust definitions."""

    def test_open_deviation_bar_schema(self):
        """Validate Arrow schema matches Rust OpenDeviationBar type."""
        from opendeviationbar._core import (
            opendeviationbar_schema,
            opendeviationbar_vec_to_record_batch,
        )

        schema = opendeviationbar_schema()
        assert schema.field("open_time_ms").type == pa.int64()
        assert schema.field("close_time_ms").type == pa.int64()
        assert schema.field("open").type == pa.float64()
        assert schema.field("ofi").type == pa.float64()  # Microstructure
```

**Marker**: `@pytest.mark.contract` (permanent, alpha-forge)

---

### 4. Property-Based Tests (Deterministic)

**Framework**: `hypothesis` (property-based testing)

```python
# File: tests/test_properties.py (not present yet, but pattern)

from hypothesis import given, strategies as st

@given(
    threshold=st.integers(min_value=1, max_value=100_000),
    price=st.floats(min_value=0.01, max_value=1_000_000),
)
def test_threshold_always_positive(threshold, price):
    """Threshold should always be positive."""
    processor = PyOpenDeviationBarProcessor(threshold_decimal_bps=threshold)
    assert processor.threshold_decimal_bps > 0
```

**Libraries**: `hypothesis` + `hypothesis-polars` for DataFrame strategies

---

### 5. Performance Tests

**Framework**: `pytest-benchmark`

```python
# File: tests/test_performance.py

def test_benchmark_1m_trades(benchmark):
    """Benchmark processing 1M trades."""
    processor = PyOpenDeviationBarProcessor(threshold_decimal_bps=250)
    trades = generate_random_trades(1_000_000)

    result = benchmark(processor.process_trades, trades)
    assert len(result) > 0
```

**Run**: `pytest tests/ -m benchmark --benchmark-only`

**Markers**: `@pytest.mark.benchmark`

---

## Mock Strategy

### Python: unittest.mock

Minimal mocking — prefer integration tests over unit tests with heavy mocks.

```python
# File: tests/test_sidecar.py

from unittest.mock import MagicMock, patch

def test_sidecar_http_request_handling():
    """Test HTTP request parsing without full sidecar."""
    from opendeviationbar.sidecar import _SidecarHTTPHandler

    handler = _SidecarHTTPHandler(
        request=MagicMock(),
        client_address=("127.0.0.1", 12345),
        server=MagicMock(),
    )
    handler.path = "/health"
    handler._send_json = MagicMock()

    handler.do_GET()

    # Verify response
    handler._send_json.assert_called_once()
    args, kwargs = handler._send_json.call_args
    assert args[0] == 200  # Status code
```

**Avoid**: Mocking ClickHouse or Binance API for integration tests (use real data instead)

---

## Test Markers

**Configuration**: `pyproject.toml` lines 93-98

```toml
markers = [
    "benchmark: marks tests as performance benchmarks",
    "asyncio: marks tests as async (requires pytest-asyncio)",
    "bootstrap: marks tests as transitional bootstrap (archive after stable)",
    "contract: marks tests as alpha-forge contract tests (permanent)",
]
```

**Usage**:

```python
# Skip benchmarks on local development
@pytest.mark.benchmark
def test_performance_critical():
    pass

# Transient bootstrap (archive when stable)
@pytest.mark.bootstrap
def test_new_validation_layer():
    pass

# Permanent contract validation
@pytest.mark.contract
def test_arrow_schema_immutable():
    pass

# Async test (requires pytest-asyncio)
@pytest.mark.asyncio
async def test_streaming_async():
    pass
```

---

## Coverage & Reporting

**Tool**: `pytest-cov`

```bash
# Generate coverage report
pytest --cov=python/opendeviationbar tests/

# HTML report
pytest --cov=python/opendeviationbar --cov-report=html tests/
# Open htmlcov/index.html
```

**Configuration**: `pyproject.toml`

```toml
[tool.coverage.run]
source = ["python/opendeviationbar"]
omit = ["tests/*", "python/opendeviationbar/__init__.pyi"]

[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false
```

**Target**: >80% coverage on core modules

---

## CI/CD Setup

### GitHub Actions (Limited Use)

**Policy**: Local-first CI/CD. GitHub Actions reserved for:

- Semantic-release (versioning)
- CodeQL (security scanning)
- Dependabot (dependency updates)
- Deployment only (no testing)

**Location**: `.github/workflows/`

**Test runs**: Local via `mise run check-full` (see below)

---

### Local Quality Gates

**Command**: `mise run check-full` (runs all checks locally)

```bash
# Run full quality pipeline (formatter, linter, type checker, tests)
mise run check-full
```

**Tasks** (from `.mise/tasks/`):

```bash
# Individual checks
mise run fmt              # rustfmt + black
mise run lint             # ruff
mise run type-check       # mypy
mise run test             # cargo nextest + pytest
mise run deny             # cargo-deny (supply chain)
```

**Total runtime**: ~2-3 minutes local (parallelized)

---

## Benchmark Framework

**Tool**: `criterion` (Rust) + `pytest-benchmark` (Python)

### Rust Benchmarks

```bash
# Run all benchmarks
mise run bench:run

# Quick validation (reduced iterations)
mise run bench:quick

# Create baseline
mise run bench:baseline

# Compare with baseline
mise run bench:compare

# Validate performance targets (1M ticks < 100ms)
mise run bench:validate
```

**Configuration**: `.mise/tasks/bench.toml`

**Location**: `crates/opendeviationbar-core/benches/`

**Example**: `processor_bench.rs` (criterion benchmarks)

---

## Debugging Tests

### Rust

```bash
# Run with backtrace on panic
RUST_BACKTRACE=1 cargo nextest run -p opendeviationbar-core

# Show test output (not captured)
cargo nextest run -- --nocapture

# Single-threaded (easier to debug)
cargo nextest run -- --test-threads=1
```

### Python

```bash
# Show print statements
pytest -s tests/test_example.py

# Enter debugger on failure
pytest --pdb tests/test_example.py

# Verbose output + locals
pytest -vv --tb=long tests/test_example.py

# Drop to pdb on first failure
pytest -x --pdb tests/test_example.py
```

---

## Snapshot Testing

**Pattern**: Store expected outputs in fixture files, compare against current runs.

```python
# File: tests/test_snapshot_validation.py

import json

def test_microstructure_features_snapshot(tmp_path):
    """Validate microstructure features against snapshot."""
    bars = get_open_deviation_bars("BTCUSDT", "2024-01-01", "2024-01-02")

    # First run: create snapshot
    snapshot_path = tmp_path / "features_snapshot.json"
    output = bars.to_dict(orient="records")

    # Compare (or create if missing)
    if snapshot_path.exists():
        expected = json.loads(snapshot_path.read_text())
        assert output == expected, "Features changed"
    else:
        snapshot_path.write_text(json.dumps(output, indent=2))
```

**Tool**: `pytest-snapshot` or custom JSON comparison

---

## Test-Driven Development Pattern

1. **Write test** (RED)

   ```python
   def test_new_feature():
       result = new_feature()
       assert result == expected
   ```

2. **Implement minimally** (GREEN)

   ```python
   def new_feature():
       return expected  # Stub
   ```

3. **Refactor** (REFACTOR)

   ```python
   def new_feature():
       # Real implementation
       pass
   ```

4. **Add contract test** (PERMANENT)

   ```python
   @pytest.mark.contract
   def test_new_feature_invariants():
       # Validates against oracle or spec
       pass
   ```

---

## Related Files

- `pyproject.toml` — pytest config, coverage settings
- `.mise/tasks/test.toml` — Test execution tasks
- `crates/opendeviationbar-core/src/test_utils/` — Rust test fixtures
- `tests/conftest.py` — Shared pytest fixtures
- `docs/development/RELEASE.md` — Release workflow (includes test requirements)
- `.github/workflows/` — CI/CD automation
