Testing Guide
Chassis has a comprehensive test suite covering unit tests, integration tests, and compile-time borrow checker validation.
Running Tests
All Tests
# Run all tests in the workspace
cargo test --workspace
# Run with output
cargo test --workspace -- --nocapture
# Run with specific number of threads
cargo test --workspace -- --test-threads=4
Specific Test Suites
# Storage layer tests
cargo test --package chassis-core --test storage_tests
# Graph I/O tests
cargo test --package chassis-core --test graph_io_tests
# Linking tests
cargo test --package chassis-core --test link_tests
# Search tests
cargo test --package chassis-core --test search_tests
# Compile-time safety tests
cargo test --package chassis-core --test compile_fail
Unit Tests (Embedded)
# Run unit tests in a specific module
cargo test --package chassis-core storage::tests
cargo test --package chassis-core distance::tests
cargo test --package chassis-core hnsw::node::tests
Test Coverage
Storage Layer Tests (storage_tests.rs)
Tests the memory-mapped storage engine:
Basic Operations:
test_create_new_storage(): File creation and initializationtest_insert_and_retrieve(): Vector insertion and retrievaltest_multiple_inserts(): Sequential insertiontest_insert_wrong_dimensions(): Dimension validation
Zero-Copy Access:
test_get_vector_slice(): Slice-based accesstest_vector_slice_lifetime(): Borrow checker enforcement
Durability:
test_commit(): Persistence guaranteestest_persistence(): File reopen after committest_uncommitted_changes(): Data loss on crash simulation
File Growth:
test_storage_growth(): Automatic file expansiongrowthbench (storage_bench): growing an empty file to 1,000 vectors
Concurrency:
test_exclusive_lock(): File lockingtests/reader_tests.rs: readers in this and another process while a writer adds, deletes and restarts
Graph I/O Tests (graph_io_tests.rs)
Tests HNSW graph persistence and addressing:
Graph Header:
test_graph_header_roundtrip(): Serialization/deserializationtest_graph_header_validation(): Magic bytes and version checkstest_graph_header_persistence(): Survives reopening
Node Records:
test_node_record_write_read(): Basic record I/Otest_sequential_node_writes(): Batch writestest_node_record_updates(): Updating existing nodes
Addressing:
test_o1_addressing(): Offset computation correctnesstest_node_offset_consistency(): Deterministic offsetstest_addressing_formula_correctness(): Mathematical validation
Neighbor Iteration:
test_neighbors_iter_from_mmap(): Zero-allocation iterationtest_neighbors_iter_empty_layer(): Edge case handlingtest_neighbors_iter_filters_invalid():INVALID_NODE_IDfiltering
Linking Tests (link_tests.rs)
Tests bidirectional graph linking and diversity heuristics:
Basic Linking:
test_link_single_node(): Single node insertiontest_link_multiple_nodes(): Sequential linkingtest_forward_links_exist(): Outgoing edges persisted
Backward Links:
test_backward_links_added(): Reciprocal edges createdtest_idempotency(): Duplicate link preventiontest_backward_link_pruning(): Overflow handling
Diversity Heuristic:
test_diversity_selection(): Heuristic 2 applicationtest_starvation_fallback(): Minimum degree guarantee
Invariant Enforcement:
test_self_links_filtered(): No self-loopstest_future_neighbors_filtered(): No forward references (Model A)test_sequential_id_enforcement(): Node ID monotonicity
Crash Consistency:
test_crash_during_linking(): Partial updates are safetest_ghost_nodes(): Unreachable nodes ignored
Search Tests (search_tests.rs)
Tests HNSW search algorithm:
Basic Search:
test_search_empty_graph(): Empty graph returns empty resultstest_search_single_node(): Trivial casetest_search_k_neighbors(): Correct k returnedtest_search_ef_correction():ef < khandled
Result Quality:
test_search_accuracy(): Distance sortingtest_search_recall(): Finds true nearest neighborstest_search_no_duplicates(): Unique results only
Visited Filter:
test_visited_filter(): Dense array correctnesstest_visited_filter_out_of_bounds(): Bounds checking
Edge Cases:
test_nan_safe_ordering(): NaN distances don’t panictest_search_large_ef(): High ef valuestest_search_disconnected_graph(): Unreachable nodes
Performance:
test_search_scales_logarithmically(): Hop count grows slowly
Compile-Time Safety Tests (compile_fail.rs)
Uses trybuild to verify borrow checker enforcement:
UI Tests (tests/ui/):
-
slice_then_insert.rs(Expected: Fails to compile)#![allow(unused)] fn main() { let slice = storage.get_vector_slice(0)?; storage.insert(&vec![1.0; 128])?; // Mutable borrow while slice alive println!("{}", slice[0]); }Error: Cannot borrow
storageas mutable -
slice_then_commit.rs(Expected: Fails to compile)#![allow(unused)] fn main() { let slice = storage.get_vector_slice(0)?; storage.commit()?; // Mutable borrow (commit modifies mmap) println!("{}", slice[0]); }Error: Cannot borrow
storageas mutable -
slice_escapes_scope.rs(Expected: Fails to compile)#![allow(unused)] fn main() { let slice = { let storage = Storage::open(...)?; storage.get_vector_slice(0)? }; // storage dropped, slice now dangling println!("{}", slice[0]); }Error: Slice lifetime exceeds storage
-
return_slice_then_mutate.rs(Expected: Fails to compile)#![allow(unused)] fn main() { fn get_slice(storage: &Storage) -> &[f32] { storage.get_vector_slice(0).unwrap() } let slice = get_slice(&storage); storage.insert(&vec![1.0; 128])?; // Mutable borrow }Error: Cannot borrow
storageas mutable
Run UI Tests:
cargo test --package chassis-core --test compile_fail
Expected output: All tests pass (compilation failures are expected).
Unit Tests by Module
distance.rs Tests
#![allow(unused)]
fn main() {
#[test]
fn test_euclidean_distance_basic()
fn test_cosine_distance()
fn test_simd_correctness_small() // Tests tail loop
fn test_simd_correctness_large() // Tests main loop
fn test_simd_random_vectors() // Fuzz testing
fn test_simd_edge_cases() // Zero, identical, sparse
fn test_avx2_specific() // x86_64 only
fn test_neon_specific() // aarch64 only
}
Coverage:
- SIMD vs scalar equivalence
- Edge cases (zeros, identical vectors, NaN)
- Dimension scaling (3D to 3072D)
- Architecture-specific paths
header.rs Tests
#![allow(unused)]
fn main() {
#[test]
fn test_roundtrip()
fn test_any_flipped_bit_invalidates_the_copy()
fn test_newer_versions_are_errors_and_old_ones_are_not_v3()
fn test_newest_copy()
}
Coverage:
- Header validation logic
- Magic byte checking
- Dimension bounds (0, MAX_DIMENSIONS+1)
hnsw/node.rs Tests
#![allow(unused)]
fn main() {
#[test]
fn test_node_header_size()
fn test_record_size_calculation()
fn test_layer_offset_calculation()
fn test_set_and_get_neighbors()
fn test_add_neighbor()
fn test_serialization_roundtrip()
fn test_node_to_record_conversion()
fn test_compute_node_offset()
fn test_addressing_formula_consistency()
}
Coverage:
- Fixed-size record layout
- Neighbor management
- Serialization correctness
- O(1) addressing formula
hnsw/graph.rs Tests
#![allow(unused)]
fn main() {
#[test]
fn test_graph_header_roundtrip()
fn test_o1_addressing_no_hashmap()
fn test_graph_persistence()
fn test_insert_out_of_order_panics() // Debug only
fn test_insert_duplicate_panics() // Debug only
fn test_update_existing_node()
}
Coverage:
- Graph header persistence
- O(1) node addressing
- Sequential ID enforcement
- Update vs insert semantics
Testing Best Practices
1. Use Temporary Files
#![allow(unused)]
fn main() {
use tempfile::NamedTempFile;
#[test]
fn my_test() -> anyhow::Result<()> {
let temp_file = NamedTempFile::new()?;
let storage = Storage::open(temp_file.path(), 128)?;
// Test logic...
Ok(())
} // File automatically deleted
}
Why: Prevents test pollution and ensures isolation.
2. Test Invariants Explicitly
#![allow(unused)]
fn main() {
#[test]
fn test_node_id_invariant() {
let graph = create_test_graph();
// Invariant: node_count tracks reachable nodes
assert_eq!(graph.node_count(), 10);
// Invariant: all nodes have sequential IDs
for i in 0..graph.node_count() {
assert!(graph.read_node_record(i).is_ok());
}
}
}
3. Test Edge Cases
#![allow(unused)]
fn main() {
#[test]
fn test_empty_graph() {
let graph = create_empty_graph();
assert!(graph.search(&query, 10, 50)?.is_empty());
}
#[test]
fn test_single_node() {
let graph = create_graph_with_one_node();
let results = graph.search(&query, 10, 50)?;
assert_eq!(results.len(), 1);
}
}
4. Use Deterministic Random Data
#![allow(unused)]
fn main() {
#[test]
fn test_with_deterministic_random() {
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hash, Hasher};
fn hash_to_f32(seed: u64) -> f32 {
let state = RandomState::new();
let mut hasher = state.build_hasher();
seed.hash(&mut hasher);
(hasher.finish() % 10000) as f32 / 10000.0
}
let vector: Vec<f32> = (0..128).map(hash_to_f32).collect();
// Reproducible across test runs
}
}
5. Test Concurrency (Where Applicable)
#![allow(unused)]
fn main() {
#[test]
fn test_exclusive_lock() {
let temp_file = NamedTempFile::new().unwrap();
let path = temp_file.path();
let _storage1 = Storage::open(path, 128).unwrap();
// Second open should fail
let result = Storage::open(path, 128);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("already open"));
}
}
Running Tests with Coverage
Install tarpaulin:
cargo install cargo-tarpaulin
Generate coverage report:
cargo tarpaulin --workspace --out Html --output-dir coverage
Open coverage/index.html in browser.
Target: Aim for >80% line coverage, >90% for critical paths (storage, search).
Continuous Integration
Example GitHub Actions workflow:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- name: Run tests
run: cargo test --workspace --verbose
- name: Run compile-fail tests
run: cargo test --package chassis-core --test compile_fail
- name: Check formatting
run: cargo fmt -- --check
- name: Run clippy
run: cargo clippy --workspace -- -D warnings