| name | TDD Solver |
| description | Implement Advent of Code solutions using Test-Driven Development. Generates test cases from puzzle examples, writes failing tests first, implements solutions incrementally, and iterates until all tests pass. Use when solving AoC puzzles, implementing solutions with TDD, or when user mentions test-driven development or writing tests. |
TDD Solver
Purpose
This skill implements puzzle solutions using strict Test-Driven Development (TDD) methodology. It generates test cases from examples, implements solutions incrementally, and iterates until all tests pass.
Core Principles
- Tests First: Always write tests before implementation
- Red-Green-Refactor:
- Red: Write failing test
- Green: Make it pass with minimal code
- Refactor: Clean up while keeping tests passing
- Incremental Development: Build solution step by step
- Example-Driven: All examples from puzzle must become tests
Workflow
Phase 1: Generate Test Cases
From parsed puzzle data, generate Rust test functions:
#[cfg(test)]
mod tests {
use super::*;
const EXAMPLE_INPUT: &str = "<extracted from puzzle>";
#[test]
fn test_part1_example1() {
let result = part1(EXAMPLE_INPUT);
assert_eq!(result, <expected_output>, "Example 1 should match");
}
#[test]
fn test_part1_edge_case_empty() {
}
}
Phase 2: Initial Implementation Stub
Create minimal function signatures:
pub fn part1(input: &str) -> i64 {
0
}
pub fn part2(input: &str) -> i64 {
0
}
Phase 3: Iterative Development
Loop:
1. Run: cargo test
2. If all tests pass → DONE
3. If tests fail:
a. Read failure output
b. Understand what's needed
c. Implement next piece
d. Goto 1
Max iterations: 50
If exceeded → Flag for manual review
Phase 4: Edge Case Detection
After examples pass, consider common edge cases:
#[test]
fn test_empty_input() {
assert_eq!(part1(""), expected_for_empty);
}
#[test]
fn test_single_item() {
assert_eq!(part1("single"), expected_for_single);
}
#[test]
fn test_large_numbers() {
}
#[test]
fn test_boundary_conditions() {
}
Phase 5: Validation
Before declaring solution complete:
cargo test --package aoc-2025 --lib days::day{day}::tests
cargo build 2>&1 | grep -i warning && exit 1
timeout 15s cargo run -- {day}
cargo fmt --check
cargo clippy -- -D warnings
File Structure Per Day
use std::fs;
fn parse_input(input: &str) -> DataStructure {
}
fn solve_part1_logic(data: &DataStructure) -> i64 {
}
pub fn part1(input: &str) -> i64 {
let data = parse_input(input);
solve_part1_logic(&data)
}
pub fn part2(input: &str) -> i64 {
let data = parse_input(input);
solve_part2_logic(&data)
}
pub fn run() {
let input = fs::read_to_string("puzzles/day{day:02}/input.txt")
.expect("Failed to read input file");
println!("Day {day}: {Puzzle Title}");
println!("Part 1: {}", part1(&input));
println!("Part 2: {}", part2(&input));
}
#[cfg(test)]
mod tests {
use super::*;
}
Test Generation Templates
Template 1: Basic Equality Test
#[test]
fn test_part1_example1() {
let input = r#"<example input>"#;
assert_eq!(part1(input), <expected>, "<description>");
}
Template 2: Multi-Step Verification
#[test]
fn test_parsing_then_solving() {
let input = r#"<example input>"#;
let parsed = parse_input(input);
assert_eq!(parsed.len(), <expected_length>);
let result = solve_part1_logic(&parsed);
assert_eq!(result, <expected>);
}
Template 3: Error Handling
#[test]
#[should_panic(expected = "Invalid input")]
fn test_invalid_input() {
part1("garbage input");
}
Common Parsing Patterns
Pattern 1: Line-by-Line Numbers
fn parse_input(input: &str) -> Vec<i64> {
input
.lines()
.filter_map(|line| line.trim().parse().ok())
.collect()
}
Pattern 2: Groups Separated by Blank Lines
fn parse_input(input: &str) -> Vec<Vec<String>> {
input
.split("\n\n")
.map(|group| group.lines().map(String::from).collect())
.collect()
}
Pattern 3: Grid/Matrix
fn parse_input(input: &str) -> Vec<Vec<char>> {
input
.lines()
.map(|line| line.chars().collect())
.collect()
}
Pattern 4: Key-Value Pairs
fn parse_input(input: &str) -> HashMap<String, i64> {
input
.lines()
.filter_map(|line| {
let parts: Vec<_> = line.split(": ").collect();
if parts.len() == 2 {
Some((parts[0].to_string(), parts[1].parse().ok()?))
} else {
None
}
})
.collect()
}
Implementation Strategies
Strategy 1: Brute Force First
Start with the simplest, most obvious solution:
pub fn part1(input: &str) -> i64 {
}
Strategy 2: Incremental Optimization
Only optimize if:
- Tests are failing due to timeout
- Real input is too large for brute force
- Problem explicitly requires optimization
Strategy 3: Reuse Common Utilities
use crate::utils::{
read_input,
parse_char_grid,
parse_int_lines,
split_by_blank_lines
};
Debugging Failed Tests
When tests fail:
1. Read Error Message Carefully
assertion `left == right` failed
left: 42
right: 24
→ Solution returned 42, expected 24
→ Check logic, likely off by some factor
2. Add Debug Prints
#[test]
fn test_part1_example1() {
let input = EXAMPLE_INPUT;
let parsed = parse_input(input);
eprintln!("Parsed: {:?}", parsed);
let result = part1(input);
eprintln!("Result: {}", result);
assert_eq!(result, 24000);
}
3. Verify Parsing
Most errors come from incorrect parsing:
#[test]
fn test_parsing() {
let input = EXAMPLE_INPUT;
let parsed = parse_input(input);
assert_eq!(parsed.len(), 5, "Should have 5 groups");
}
4. Test Individual Components
#[test]
fn test_calculate_totals() {
let data = vec![vec![1, 2, 3], vec![4, 5]];
let totals = calculate_totals(&data);
assert_eq!(totals, vec![6, 9]);
}
Handling Part 2
Part 2 often:
- Extends Part 1 logic
- Changes the question asked
- Adds complexity or new requirements
Option 1: Extend Part 1
pub fn part2(input: &str) -> i64 {
let data = parse_input(input);
solve_part2_logic(&data)
}
Option 2: Refactor Both Parts
If Part 2 reveals a better structure:
fn solve_generic(input: &str, mode: SolveMode) -> i64 {
let data = parse_input(input);
match mode {
SolveMode::Part1 => solve_part1_logic(&data),
SolveMode::Part2 => solve_part2_logic(&data),
}
}
pub fn part1(input: &str) -> i64 {
solve_generic(input, SolveMode::Part1)
}
pub fn part2(input: &str) -> i64 {
solve_generic(input, SolveMode::Part2)
}
Testing Before Submission
Before generating answer for submission:
cargo test days::day{day}
time cargo run -- {day}
cargo run -- {day} | grep "Part 1:" | awk '{print $3}' | grep -E '^[0-9]+$'
Common Pitfalls
Pitfall 1: Integer Overflow
pub fn part1(input: &str) -> i64 {
}
Pitfall 2: Off-By-One Errors
for i in 0..n {
for i in 0..=n {
Pitfall 3: String Trimming
let clean_input = input.trim();
Pitfall 4: Example vs Real Input
Performance Requirements
- Solution must complete in < 15 seconds (AoC rule)
- Most efficient solutions run in < 1 second
- If taking too long, consider:
- Better algorithm (reduce time complexity)
- Memoization/caching
- Different data structure
Integration Points
Input
- Parsed puzzle data from puzzle-fetcher skill
- Real input file from puzzles/day{day}/input.txt
Output
- Implemented solution in src/days/day{day}.rs
- Answer for submission (single integer/string)
- All tests passing
Called By
Reports To
- Test results to orchestrator
- Generated answer for submission-handler