Architecture Overview¶
rpytest uses a hybrid Rust/Python architecture to achieve maximum performance while maintaining full pytest compatibility.
High-Level Design¶
┌────────────────────────────────────────────────────────────────┐
│ rpytest CLI │
│ (Rust Binary) │
├────────────────────────────────────────────────────────────────┤
│ Argument Parsing │ Config Loading │ Output Formatting │ Watch │
└─────────────────────────────┬──────────────────────────────────┘
│ IPC (NNG + MessagePack)
▼
┌────────────────────────────────────────────────────────────────┐
│ Python Daemon │
│ (rpytest_daemon package) │
├────────────────────────────────────────────────────────────────┤
│ Test Collection │ Scheduling │ Workers │ Fixtures │ Reporting │
└────────────────────────────────────────────────────────────────┘
Why Hybrid?¶
Rust for CLI¶
- Fast startup: Sub-10ms cold start
- Efficient I/O: Native file watching, parallel file reading
- Low memory: Minimal overhead for coordination
- Single binary: No Python environment needed for invocation
Python for Execution¶
- Full compatibility: Native pytest plugin loading
- Fixture support: Complex fixture scoping and dependency resolution
- Plugin ecosystem: All pytest plugins work out of the box
- Dynamic features: Runtime test generation, parametrization
Component Breakdown¶
1. CLI Layer (crates/rpytest/)¶
crates/rpytest/src/
├── cli/
│ ├── args.rs # Argument parsing (clap)
│ ├── output.rs # Terminal output formatting
│ └── json_output.rs # JSON report generation
├── config/
│ └── mod.rs # Config file loading (pyproject.toml)
├── daemon/
│ ├── client.rs # Daemon communication
│ └── lifecycle.rs # Auto-start/stop management
├── watch/
│ └── dependency.rs # File change tracking
└── main.rs # Entry point
2. IPC Layer (crates/rpytest-ipc/)¶
Handles communication between Rust and Python:
// Message protocol
pub enum Request {
Collect { paths: Vec<String>, ... },
Run { tests: Vec<String>, ... },
Shutdown,
}
pub enum Response {
CollectResult { tests: Vec<TestItem> },
TestResult { node_id: String, outcome: Outcome, ... },
Error { message: String },
}
- Transport: NNG (nanomsg-next-gen) sockets
- Serialization: MessagePack for efficiency
- Protocol: Request-response with streaming results
3. Core Types (crates/rpytest-core/)¶
Shared data structures:
pub struct TestItem {
pub node_id: String,
pub name: String,
pub path: PathBuf,
pub line: usize,
pub markers: Vec<String>,
}
pub enum Outcome {
Passed,
Failed { message: String },
Skipped { reason: String },
Error { message: String },
}
4. Python Daemon (daemon/rpytest_daemon/)¶
daemon/rpytest_daemon/
├── cli.py # Daemon entry point
├── server.py # IPC server implementation
├── protocol.py # Message serialization
├── native_collector.py # AST-based fast collection
├── executor.py # Test execution coordination
├── worker.py # Individual test runner
├── scheduler.py # LPT-based work distribution
├── sharding.py # CI/CD test distribution
└── fixtures.py # Session fixture management
Data Flow¶
Collection Phase¶
1. CLI receives test paths
2. CLI sends Collect request to daemon
3. Daemon uses native_collector for fast AST parsing
4. Daemon falls back to pytest for complex cases
5. Daemon returns test list with metadata
6. CLI filters and displays results
Execution Phase¶
1. CLI sends Run request with test IDs
2. Daemon scheduler assigns tests to workers
3. Workers execute via subprocess (isolation)
4. Results stream back through IPC
5. CLI formats and displays progress
6. Final summary computed and shown
Key Design Decisions¶
1. Daemon Model¶
Decision: Persistent daemon vs. fresh process per run
Rationale: - Session fixtures need to persist across runs - Import caching saves startup time - Worker process pool stays warm
2. Subprocess Workers¶
Decision: Each test run in subprocess vs. in-process
Rationale: - Complete isolation between tests - Crash in one test doesn't affect others - Clean import state for each test
3. Native Collection¶
Decision: AST parsing in Python vs. pytest collection
Rationale: - 10-100x faster than pytest's import-based collection - No side effects during collection - Falls back to pytest for edge cases
4. IPC Protocol¶
Decision: NNG + MessagePack vs. stdio/JSON
Rationale: - Binary protocol is faster than text - Async socket supports streaming results - Structured messages prevent parsing errors
Extension Points¶
Custom Schedulers¶
class CustomScheduler:
def schedule(self, tests: List[TestItem]) -> List[Batch]:
# Custom test ordering logic
pass
Custom Reporters¶
class CustomReporter:
def on_test_start(self, test: TestItem): ...
def on_test_end(self, test: TestItem, result: Result): ...
def on_session_end(self, summary: Summary): ...
Performance Characteristics¶
| Component | Startup | Memory | Throughput |
|---|---|---|---|
| Rust CLI | <10ms | ~5MB | N/A |
| Python Daemon | ~100ms | ~30MB | N/A |
| Worker Process | ~50ms | ~20MB | ~100 tests/s |
| IPC | <1ms/msg | ~1KB/msg | ~10k msg/s |
Security Model¶
- Daemon runs as current user
- Socket permissions restricted to user
- No network exposure (Unix domain socket)
- Tests run in isolated subprocesses