Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ make test clippy export-graph # Must pass before PR
- `src/traits.rs` - `Problem`, `ConstraintSatisfactionProblem` traits
- `src/rules/traits.rs` - `ReduceTo<T>`, `ReductionResult` traits
- `src/registry/` - Compile-time reduction metadata collection
- `src/unit_tests/` - Unit test files (extracted from inline `mod tests` blocks via `#[path]`)
- `tests/main.rs` - User-facing integration tests only (modules in `tests/suites/`)

### Trait Hierarchy

Expand Down
16 changes: 16 additions & 0 deletions .claude/rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,23 @@ make clippy # No warnings
make coverage # >95% for new code
```

## Test File Organization

Unit tests live in `src/unit_tests/`, mirroring `src/` structure. Source files reference them via `#[path]`:

```rust
// In src/rules/foo_bar.rs:
#[cfg(test)]
#[path = "../unit_tests/rules/foo_bar.rs"]
mod tests;
```

The `#[path]` is relative to the source file's directory. `use super::*` in the test file resolves to the parent module (same as inline tests).

Integration tests are consolidated into a single binary at `tests/main.rs`, with test modules in `tests/suites/`.

## Anti-patterns
- Don't skip closed-loop tests for reductions
- Don't test only happy paths - include edge cases
- Don't ignore clippy warnings
- Don't add inline `mod tests` blocks in `src/` — use `src/unit_tests/` with `#[path]`
116 changes: 8 additions & 108 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ pub struct ConfigIterator {
impl ConfigIterator {
/// Create a new configuration iterator.
pub fn new(num_variables: usize, num_flavors: usize) -> Self {
let total_configs = num_flavors.pow(num_variables as u32);
let current = if num_variables == 0 || num_flavors == 0 {
let total_configs = if num_variables == 0 || num_flavors == 0 {
0
} else {
num_flavors.pow(num_variables as u32)
};
let current = if total_configs == 0 {
None
} else {
Some(vec![0; num_variables])
Expand Down Expand Up @@ -108,109 +112,5 @@ pub fn bits_to_config(bits: &[bool]) -> Vec<usize> {
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_config_iterator_binary() {
let iter = ConfigIterator::new(3, 2);
assert_eq!(iter.total(), 8);

let configs: Vec<_> = iter.collect();
assert_eq!(configs.len(), 8);
assert_eq!(configs[0], vec![0, 0, 0]);
assert_eq!(configs[1], vec![0, 0, 1]);
assert_eq!(configs[2], vec![0, 1, 0]);
assert_eq!(configs[3], vec![0, 1, 1]);
assert_eq!(configs[4], vec![1, 0, 0]);
assert_eq!(configs[5], vec![1, 0, 1]);
assert_eq!(configs[6], vec![1, 1, 0]);
assert_eq!(configs[7], vec![1, 1, 1]);
}

#[test]
fn test_config_iterator_ternary() {
let iter = ConfigIterator::new(2, 3);
assert_eq!(iter.total(), 9);

let configs: Vec<_> = iter.collect();
assert_eq!(configs.len(), 9);
assert_eq!(configs[0], vec![0, 0]);
assert_eq!(configs[1], vec![0, 1]);
assert_eq!(configs[2], vec![0, 2]);
assert_eq!(configs[3], vec![1, 0]);
assert_eq!(configs[8], vec![2, 2]);
}

#[test]
fn test_config_iterator_empty() {
let iter = ConfigIterator::new(0, 2);
assert_eq!(iter.total(), 1);
let configs: Vec<_> = iter.collect();
assert_eq!(configs.len(), 0); // Empty because num_variables is 0
}

#[test]
fn test_config_iterator_single_variable() {
let iter = ConfigIterator::new(1, 4);
assert_eq!(iter.total(), 4);

let configs: Vec<_> = iter.collect();
assert_eq!(configs, vec![vec![0], vec![1], vec![2], vec![3]]);
}

#[test]
fn test_index_to_config() {
assert_eq!(index_to_config(0, 3, 2), vec![0, 0, 0]);
assert_eq!(index_to_config(1, 3, 2), vec![0, 0, 1]);
assert_eq!(index_to_config(7, 3, 2), vec![1, 1, 1]);
assert_eq!(index_to_config(5, 3, 2), vec![1, 0, 1]);
}

#[test]
fn test_config_to_index() {
assert_eq!(config_to_index(&[0, 0, 0], 2), 0);
assert_eq!(config_to_index(&[0, 0, 1], 2), 1);
assert_eq!(config_to_index(&[1, 1, 1], 2), 7);
assert_eq!(config_to_index(&[1, 0, 1], 2), 5);
}

#[test]
fn test_index_config_roundtrip() {
for i in 0..27 {
let config = index_to_config(i, 3, 3);
let back = config_to_index(&config, 3);
assert_eq!(i, back);
}
}

#[test]
fn test_config_to_bits() {
assert_eq!(
config_to_bits(&[0, 1, 0, 1]),
vec![false, true, false, true]
);
assert_eq!(config_to_bits(&[0, 0, 0]), vec![false, false, false]);
assert_eq!(config_to_bits(&[1, 1, 1]), vec![true, true, true]);
}

#[test]
fn test_bits_to_config() {
assert_eq!(
bits_to_config(&[false, true, false, true]),
vec![0, 1, 0, 1]
);
assert_eq!(bits_to_config(&[true, true, true]), vec![1, 1, 1]);
}

#[test]
fn test_exact_size_iterator() {
let mut iter = ConfigIterator::new(3, 2);
assert_eq!(iter.len(), 8);
iter.next();
assert_eq!(iter.len(), 7);
iter.next();
iter.next();
assert_eq!(iter.len(), 5);
}
}
#[path = "unit_tests/config.rs"]
mod tests;
74 changes: 2 additions & 72 deletions src/graph_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,75 +67,5 @@ declare_graph_subtype!(PlanarGraph => SimpleGraph);
declare_graph_subtype!(BipartiteGraph => SimpleGraph);

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_reflexive_subtype() {
fn assert_subtype<A: GraphSubtype<B>, B: GraphMarker>() {}

// Every type is a subtype of itself
assert_subtype::<SimpleGraph, SimpleGraph>();
assert_subtype::<PlanarGraph, PlanarGraph>();
assert_subtype::<UnitDiskGraph, UnitDiskGraph>();
}

#[test]
fn test_subtype_entries_registered() {
let entries: Vec<_> = inventory::iter::<GraphSubtypeEntry>().collect();

// Should have at least 4 entries
assert!(entries.len() >= 4);

// Check specific relationships
assert!(entries
.iter()
.any(|e| e.subtype == "UnitDiskGraph" && e.supertype == "SimpleGraph"));
assert!(entries
.iter()
.any(|e| e.subtype == "PlanarGraph" && e.supertype == "SimpleGraph"));
}

#[test]
fn test_declared_subtypes() {
fn assert_subtype<A: GraphSubtype<B>, B: GraphMarker>() {}

// Declared relationships
assert_subtype::<UnitDiskGraph, PlanarGraph>();
assert_subtype::<UnitDiskGraph, SimpleGraph>();
assert_subtype::<PlanarGraph, SimpleGraph>();
assert_subtype::<BipartiteGraph, SimpleGraph>();
}

#[test]
fn test_graph_type_traits() {
// Test Default
let _: SimpleGraph = Default::default();
let _: PlanarGraph = Default::default();
let _: UnitDiskGraph = Default::default();
let _: BipartiteGraph = Default::default();

// Test Copy (SimpleGraph implements Copy, so no need to clone)
let g = SimpleGraph;
let _g2 = g; // Copy
let g = SimpleGraph;
let _g2 = g;
let _g3 = g; // still usable
}

#[test]
fn test_bipartite_entry_registered() {
let entries: Vec<_> = inventory::iter::<GraphSubtypeEntry>().collect();
assert!(entries
.iter()
.any(|e| e.subtype == "BipartiteGraph" && e.supertype == "SimpleGraph"));
}

#[test]
fn test_unit_disk_to_planar_registered() {
let entries: Vec<_> = inventory::iter::<GraphSubtypeEntry>().collect();
assert!(entries
.iter()
.any(|e| e.subtype == "UnitDiskGraph" && e.supertype == "PlanarGraph"));
}
}
#[path = "unit_tests/graph_types.rs"]
mod tests;
85 changes: 2 additions & 83 deletions src/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,86 +129,5 @@ pub fn write_file<P: AsRef<Path>>(path: P, contents: &str) -> Result<()> {
}

#[cfg(test)]
mod tests {
use super::*;
use crate::models::graph::IndependentSet;
use crate::topology::SimpleGraph;
use std::fs;

#[test]
fn test_to_json() {
let problem = IndependentSet::<SimpleGraph, i32>::new(3, vec![(0, 1), (1, 2)]);
let json = to_json(&problem);
assert!(json.is_ok());
let json = json.unwrap();
assert!(json.contains("graph"));
}

#[test]
fn test_from_json() {
let problem = IndependentSet::<SimpleGraph, i32>::new(3, vec![(0, 1), (1, 2)]);
let json = to_json(&problem).unwrap();
let restored: IndependentSet<SimpleGraph, i32> = from_json(&json).unwrap();
assert_eq!(restored.num_vertices(), 3);
assert_eq!(restored.num_edges(), 2);
}

#[test]
fn test_json_compact() {
let problem = IndependentSet::<SimpleGraph, i32>::new(3, vec![(0, 1)]);
let compact = to_json_compact(&problem).unwrap();
let pretty = to_json(&problem).unwrap();
// Compact should be shorter
assert!(compact.len() < pretty.len());
}

#[test]
fn test_file_roundtrip() {
let problem = IndependentSet::<SimpleGraph, i32>::new(4, vec![(0, 1), (1, 2), (2, 3)]);
let path = "/tmp/test_problem.json";

// Write
write_problem(&problem, path, FileFormat::Json).unwrap();

// Read back
let restored: IndependentSet<SimpleGraph, i32> = read_problem(path, FileFormat::Json).unwrap();
assert_eq!(restored.num_vertices(), 4);
assert_eq!(restored.num_edges(), 3);

// Cleanup
fs::remove_file(path).ok();
}

#[test]
fn test_file_format_from_extension() {
assert_eq!(
FileFormat::from_extension(Path::new("test.json")),
Some(FileFormat::Json)
);
assert_eq!(
FileFormat::from_extension(Path::new("test.JSON")),
Some(FileFormat::Json)
);
assert_eq!(FileFormat::from_extension(Path::new("test.txt")), None);
assert_eq!(FileFormat::from_extension(Path::new("noext")), None);
}

#[test]
fn test_read_write_file() {
let path = "/tmp/test_io.txt";
let contents = "Hello, World!";

write_file(path, contents).unwrap();
let read_back = read_file(path).unwrap();

assert_eq!(read_back, contents);

fs::remove_file(path).ok();
}

#[test]
fn test_invalid_json() {
let result: Result<IndependentSet<SimpleGraph, i32>> = from_json("not valid json");
assert!(result.is_err());
}
}
#[path = "unit_tests/io.rs"]
mod tests;
16 changes: 16 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,19 @@ pub use types::{EnergyMode, LocalConstraint, LocalSolutionSize, ProblemSize, Sol

// Re-export proc macro for reduction registration
pub use problemreductions_macros::reduction;

#[cfg(test)]
#[path = "unit_tests/graph_models.rs"]
mod test_graph_models;
#[cfg(test)]
#[path = "unit_tests/property.rs"]
mod test_property;
#[cfg(test)]
#[path = "unit_tests/reduction_graph.rs"]
mod test_reduction_graph;
#[cfg(test)]
#[path = "unit_tests/trait_consistency.rs"]
mod test_trait_consistency;
#[cfg(test)]
#[path = "unit_tests/unitdiskmapping_algorithms/mod.rs"]
mod test_unitdiskmapping_algorithms;
Loading