This guide covers running and writing tests for reovim.
# Run all tests
cargo test
# Run tests for a specific crate
cargo test -p reovim-kernel
cargo test -p reovim-server
# Run tests with output
cargo test -- --nocapture
# Run a specific test
cargo test test_name
# Run tests matching a pattern
cargo test bufferTests should be separated from implementation files. Three rules based on module structure and test size:
Rule 1 -- Single file module: {name}_tests.rs sibling
src/
├── word.rs
├── word_tests.rs # tests for word.rs
├── bracket.rs
└── bracket_tests.rs # tests for bracket.rs
Included via #[cfg(test)] mod word_tests; in the parent mod.rs or lib.rs.
Rule 2 -- Directory module, total tests < 500 lines: single tests.rs
src/
├── operators/
│ ├── mod.rs
│ ├── delete.rs
│ ├── change.rs
│ └── tests.rs # tests for all operators
Included via #[cfg(test)] mod tests; in mod.rs.
Rule 3 -- Directory module, total tests >= 500 lines: tests/ subdirectory
src/
├── operators/
│ ├── mod.rs
│ ├── delete.rs
│ ├── change.rs
│ └── tests/
│ ├── mod.rs
│ ├── delete.rs # tests for delete only
│ ├── change.rs # tests for change only
│ └── yank.rs # tests for yank only
Included via #[cfg(test)] mod tests; in mod.rs, with tests/mod.rs
re-exporting submodules.
| Situation | Layout | Inclusion |
|---|---|---|
| Single file, any test size | {name}_tests.rs sibling |
#[cfg(test)] mod {name}_tests; in parent mod |
| Directory, tests < 500 lines | tests.rs inside dir |
#[cfg(test)] mod tests; in mod.rs |
| Directory, tests >= 500 lines | tests/ subdir mirroring source |
#[cfg(test)] mod tests; in mod.rs |
| E2E / cross-module | Cargo tests/ directory |
Automatic by Cargo |
Enforced by scripts/check-test-layout.sh which detects inline test blocks.
Run ./scripts/check-test-layout.sh --fix for guidance on fixing violations.
reovim_kernel::testing provides kernel-level test utilities:
use reovim_kernel::testing::{create_test_context, setup_buffer, TestBufferManager};
// Standard test context with real in-memory buffer manager
let ctx = create_test_context();
// Create a buffer with content
let id = setup_buffer(&ctx, "hello\nworld");create_test_context()--KernelContextwith a realTestBufferManager(stores and retrieves buffers, unlikeKernelContext::default()which uses a stub)setup_buffer(ctx, content)-- convenience forBuffer::from_string()+ctx.buffers.register()TestBufferManager-- in-memoryBufferManagerimplementation
For tests that need services (undo, search, etc.), create a context then register services:
let ctx = create_test_context();
let undo_registry = Arc::new(UndoProviderRegistry::new());
undo_registry.register(UndoKey::Buffer, mock_undo.clone());
ctx.services.register(undo_registry);reovim_driver_session::testing provides session-level test utilities:
use reovim_driver_session::testing::TestSessionRuntime;
let mut test = TestSessionRuntime::new();
test.with_runtime(|runtime| { /* use runtime */ });
test.assert_mode_name("normal");
test.assert_cursor(0, 0);Use reovim_kernel::testing for pure kernel-level tests (motions, text objects,
operators). Use TestSessionRuntime for tests that need session state (mode
transitions, window management, command execution).
test_{action}_{scenario}[_{expected_outcome}]
Examples:
test_delete_word_at_line_starttest_cursor_move_past_eof_clampstest_yank_register_stores_linewisetest_visual_select_empty_line
Every #[ignore] must reference a tracking issue:
#[ignore = "Dot repeat not fully implemented (#NNN)"]Process for deferring tests:
- Create a deferral draft at
tmp/deferral-draft-{topic}.md - Continue working
- After user approval, create the issue and update the
#[ignore]annotation
Reovim includes an end-to-end integration test system that uses server mode to verify key input → expected output behavior by spawning a real server process.
TestServerHarness
├── Spawns: reovim server --grpc 0
├── GrpcClient (TCP connection via gRPC)
└── Auto-cleanup on Drop
Test Flow:
┌─────────────┐ gRPC ┌─────────────────────────┐
│ GrpcClient │ ────────────────→ │ reovim server │
│ │ │ │
│ keys() │ input/keys │ ChannelKeySource │
│ mode() │ state/mode │ Runtime │
│ cursor() │ state/cursor │ Buffer, Screen │
│ buffer() │ buffer/content │ │
│ windows() │ state/windows │ Screen (scroll/cursor) │
└─────────────┘ ←──────────────── └─────────────────────────┘
Response
# All integration tests (using shared testing infrastructure)
cargo test -p reovim-testing
# Run specific module tests
cargo test -p reovim-module-vim
# Single test
cargo test -p reovim-module-vim test_visual_modeNote: Tests use the debug binary by default. Set REOVIM_TEST_BINARY env var to override.
use reovim_testing::IntegrationTest;
#[tokio::test]
async fn test_example() {
let result = IntegrationTest::new()
.await
.with_buffer("hello\nworld") // Initial buffer content
.send_keys("jj") // Key sequence (vim notation)
.run()
.await;
result.assert_cursor(2, 0);
result.assert_normal_mode();
}| Notation | Key Event |
|---|---|
a-z, 0-9 |
Character keys |
<Esc> |
Escape key |
<CR> or <Enter> |
Enter key |
<BS> |
Backspace |
<Tab> |
Tab |
<Space> |
Space |
<C-x> |
Ctrl+X |
<S-x> |
Shift+X |
<Up>, <Down>, <Left>, <Right> |
Arrow keys |
<Home>, <End> |
Navigation keys |
<PageUp>, <PageDown> |
Page keys |
Examples:
.send_keys("ihello<Esc>") // Enter insert, type "hello", escape
.send_keys(":wq<CR>") // Command mode, type "wq", enter
.send_keys("<C-d>") // Ctrl+D
.send_keys("5j") // Move down 5 lines
.send_keys("daw") // Delete a word| Method | Description |
|---|---|
assert_normal_mode() |
Editor is in normal mode |
assert_insert_mode() |
Editor is in insert mode |
assert_visual_mode() |
Editor is in visual mode |
assert_cursor(line, col) |
Cursor at position (0-indexed, line then column) |
assert_buffer_contains("text") |
Buffer contains substring |
assert_buffer_eq("text") |
Buffer equals exactly |
let result = IntegrationTest::new()
.await
.send_keys("ihello<Esc>")
.run()
.await;
// Direct field access
println!("Mode: {:?}", result.edit_mode);
println!("Buffer: {}", result.buffer_content);
println!("Cursor: line={}, col={}", result.cursor_line, result.cursor_column);shared/testing/src/
├── lib.rs # Test harness exports
├── harness.rs # TestServerHarness (subprocess management)
├── integration.rs # IntegrationTest builder + TestResult
└── assertions.rs # Assertion utilities
server/modules/vim/tests/ # Vim module-specific tests
├── operators.rs # dd, yy, p, P, cw, etc.
├── motions.rs # hjkl, w/b/e, gg/G
└── text_objects.rs # iw, aw, i(, a{, etc.
Spawns and manages a reovim server process:
// Automatically spawns server on OS-assigned port
let harness = TestServerHarness::spawn().await?;
// Get a connected client
let client = harness.client().await?;
// Server is killed when harness is dropped (kill_on_drop)gRPC client used in multi-client tests (obtained from MultiClientTest):
// TestClient is accessed via the clients array in MultiClientTest::run()
clients[0].send_keys("ihello<Esc>").await.unwrap(); // Inject keys
let cursor = clients[0].get_cursor().await.unwrap(); // Get cursor position
let content = clients[0].get_buffer().await.unwrap(); // Get buffer content
clients[0].open_buffer("/path/to/file").await.unwrap(); // Open a fileFluent builder for tests:
IntegrationTest::new()
.await
.with_buffer("initial text") // Optional: set initial buffer
.send_keys("dd") // Optional: inject keys (can chain)
.send_keys("p")
.run()
.awaitFor tests that need to verify state at each keystroke, use StepTest:
use reovim_testing::StepTest;
#[tokio::test]
async fn test_delete_word_step_by_step() {
let trace = StepTest::new()
.await
.with_buffer("hello world")
.step("d")
.expect_mode_contains("DELETE")
.step("w")
.expect_buffer("world")
.run()
.await;
trace.assert_ok();
}For tests that need to verify TUI output:
use reovim_testing::frame::{assert_frame_contains, assert_statusline_mode};
// Assert frame contains text
assert_frame_contains(&frame, "Hello");
// Assert statusline shows mode
assert_statusline_mode(&frame, "NORMAL");
// Assert specific line content
assert_frame_line_contains(&frame, 0, "fn main");- Tests use the debug binary by default - Set
REOVIM_TEST_BINARYenv var to override - Keep key sequences short - Long sequences are harder to debug
- Test one behavior per test - Makes failures easier to diagnose
- Use
with_buffer()for cursor tests - Need text to move through - Tests run in parallel - Each spawns its own server on an OS-assigned port
Phase 7+ uses a comprehensive integration test framework in shared/testing/ that tests the complete editor through gRPC, using subprocess-based test isolation.
IntegrationTest (Builder)
├── TestServerHarness
│ ├── Spawns: reovim server --grpc 0 (OS-assigned port)
│ ├── Parses port from stderr
│ └── kill_on_drop(true) for cleanup
├── GrpcClient (TCP gRPC connection)
└── TestResult (captures final state)
Test Flow:
┌──────────────────┐ gRPC ┌─────────────────────────┐
│ IntegrationTest │ ────────────────→ │ reovim server │
│ │ │ │
│ with_buffer() │ buffer/set │ Buffer state │
│ send_keys() │ input/keys │ Key processing │
│ run() → Result │ state/* queries │ Mode, cursor, etc. │
└──────────────────┘ ←──────────────── └─────────────────────────┘
TestResult
The fluent builder pattern makes tests readable and maintainable:
use reovim_testing::IntegrationTest;
#[tokio::test]
async fn test_delete_line() {
let result = IntegrationTest::new()
.await
.with_buffer("line 1\nline 2\nline 3") // Set initial content
.send_keys("jdd") // Move down, delete line
.run()
.await;
result.assert_buffer_eq("line 1\nline 3");
result.assert_cursor(1, 0);
}| Method | Description |
|---|---|
new().await |
Spawn server, create test instance |
with_buffer(content) |
Set initial buffer content |
with_cursor_at(line, col) |
Set initial cursor position |
send_keys(keys) |
Queue key sequence (can chain multiple) |
run().await |
Execute test, return TestResult |
| Method | Description |
|---|---|
assert_buffer_eq(expected) |
Buffer exactly matches |
assert_buffer_contains(text) |
Buffer contains substring |
assert_cursor(line, col) |
Cursor at position (0-indexed) |
assert_mode!(mode) |
Editor in specified mode (macro) |
assert_register(name, content, yank_type) |
Register contains expected text and yank type |
For concurrent client testing:
use reovim_testing::MultiClientTest;
#[tokio::test]
async fn test_two_clients_see_changes() {
MultiClientTest::with_clients(2)
.await
.run(|mut clients| async move {
// Client 0 makes a change
clients[0].send_keys("ihello<Esc>").await.unwrap();
// Client 1 should see it
let content = clients[1].get_buffer().await.unwrap();
assert!(content.contains("hello"));
})
.await;
}shared/testing/src/
├── lib.rs # Test harness exports
├── harness.rs # TestServerHarness (subprocess management)
├── integration.rs # IntegrationTest builder + TestResult
├── multi_client.rs # MultiClientTest + TestClient
├── presence.rs # Presence/connection utilities
├── step_test.rs # StepTest builder for per-keystroke assertions
├── frame.rs # Frame assertion helpers
└── assertions.rs # Assertion macros
server/modules/vim/tests/ # Vim module tests
├── operators.rs # dd, yy, p, P, cw, cc, x, etc.
├── registers.rs # Register operations
├── undo_redo.rs # u, Ctrl-R
├── cursor_movement.rs # hjkl, 0$, gg/G, w/b/e
└── edge_cases.rs # Empty buffer, Unicode, boundaries
# Run all integration tests
cargo test -p reovim-testing
# Run specific module tests
cargo test -p reovim-module-vim --test operators
cargo test -p reovim-module-vim --test cursor_movement
# Run single test
cargo test -p reovim-module-vim --test operators test_dd_deletes_line
# Run with output
cargo test -- --nocapture- Create test file in module's
tests/directory - Import common utilities:
use reovim_testing::IntegrationTest;
- Use async test:
#[tokio::test] async fn test_your_feature() { let result = IntegrationTest::new() .await .with_buffer("test content") .send_keys("your keys") .run() .await; result.assert_buffer_eq("expected"); }
| Component | Location | Purpose |
|---|---|---|
| Test harness | shared/testing/ |
Subprocess management, client utilities |
| Integration builder | shared/testing/src/integration.rs |
Fluent test API |
| Module tests | server/modules/*/tests/ |
Per-module integration tests |
| Protocol | shared/protocol/ |
gRPC v2 definitions |
Integration tests use OS-assigned ports (port 0) for collision-free parallel execution:
- Test servers: OS-assigned ephemeral ports (let the kernel pick an available port)
- Regular servers: Port 12540 by default, auto-increments to 12541-12549 if in use
This approach ensures:
- No port collisions - OS guarantees port uniqueness
- Parallel-safe - Multiple test processes can run simultaneously
- Cross-process safe - Works correctly with
cargo testparallelism - Tests don't interfere with manually started debug servers
IntegrationTest::new()spawnsreovim server --grpc 0(OS-assigned port)- Server binds to an ephemeral port and prints
Listening on 127.0.0.1:<port>to stderr - Test harness reads stderr to discover the actual port
GrpcClientconnects via gRPC and sends commands- Keys are injected with delays between each (for mode propagation)
- After keys, delay allows processing before querying state
- Test cleans up server via kill command
Test coverage is tracked via Codecov. The project enforces 100% coverage on all PRs:
- Non-server crates: 100% MC/DC coverage (line + condition)
- Server crate (
reovim-server): 100% line coverage
Coverage reports are uploaded as CI artifacts (COVERAGE-workspace.md, COVERAGE-server.md) and enforced via Codecov status checks. PRs that regress coverage are blocked.
| Test File | Coverage Area |
|---|---|
operators.rs |
dd, yy, p, P, cw, cc, x, dw, yw, counts |
cursor_movement.rs |
hjkl, 0$^, gg/G, w/b/e, boundaries |
edge_cases.rs |
Empty buffer, Unicode, special chars |
undo_redo.rs |
u, Ctrl-R, multiple undo/redo |
registers.rs |
Named registers, unnamed, append |
multi_client_tests.rs |
Concurrent clients, shared state |
Tests are placed in a tests submodule within the source file:
// In server/lib/kernel/src/core/buffer.rs
#[cfg(test)]
mod tests;
// In server/lib/kernel/src/core/tests.rs
use super::*;
#[test]
fn test_buffer_insert() {
let mut buffer = Buffer::new(0);
buffer.insert_char('a');
assert_eq!(buffer.contents[0].content, "a");
}- Use descriptive names:
test_cursor_moves_down_by_count - Prefix with
test_ - Group related tests in the same file
#[test]
fn test_delete_selection() {
let mut buffer = Buffer::from_content(0, "hello world");
buffer.start_selection();
buffer.cur = Position { row: 0, col: 5 };
buffer.delete_selection();
assert_eq!(buffer.contents[0].content, " world");
}Event handlers can be tested by simulating key events:
#[test]
fn test_count_parser() {
let result = parse_count("5j");
assert_eq!(result.count, Some(5));
assert_eq!(result.remaining, "j");
}- Isolate tests: Each test should be independent
- Test edge cases: Empty buffers, single characters, large files
- Use descriptive assertions:
assert_eq!overassert! - Keep tests fast: Avoid I/O in unit tests
Reovim enforces 100% code coverage in CI. Every PR must maintain full coverage.
# MC/DC coverage (non-server crates) — primary metric
./scripts/coverage.sh mcdc --lcov
# Line coverage (full workspace including server)
./scripts/coverage.sh line --lcov
# Generate human-readable report
./scripts/coverage-report.sh target/llvm-cov/lcov.mcdc.info
# Open HTML report in browser
./scripts/coverage.sh mcdc --openFor genuinely untestable code, use the coverage(off) attribute:
#[cfg_attr(coverage_nightly, coverage(off))]
fn untestable_function() { /* ... */ }The coverage_attribute feature is now stable — no feature gate is needed in lib.rs.
Use sparingly — only for code paths that cannot be exercised in unit tests (async runtime internals, tracing closures, panic handlers).
Coverage is enforced via Codecov status checks on PRs:
- Project target: 100% (0.5% threshold for float rounding)
- Patch target: 100% (all new code must be covered)
- Reports uploaded as CI artifacts (
COVERAGE-workspace.md,COVERAGE-server.md)
Reovim uses Criterion for performance benchmarking.
# Run all benchmarks
cargo bench -p reovim-bench
# Run specific benchmark group
cargo bench -p reovim-bench -- window_render
cargo bench -p reovim-bench -- stress
cargo bench -p reovim-bench -- buffer_clone
# List available benchmarks
cargo run -p perf-report -- list| Category | Benchmarks | Description |
|---|---|---|
window_render |
4 | Window::render() with various buffer sizes |
viewport_size |
4 | Different viewport heights |
screen_io |
3 | Full screen I/O with mock writer |
screen_viewport_io |
3 | Viewport with I/O |
file_io |
2 | Real file I/O (buffered vs unbuffered) |
file_io_viewport |
3 | File I/O at different viewports |
input_typing |
2 | Single char and burst typing |
input_scrolling |
3 | Line, 10-line, half-page scroll |
input_mode_switch |
1 | Normal/Insert/Normal cycle |
input_completion |
2 | With/without completion popup |
input_sustained |
1 | 100 keystrokes with render |
rtt_explorer |
3 | Open, close, toggle explorer |
rtt_input_lag |
2 | Char insert, backspace RTT |
rtt_movement_lag |
5 | Movement operations RTT |
stress_editing |
3 | Edit/navigate cycles (1k, 10k, 50k) |
stress_scroll |
3 | Rapid scrolling tests |
stress_mode_ops |
1 | Insert/escape/move cycles |
stress_completion |
1 | Completion scroll 100 items |
stress_worst_case |
1 | All features, 50k file |
buffer_clone |
4 | Clone overhead by size |
buffer_vec |
4 | Vec creation |
Total: 39 benchmarks
# Generate report for current version
cargo run -p perf-report -- update --version X.Y.Z
# Check for regressions
cargo run -p perf-report -- check
# Compare versions
cargo run -p perf-report -- compare OLD_VER NEW_VERReports are stored in perf/PERF-{version}.md.
Key improvements over v0.3.0 baseline:
| Benchmark | Improvement |
|---|---|
| window_render | 50-62% faster |
| viewport_size | 68-71% faster |
| screen_io | 66-79% faster |
| file_io | 79% faster |
| rtt_movement_lag | 64-91% faster |
| stress_editing | 54-65% faster |
| throughput | 71% faster |
See perf/PERF-0.4.2.md for detailed results.
tools/bench/
├── src/
│ └── lib.rs # Benchmark utilities
└── benches/
├── render.rs # Main entry point
└── bench_modules/
├── common.rs # Shared utilities
├── window.rs # Window render benchmarks
├── screen.rs # Screen I/O benchmarks
├── input.rs # Input simulation
├── rtt.rs # Round-trip time
└── stress.rs # Stress tests
Before submitting a PR:
cargo test # All tests must pass
cargo clippy # No warnings allowed
cargo fmt -- --check # Code must be formatted- Getting Started - Build and code standards
- Commands - Command system (testable units)