| name | tdd-workflow |
| description | Enforces test-driven development for Rust. Write tests first with cargo test/nextest, implement to pass, refactor, verify >85% coverage with cargo llvm-cov. |
Test-Driven Development Workflow (Rust)
Code samples below use placeholder types (MagicRule, evaluate_rule,
parse_magic_line) for illustration. The current AST types are
#[non_exhaustive] -- literal-construction from outside the crate
won't compile. Construct test fixtures via crate-internal helpers or
the public builder APIs. See AGENTS.md and
GOTCHAS.md for authoritative project structure.
When to Activate
- Writing new features or functionality
- Fixing bugs or issues
- Refactoring existing code
- Adding new magic rule types or operators
- Extending parser, evaluator, or output modules
Core Principles
1. Tests BEFORE Code
Always write tests first, then implement code to make tests pass.
2. Coverage Requirements
- Minimum 85% line coverage per AGENTS.md
- All edge cases covered
- Error scenarios tested
- Boundary conditions verified
- Doc examples verified with
cargo test --doc
3. Test Types
Unit Tests
- Inline
#[cfg(test)] modules alongside source
- Individual functions, parsers, evaluators
- Pure logic and data transformations
.unwrap() / .expect() are acceptable here (see
.claude/hookify.warn-panic-in-lib.md)
Integration Tests
- In
tests/ directory with real magic files
- End-to-end rule parsing and evaluation
- CLI argument handling and output formatting
Property Tests
- Use
proptest for fuzzing magic rule evaluation (tests/property_tests.rs)
- Random input generation for parser robustness
- Boundary value exploration
Benchmarks
- Use
criterion for performance-critical code (benches/*.rs)
- Evaluator hot paths, parser throughput
- Memory-mapped I/O performance
TDD Workflow Steps
Step 1: Define the Behavior
Given [a magic rule with specific offset/type/operator],
When [evaluated against a file buffer with known contents],
Then [the evaluator returns the expected match result].
Step 2: Write Failing Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_feature_basic() {
let rule = make_test_rule();
let buffer: &[u8] = &[0x7f, 0x45, 0x4c, 0x46];
let result = evaluate_rule(&rule, buffer);
assert_eq!(result.unwrap().description, "ELF");
}
#[test]
fn test_new_feature_edge_case() {
let rule = make_test_rule();
assert!(evaluate_rule(&rule, &[]).unwrap().is_none());
}
#[test]
fn test_new_feature_error_case() {
let rule = make_bad_offset_rule();
assert!(evaluate_rule(&rule, &[0x00]).is_err());
}
}
Step 3: Run Tests (They Should Fail)
mise exec -- cargo nextest run -E 'test(test_new_feature)' --no-capture
Step 4: Implement Code
Write minimal code to make tests pass. Follow project patterns:
- Use
.get() for bounds-checked buffer access
- Return
Result<T, LibmagicError> (or a more-specific variant)
- No
unsafe -- workspace lint forbids it
- No
.unwrap(), .expect(), or panic! in library code (test code is
exempt)
Step 5: Run Tests Again
mise exec -- cargo nextest run
Step 6: Refactor
Improve code quality while keeping tests green:
- Remove duplication
- Improve naming
- Extract submodules if a file exceeds the 500--600-line guideline
- Ensure
cargo clippy -- -D warnings is clean
Step 7: Verify Coverage
mise exec -- cargo llvm-cov --html
Testing Patterns
Property-Based Testing
use proptest::prelude::*;
proptest! {
#[test]
fn parser_never_panics_on_arbitrary_input(input in ".*") {
let _ = parse_magic_line(&input);
}
#[test]
fn evaluator_handles_any_buffer(
buffer in prop::collection::vec(any::<u8>(), 0..1024)
) {
let rule = make_test_rule();
let _ = evaluate_rule(&rule, &buffer);
}
}
Table-Driven Tests
Prefer consolidating related cases into a single test with descriptive
failure messages, rather than one assertion per function (AGENTS.md
"Test style").
#[test]
fn test_endianness_variants() {
let cases: &[(_, &[u8], u64)] = &[
(Endianness::Big, &[0x00, 0x01], 1),
(Endianness::Little, &[0x01, 0x00], 1),
];
for &(endian, buffer, expected) in cases {
let actual = read_short(buffer, endian).unwrap();
assert_eq!(actual, expected, "endian={endian:?} buffer={buffer:?}");
}
}
Test Organization
Authoritative module tree lives in AGENTS.md "Module Organization". The
parser and evaluator are both directory modules (parser/grammar/,
evaluator/engine/, evaluator/types/, evaluator/operators/,
evaluator/offset/), not flat files. Inline #[cfg(test)] mod tests
lives alongside the source in each submodule.
Integration tests live in tests/*.rs (e.g.,
tests/compatibility_tests.rs, tests/integration_tests.rs,
tests/property_tests.rs, tests/cli_integration.rs,
tests/json_integration_test.rs).
Benchmarks live in benches/*.rs.
Common Testing Mistakes
WRONG -- testing internal state
assert_eq!(parser.line_number, 5);
CORRECT -- testing observable behavior
let rules = parse_magic_file(input).unwrap();
assert_eq!(rules.len(), 5);
assert_eq!(rules[0].message, "ELF");
WRONG -- only happy path
let result = evaluate_rule(&rule, buffer).unwrap();
CORRECT -- success and error paths both covered
assert!(evaluate_rule(&rule, buffer).is_ok());
assert!(evaluate_rule(&rule, &[]).is_ok());
assert!(evaluate_rule(&bad_rule, buffer).is_err());
Quick Reference
mise exec -- cargo nextest run
mise exec -- cargo test parser::grammar::tests
mise exec -- cargo test -- --nocapture
mise exec -- cargo test --doc
mise exec -- cargo llvm-cov --html
mise exec -- cargo bench
mise exec -- just ci-check