Testing Guidelines
Comprehensive testing guidelines for libmagic-rs to ensure code quality, reliability, and maintainability.
Testing Philosophy
libmagic-rs follows a comprehensive testing strategy:
- Unit tests: Test individual functions and methods in isolation
- Integration tests: Test complete workflows and component interactions
- Property tests: Use fuzzing to discover edge cases and ensure robustness
- Compatibility tests: Verify compatibility with existing magic files and GNU file output
- Performance tests: Ensure performance requirements are met
Test Organization
Directory Structure
libmagic-rs/
├── src/
│ ├── lib.rs # Unit tests in #[cfg(test)] modules
│ ├── parser/
│ │ ├── mod.rs # Parser unit tests
│ │ └── ast.rs # AST unit tests
│ └── evaluator/
│ └── mod.rs # Evaluator unit tests
├── tests/
│ ├── integration/ # Integration tests
│ ├── compatibility/ # GNU file compatibility tests
│ └── fixtures/ # Test data and expected outputs
│ ├── magic/ # Sample magic files
│ ├── samples/ # Test binary files
│ └── expected/ # Expected output files
└── benches/ # Performance benchmarks
Test Categories
Unit Tests
Located in #[cfg(test)]
modules within source files:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; #[test] fn test_basic_functionality() { // Arrange let input = create_test_input(); // Act let result = function_under_test(input); // Assert assert_eq!(result, expected_output); } } }
Integration Tests
Located in tests/
directory:
#![allow(unused)] fn main() { // tests/integration/basic_workflow.rs use libmagic_rs::{EvaluationConfig, MagicDatabase}; #[test] fn test_complete_file_analysis_workflow() { let db = MagicDatabase::load_from_file("tests/fixtures/magic/basic.magic") .expect("Failed to load magic database"); let result = db .evaluate_file("tests/fixtures/samples/elf64") .expect("Failed to evaluate file"); assert_eq!(result.description, "ELF 64-bit LSB executable"); } }
Writing Effective Tests
Test Naming
Use descriptive names that explain the scenario being tested:
#![allow(unused)] fn main() { // Good: Descriptive test names #[test] fn test_parse_absolute_offset_with_positive_decimal_value() {} #[test] fn test_parse_absolute_offset_with_hexadecimal_value() {} #[test] fn test_parse_offset_returns_error_for_invalid_syntax() {} // Bad: Generic test names #[test] fn test_parse_offset() {} #[test] fn test_error_case() {} }
Test Structure
Follow the Arrange-Act-Assert pattern:
#![allow(unused)] fn main() { #[test] fn test_magic_rule_evaluation_with_matching_bytes() { // Arrange let rule = MagicRule { offset: OffsetSpec::Absolute(0), typ: TypeKind::Byte, op: Operator::Equal, value: Value::Uint(0x7f), message: "ELF magic".to_string(), children: vec![], level: 0, }; let buffer = vec![0x7f, 0x45, 0x4c, 0x46]; // ELF magic // Act let result = evaluate_rule(&rule, &buffer); // Assert assert!(result.is_ok()); assert!(result.unwrap()); } }
Assertion Best Practices
Use specific assertions with helpful error messages:
#![allow(unused)] fn main() { // Good: Specific assertions assert_eq!(result.description, "ELF executable"); assert!(result.confidence > 0.8); // Good: Custom error messages assert_eq!( parsed_offset, OffsetSpec::Absolute(42), "Parser should correctly handle decimal offset values" ); // Good: Pattern matching for complex types match result { Ok(OffsetSpec::Indirect { base_offset, adjustment, .. }) => { assert_eq!(base_offset, 0x20); assert_eq!(adjustment, 4); } _ => panic!("Expected indirect offset specification"), } // Avoid: Generic assertions assert!(result.is_ok()); assert_ne!(value, 0); }
Error Testing
Test error conditions thoroughly:
#![allow(unused)] fn main() { #[test] fn test_parse_magic_file_with_invalid_syntax() { let invalid_magic = "0 invalid_type value message"; let result = parse_magic_string(invalid_magic); assert!(result.is_err()); match result { Err(LibmagicError::ParseError { line, message }) => { assert_eq!(line, 1); assert!(message.contains("invalid_type")); } _ => panic!("Expected ParseError for invalid syntax"), } } #[test] fn test_file_evaluation_with_missing_file() { let db = MagicDatabase::load_from_file("tests/fixtures/magic/basic.magic").unwrap(); let result = db.evaluate_file("nonexistent_file.bin"); assert!(result.is_err()); match result { Err(LibmagicError::IoError(_)) => (), // Expected _ => panic!("Expected IoError for missing file"), } } }
Edge Case Testing
Test boundary conditions and edge cases:
#![allow(unused)] fn main() { #[test] fn test_offset_parsing_edge_cases() { // Test zero offset let result = parse_offset("0"); assert_eq!(result.unwrap(), OffsetSpec::Absolute(0)); // Test maximum positive offset let result = parse_offset(&i64::MAX.to_string()); assert_eq!(result.unwrap(), OffsetSpec::Absolute(i64::MAX)); // Test negative offset let result = parse_offset("-1"); assert_eq!(result.unwrap(), OffsetSpec::Absolute(-1)); // Test empty input let result = parse_offset(""); assert!(result.is_err()); } }
Property-Based Testing
Use proptest
for fuzzing and property-based testing:
#![allow(unused)] fn main() { use proptest::prelude::*; proptest! { #[test] fn test_magic_rule_serialization_roundtrip(rule in any::<MagicRule>()) { // Property: serialization should be reversible let json = serde_json::to_string(&rule)?; let deserialized: MagicRule = serde_json::from_str(&json)?; prop_assert_eq!(rule, deserialized); } #[test] fn test_offset_resolution_never_panics( offset in any::<OffsetSpec>(), buffer in prop::collection::vec(any::<u8>(), 0..1024) ) { // Property: offset resolution should never panic let _ = resolve_offset(&offset, &buffer, 0); // If we reach here without panicking, the test passes } } }
Test Data Management
Fixture Organization
Organize test data systematically:
tests/fixtures/
├── magic/
│ ├── basic.magic # Simple rules for testing
│ ├── complex.magic # Complex hierarchical rules
│ └── invalid.magic # Invalid syntax for error testing
├── samples/
│ ├── elf32 # 32-bit ELF executable
│ ├── elf64 # 64-bit ELF executable
│ ├── zip_archive.zip # ZIP file
│ └── text_file.txt # Plain text file
└── expected/
├── elf32.txt # Expected output for elf32
├── elf64.json # Expected JSON output for elf64
└── compatibility.txt # GNU file compatibility results
Creating Test Fixtures
#![allow(unused)] fn main() { // Helper function for creating test data fn create_elf_magic_rule() -> MagicRule { MagicRule { offset: OffsetSpec::Absolute(0), typ: TypeKind::Long { endian: Endianness::Little, signed: false, }, op: Operator::Equal, value: Value::Bytes(vec![0x7f, 0x45, 0x4c, 0x46]), message: "ELF executable".to_string(), children: vec![], level: 0, } } // Helper for creating test buffers fn create_elf_buffer() -> Vec<u8> { let mut buffer = vec![0x7f, 0x45, 0x4c, 0x46]; // ELF magic buffer.extend_from_slice(&[0x02, 0x01, 0x01, 0x00]); // 64-bit, little-endian buffer.resize(64, 0); // Pad to minimum ELF header size buffer } }
Compatibility Testing
GNU File Comparison
Test compatibility with GNU file
command:
#![allow(unused)] fn main() { #[test] fn test_gnu_file_compatibility() { use std::process::Command; let sample_file = "tests/fixtures/samples/elf64"; // Get GNU file output let gnu_output = Command::new("file") .arg("--brief") .arg(sample_file) .output() .expect("Failed to run GNU file command"); let gnu_result = String::from_utf8(gnu_output.stdout) .expect("Invalid UTF-8 from GNU file") .trim(); // Get libmagic-rs output let db = MagicDatabase::load_from_file("tests/fixtures/magic/standard.magic").unwrap(); let result = db.evaluate_file(sample_file).unwrap(); // Compare results (allowing for minor differences) assert!( results_are_compatible(&result.description, gnu_result), "libmagic-rs output '{}' not compatible with GNU file output '{}'", result.description, gnu_result ); } fn results_are_compatible(rust_output: &str, gnu_output: &str) -> bool { // Implement compatibility checking logic // Allow for minor differences in formatting, version numbers, etc. rust_output.contains("ELF") && gnu_output.contains("ELF") } }
Performance Testing
Benchmark Tests
Use criterion
for performance benchmarks:
#![allow(unused)] fn main() { // benches/evaluation_bench.rs use criterion::{Criterion, black_box, criterion_group, criterion_main}; use libmagic_rs::{EvaluationConfig, MagicDatabase}; fn bench_file_evaluation(c: &mut Criterion) { let db = MagicDatabase::load_from_file("tests/fixtures/magic/standard.magic") .expect("Failed to load magic database"); c.bench_function("evaluate_elf_file", |b| { b.iter(|| { db.evaluate_file(black_box("tests/fixtures/samples/elf64")) .expect("Evaluation failed") }) }); } criterion_group!(benches, bench_file_evaluation); criterion_main!(benches); }
Performance Regression Testing
#![allow(unused)] fn main() { #[test] fn test_evaluation_performance() { use std::time::Instant; let db = MagicDatabase::load_from_file("tests/fixtures/magic/standard.magic").unwrap(); let start = Instant::now(); let _result = db .evaluate_file("tests/fixtures/samples/large_file.bin") .unwrap(); let duration = start.elapsed(); // Ensure evaluation completes within reasonable time assert!( duration.as_millis() < 100, "File evaluation took too long: {}ms", duration.as_millis() ); } }
Test Execution
Running Tests
# Run all tests
cargo test
# Run with nextest (faster, better output)
cargo nextest run
# Run specific test modules
cargo test ast_structures
cargo test integration
# Run tests with output
cargo test -- --nocapture
# Run ignored tests
cargo test -- --ignored
# Run property tests with more cases
PROPTEST_CASES=10000 cargo test proptest
Coverage Analysis
# Install coverage tools
cargo install cargo-llvm-cov
# Generate coverage report
cargo llvm-cov --html --open
# Coverage for specific tests
cargo llvm-cov --html --tests integration
Continuous Integration
Ensure tests run in CI with multiple configurations:
# .github/workflows/test.yml
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable, beta]
steps:
- name: Run tests
run: cargo nextest run --all-features
- name: Run property tests
run: cargo test proptest
env:
PROPTEST_CASES: 1000
- name: Check compatibility
run: cargo test compatibility
if: matrix.os == 'ubuntu-latest'
Test Maintenance
Keeping Tests Updated
- Update fixtures: When adding new file format support
- Maintain compatibility: Update compatibility tests when GNU file changes
- Performance baselines: Update performance expectations as optimizations are added
- Documentation: Keep test documentation current with implementation
Test Debugging
#![allow(unused)] fn main() { // Use debug output for failing tests #[test] fn debug_failing_test() { let result = function_under_test(); println!("Debug output: {:?}", result); assert_eq!(result, expected_value); } // Use conditional compilation for debug tests #[cfg(test)] #[cfg(feature = "debug-tests")] mod debug_tests { #[test] fn verbose_test() { // Detailed debugging test } } }
This comprehensive testing approach ensures libmagic-rs maintains high quality, reliability, and compatibility throughout its development lifecycle.