CacheKit employs a multi-layered testing approach combining unit tests, property tests, and fuzz tests to ensure correctness and robustness.
Following the workspace rules, we:
- Test public APIs primarily - Focus on the contract users depend on
- Test critical internal algorithms - Property test complex logic like eviction policies
- Test invariants exhaustively - Capacity bounds, index consistency, reference bit behavior
- Prefer property tests over manual cases - Catch edge cases automatically
- Fuzz hot paths - Public interfaces that handle arbitrary input
Location: #[cfg(test)] mod tests in each module
Purpose: Verify specific behaviors and edge cases
Example:
#[test]
fn clock_ring_eviction_prefers_unreferenced() {
let mut ring = ClockRing::new(2);
ring.insert("a", 1);
ring.insert("b", 2);
ring.touch(&"a");
let evicted = ring.insert("c", 3);
assert_eq!(evicted, Some(("b", 2)));
assert!(ring.contains(&"a"));
}Run:
cargo testLocation: #[cfg(test)] mod property_tests in each module
Purpose: Verify invariants hold across arbitrary inputs
Dependencies: proptest = "1.5"
Key Properties for ClockRing:
- Length never exceeds capacity
- Index and slot consistency
- Get after insert returns correct value
- Remove decreases length
- Update doesn't change length
- Referenced entries survive longer
- Hand position stays within bounds
Example:
proptest! {
#[test]
fn prop_len_within_capacity(
capacity in 1usize..100,
ops in prop::collection::vec((0u32..1000, 0u32..100), 0..200)
) {
let mut ring = ClockRing::new(capacity);
for (key, value) in ops {
ring.insert(key, value);
prop_assert!(ring.len() <= ring.capacity());
}
}
}Run:
cargo test prop_Run with more cases:
PROPTEST_CASES=10000 cargo test prop_len_within_capacityLocation: fuzz/fuzz_targets/
Purpose: Find crashes and invariant violations through mutation-based testing
Dependencies: cargo-fuzz, libfuzzer-sys
Targets:
clock_ring_arbitrary_ops- Random operation sequencesclock_ring_insert_stress- Heavy insert loadclock_ring_eviction_patterns- Reference bit patterns
Run:
cd fuzz
cargo fuzz run clock_ring_arbitrary_ops -- -max_total_time=60See fuzz/README.md for detailed fuzzing instructions.
We expose complex private methods for testing using #[cfg(test)] pub(crate):
impl<K, V> ClockRing<K, V> {
#[cfg(any(test, debug_assertions))]
pub fn debug_validate_invariants(&self) {
// Check all invariants
assert_eq!(self.len, self.slots.iter().filter(|s| s.is_some()).count());
assert_eq!(self.len, self.index.len());
// ...
}
#[cfg(any(test, debug_assertions))]
pub fn debug_snapshot_slots(&self) -> Vec<Option<(&K, bool)>> {
// Expose internal state for assertions
}
}This allows thorough testing without polluting the public API.
- Public APIs: 100% test coverage with both unit and property tests
- Core algorithms: Property tests covering all branches
- Edge cases: Zero capacity, capacity 1, full ring, empty ring
- Concurrent wrappers: Same invariants as single-threaded version
# Unit tests
cargo test
# Property tests with more cases
PROPTEST_CASES=10000 cargo test
# Fuzz tests (short run)
cd fuzz
cargo fuzz run clock_ring_arbitrary_ops -- -max_total_time=60
# With features enabled
cargo test --all-features
# Concurrency tests
cargo test --features concurrencyOur CI runs:
- Unit tests on all supported platforms
- Property tests with increased case count (1000)
- Quick fuzz tests on PRs (60 seconds per target)
- Continuous fuzzing nightly (1 hour per target)
- Tests with all feature combinations
See Fuzzing in CI/CD for detailed fuzzing setup.
Example CI configuration:
# Quick fuzz on PRs
- name: Run fuzz tests (quick)
run: |
cargo install cargo-fuzz
cd fuzz
cargo fuzz run clock_ring_arbitrary_ops -- -max_total_time=60 -seed=1
cargo fuzz run clock_ring_insert_stress -- -max_total_time=60 -seed=2
cargo fuzz run clock_ring_eviction_patterns -- -max_total_time=60 -seed=3
# Property tests with more cases
- name: Run property tests
run: PROPTEST_CASES=1000 cargo test prop_
# Unit tests
- name: Run unit tests
run: cargo test --all-featuresWhen a property test fails, proptest generates a minimal failing case:
Test failed: prop_len_within_capacity
minimal failing input: capacity = 1, ops = [(5, 10)]
Replay the failure:
#[test]
fn reproduce_prop_failure() {
let mut ring = ClockRing::new(1);
ring.insert(5, 10);
ring.debug_validate_invariants();
}Crashes are saved to fuzz/artifacts/<target>/crash-<hash>:
Reproduce:
cargo fuzz run clock_ring_arbitrary_ops fuzz/artifacts/clock_ring_arbitrary_ops/crash-abc123Debug in GDB:
rust-gdb --args target/x86_64-unknown-linux-gnu/release/clock_ring_arbitrary_ops fuzz/artifacts/clock_ring_arbitrary_ops/crash-abc123Performance-critical paths have separate benchmarks (see benchmarks/):
cargo benchDon't use #[test] for performance testing; use criterion benchmarks instead.
- Keep tests close to code - Tests in same file as implementation
- Separate modules -
tests,property_tests,fuzz_tests - Descriptive names -
prop_len_within_capacity, nottest1 - Document test intent - What invariant or behavior is being verified
- Use debug helpers -
debug_validate_invariants(),debug_snapshot_slots()
// 1. Add unit test
#[test]
fn new_feature_basic_behavior() {
// Test happy path
}
// 2. Add property test
proptest! {
#[test]
fn prop_new_feature_maintains_invariants(
input in arbitrary_input_strategy()
) {
// Verify invariants
}
}
// 3. Add fuzz target (if public API)
// fuzz/fuzz_targets/new_feature.rs
fuzz_target!(|data: &[u8]| {
// Decode and test
});