pub mod categories;
pub mod generator;
pub mod parser;
pub mod report;
pub mod template;
pub use generator::{FalsifyGenerator, GeneratedTest, TargetLanguage};
pub use parser::SpecParser;
#[allow(unused_imports)]
pub use parser::{ParsedRequirement, ParsedSpec};
#[allow(unused_imports)]
pub use report::{FalsificationReport, FalsificationSummary, TestOutcome};
pub use template::FalsificationTemplate;
#[allow(unused_imports)]
pub use template::{CategoryTemplate, TestTemplate};
#[derive(Debug)]
pub struct FalsifyEngine {
template: FalsificationTemplate,
parser: SpecParser,
generator: FalsifyGenerator,
}
impl FalsifyEngine {
pub fn new() -> Self {
Self {
template: FalsificationTemplate::default(),
parser: SpecParser::new(),
generator: FalsifyGenerator::new(),
}
}
pub fn generate_from_spec(
&self,
spec_path: &std::path::Path,
language: TargetLanguage,
) -> anyhow::Result<GeneratedSuite> {
let spec = self.parser.parse_file(spec_path)?;
let tests = self.generator.generate(&spec, &self.template, language)?;
Ok(GeneratedSuite {
spec_name: spec.name.clone(),
language,
tests,
total_points: self.template.total_points(),
})
}
pub fn generate_with_points(
&self,
spec_path: &std::path::Path,
language: TargetLanguage,
target_points: u32,
) -> anyhow::Result<GeneratedSuite> {
let spec = self.parser.parse_file(spec_path)?;
let template = self.template.scale_to_points(target_points);
let tests = self.generator.generate(&spec, &template, language)?;
Ok(GeneratedSuite {
spec_name: spec.name.clone(),
language,
tests,
total_points: target_points,
})
}
pub fn template(&self) -> &FalsificationTemplate {
&self.template
}
}
impl Default for FalsifyEngine {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct GeneratedSuite {
pub spec_name: String,
pub language: TargetLanguage,
pub tests: Vec<GeneratedTest>,
pub total_points: u32,
}
impl GeneratedSuite {
pub fn to_code(&self) -> String {
self.generator_code()
}
fn generator_code(&self) -> String {
match self.language {
TargetLanguage::Rust => self.to_rust(),
TargetLanguage::Python => self.to_python(),
}
}
fn to_rust(&self) -> String {
let mut out = String::new();
out.push_str(&format!("//! Falsification Suite: {}\n", self.spec_name));
out.push_str(&format!("//! Total Points: {}\n", self.total_points));
out.push_str("//! Generated by batuta oracle falsify\n\n");
out.push_str("#![cfg(test)]\n\n");
out.push_str("use proptest::prelude::*;\n\n");
let mut current_category = String::new();
for test in &self.tests {
if test.category != current_category {
current_category = test.category.clone();
out.push_str(&format!(
"\n// {:=<60}\n",
format!(" {} ", current_category.to_uppercase())
));
}
out.push_str(&format!("\n/// {}: {}\n", test.id, test.name));
out.push_str(&format!("/// Points: {}\n", test.points));
out.push_str(&test.code);
out.push('\n');
}
out
}
fn to_python(&self) -> String {
let mut out = String::new();
out.push_str(&format!("\"\"\"Falsification Suite: {}\n\n", self.spec_name));
out.push_str(&format!("Total Points: {}\n", self.total_points));
out.push_str("Generated by batuta oracle falsify\n\"\"\"\n\n");
out.push_str("import pytest\n");
out.push_str("from hypothesis import given, strategies as st\n\n");
let mut current_category = String::new();
for test in &self.tests {
if test.category != current_category {
current_category = test.category.clone();
out.push_str(&format!(
"\n# {:=<60}\n",
format!(" {} ", current_category.to_uppercase())
));
}
out.push_str(&format!("\ndef test_{}():\n", test.id.to_lowercase().replace('-', "_")));
out.push_str(&format!(" \"\"\"{}: {}\n", test.id, test.name));
out.push_str(&format!(" Points: {}\n \"\"\"\n", test.points));
out.push_str(&test.code);
out.push('\n');
}
out
}
pub fn tests_by_category(&self) -> std::collections::HashMap<String, Vec<&GeneratedTest>> {
let mut map = std::collections::HashMap::new();
for test in &self.tests {
map.entry(test.category.clone()).or_insert_with(Vec::new).push(test);
}
map
}
pub fn summary(&self) -> SuiteSummary {
let mut points_by_category = std::collections::HashMap::new();
for test in &self.tests {
*points_by_category.entry(test.category.clone()).or_insert(0u32) += test.points;
}
SuiteSummary {
spec_name: self.spec_name.clone(),
total_tests: self.tests.len(),
total_points: self.total_points,
points_by_category,
}
}
}
#[derive(Debug)]
pub struct SuiteSummary {
pub spec_name: String,
pub total_tests: usize,
pub total_points: u32,
pub points_by_category: std::collections::HashMap<String, u32>,
}
#[cfg(test)]
mod tests {
use super::*;
use template::TestSeverity;
#[test]
fn test_falsify_engine_creation() {
let engine = FalsifyEngine::new();
assert_eq!(engine.template.total_points(), 100);
}
#[test]
fn test_template_categories() {
let engine = FalsifyEngine::new();
let template = engine.template();
assert!(!template.categories.is_empty());
}
#[test]
fn test_falsify_engine_default() {
let engine = FalsifyEngine::default();
assert_eq!(engine.template.total_points(), 100);
}
#[test]
fn test_generated_suite_to_code_rust() {
let suite = GeneratedSuite {
spec_name: "test-spec".to_string(),
language: TargetLanguage::Rust,
tests: vec![GeneratedTest {
id: "BC-001".to_string(),
name: "Boundary test".to_string(),
category: "boundary".to_string(),
points: 4,
severity: TestSeverity::High,
code: "#[test]\nfn test_boundary() {}".to_string(),
}],
total_points: 100,
};
let code = suite.to_code();
assert!(code.contains("test-spec"));
assert!(code.contains("BC-001"));
assert!(code.contains("proptest"));
}
#[test]
fn test_generated_suite_to_code_python() {
let suite = GeneratedSuite {
spec_name: "test-spec".to_string(),
language: TargetLanguage::Python,
tests: vec![GeneratedTest {
id: "BC-001".to_string(),
name: "Boundary test".to_string(),
category: "boundary".to_string(),
points: 4,
severity: TestSeverity::High,
code: " pass".to_string(),
}],
total_points: 100,
};
let code = suite.to_code();
assert!(code.contains("test-spec"));
assert!(code.contains("pytest"));
assert!(code.contains("hypothesis"));
}
#[test]
fn test_generated_suite_tests_by_category() {
let suite = GeneratedSuite {
spec_name: "test".to_string(),
language: TargetLanguage::Rust,
tests: vec![
GeneratedTest {
id: "BC-001".to_string(),
name: "Test 1".to_string(),
category: "boundary".to_string(),
points: 4,
severity: TestSeverity::High,
code: String::new(),
},
GeneratedTest {
id: "INV-001".to_string(),
name: "Test 2".to_string(),
category: "invariant".to_string(),
points: 5,
severity: TestSeverity::Critical,
code: String::new(),
},
GeneratedTest {
id: "BC-002".to_string(),
name: "Test 3".to_string(),
category: "boundary".to_string(),
points: 4,
severity: TestSeverity::Medium,
code: String::new(),
},
],
total_points: 13,
};
let by_cat = suite.tests_by_category();
assert_eq!(by_cat.len(), 2);
assert_eq!(by_cat.get("boundary").expect("key not found").len(), 2);
assert_eq!(by_cat.get("invariant").expect("key not found").len(), 1);
}
#[test]
fn test_generated_suite_summary() {
let suite = GeneratedSuite {
spec_name: "my-spec".to_string(),
language: TargetLanguage::Rust,
tests: vec![
GeneratedTest {
id: "BC-001".to_string(),
name: "Test 1".to_string(),
category: "boundary".to_string(),
points: 4,
severity: TestSeverity::High,
code: String::new(),
},
GeneratedTest {
id: "BC-002".to_string(),
name: "Test 2".to_string(),
category: "boundary".to_string(),
points: 4,
severity: TestSeverity::High,
code: String::new(),
},
],
total_points: 8,
};
let summary = suite.summary();
assert_eq!(summary.spec_name, "my-spec");
assert_eq!(summary.total_tests, 2);
assert_eq!(summary.total_points, 8);
assert_eq!(*summary.points_by_category.get("boundary").expect("key not found"), 8);
}
#[test]
fn test_suite_summary_fields() {
let summary = SuiteSummary {
spec_name: "test".to_string(),
total_tests: 10,
total_points: 100,
points_by_category: std::collections::HashMap::new(),
};
assert_eq!(summary.spec_name, "test");
assert_eq!(summary.total_tests, 10);
assert_eq!(summary.total_points, 100);
}
#[test]
fn test_generated_suite_rust_code_format() {
let suite = GeneratedSuite {
spec_name: "spec".to_string(),
language: TargetLanguage::Rust,
tests: vec![
GeneratedTest {
id: "BC-001".to_string(),
name: "First".to_string(),
category: "boundary".to_string(),
points: 4,
severity: TestSeverity::High,
code: "// code".to_string(),
},
GeneratedTest {
id: "INV-001".to_string(),
name: "Second".to_string(),
category: "invariant".to_string(),
points: 5,
severity: TestSeverity::Critical,
code: "// more".to_string(),
},
],
total_points: 9,
};
let code = suite.to_code();
assert!(code.contains("BOUNDARY"));
assert!(code.contains("INVARIANT"));
assert!(code.contains("#![cfg(test)]"));
}
#[test]
fn test_generated_suite_python_code_format() {
let suite = GeneratedSuite {
spec_name: "spec".to_string(),
language: TargetLanguage::Python,
tests: vec![GeneratedTest {
id: "BC-001".to_string(),
name: "Test".to_string(),
category: "boundary".to_string(),
points: 4,
severity: TestSeverity::High,
code: " assert True".to_string(),
}],
total_points: 4,
};
let code = suite.to_code();
assert!(code.contains("def test_bc_001"));
assert!(code.contains("BC-001: Test"));
}
#[test]
fn test_generate_from_spec_with_file() {
let dir = tempfile::TempDir::new().expect("tempdir creation failed");
let spec_file = dir.path().join("test-spec.md");
std::fs::write(
&spec_file,
r#"# My Test Spec
module: my_module
## Requirements
- MUST handle empty input gracefully
- SHOULD return error on invalid data
- The function MUST NOT panic on any input
## Functions
fn process_data(input: &[u8]) -> Result<Vec<u8>, Error>
## Types
struct DataProcessor { buffer: Vec<u8> }
"#,
)
.expect("unexpected failure");
let engine = FalsifyEngine::new();
let suite = engine
.generate_from_spec(&spec_file, TargetLanguage::Rust)
.expect("unexpected failure");
assert_eq!(suite.spec_name, "test-spec");
assert_eq!(suite.language, TargetLanguage::Rust);
assert!(!suite.tests.is_empty());
assert_eq!(suite.total_points, 100);
let code = suite.to_code();
assert!(code.contains("test-spec"));
assert!(code.contains("my_module"));
}
#[test]
fn test_generate_from_spec_python() {
let dir = tempfile::TempDir::new().expect("tempdir creation failed");
let spec_file = dir.path().join("py-spec.md");
std::fs::write(
&spec_file,
"module: test_mod\n- MUST handle empty input\n- SHOULD validate",
)
.expect("unexpected failure");
let engine = FalsifyEngine::new();
let suite = engine
.generate_from_spec(&spec_file, TargetLanguage::Python)
.expect("unexpected failure");
assert_eq!(suite.language, TargetLanguage::Python);
let code = suite.to_code();
assert!(code.contains("pytest"));
assert!(code.contains("hypothesis"));
}
#[test]
fn test_generate_from_spec_nonexistent_file() {
let engine = FalsifyEngine::new();
let result = engine
.generate_from_spec(std::path::Path::new("/nonexistent/file.md"), TargetLanguage::Rust);
assert!(result.is_err());
}
#[test]
fn test_generate_with_points() {
let dir = tempfile::TempDir::new().expect("tempdir creation failed");
let spec_file = dir.path().join("points-spec.md");
std::fs::write(
&spec_file,
"module: scaled_module\n- MUST work correctly\n- SHOULD be fast",
)
.expect("unexpected failure");
let engine = FalsifyEngine::new();
let suite = engine
.generate_with_points(&spec_file, TargetLanguage::Rust, 50)
.expect("unexpected failure");
assert_eq!(suite.spec_name, "points-spec");
assert_eq!(suite.total_points, 50);
assert!(!suite.tests.is_empty());
}
#[test]
fn test_generate_with_points_200() {
let dir = tempfile::TempDir::new().expect("tempdir creation failed");
let spec_file = dir.path().join("large-spec.md");
std::fs::write(&spec_file, "module: large_mod\n- MUST handle edge cases")
.expect("fs write failed");
let engine = FalsifyEngine::new();
let suite = engine
.generate_with_points(&spec_file, TargetLanguage::Python, 200)
.expect("unexpected failure");
assert_eq!(suite.total_points, 200);
assert_eq!(suite.language, TargetLanguage::Python);
let code = suite.to_code();
assert!(code.contains("pytest"));
assert!(!suite.tests.is_empty());
}
#[test]
fn test_generate_with_points_100_no_scaling() {
let dir = tempfile::TempDir::new().expect("tempdir creation failed");
let spec_file = dir.path().join("same-spec.md");
std::fs::write(&spec_file, "module: same_mod\n- MUST be tested").expect("fs write failed");
let engine = FalsifyEngine::new();
let suite = engine
.generate_with_points(&spec_file, TargetLanguage::Rust, 100)
.expect("unexpected failure");
assert_eq!(suite.total_points, 100);
}
#[test]
fn test_generate_with_points_nonexistent_file() {
let engine = FalsifyEngine::new();
let result = engine.generate_with_points(
std::path::Path::new("/nonexistent/spec.md"),
TargetLanguage::Rust,
50,
);
assert!(result.is_err());
}
#[test]
fn test_rust_code_multiple_categories_same_category() {
let suite = GeneratedSuite {
spec_name: "multi".to_string(),
language: TargetLanguage::Rust,
tests: vec![
GeneratedTest {
id: "BC-001".to_string(),
name: "First boundary".to_string(),
category: "boundary".to_string(),
points: 4,
severity: TestSeverity::High,
code: "// bc1".to_string(),
},
GeneratedTest {
id: "BC-002".to_string(),
name: "Second boundary".to_string(),
category: "boundary".to_string(),
points: 4,
severity: TestSeverity::Medium,
code: "// bc2".to_string(),
},
],
total_points: 8,
};
let code = suite.to_code();
let boundary_count = code.matches("BOUNDARY").count();
assert_eq!(boundary_count, 1, "BOUNDARY header should appear once");
assert!(code.contains("BC-001"));
assert!(code.contains("BC-002"));
assert!(code.contains("Points: 4"));
}
#[test]
fn test_python_code_multiple_categories() {
let suite = GeneratedSuite {
spec_name: "pytest".to_string(),
language: TargetLanguage::Python,
tests: vec![
GeneratedTest {
id: "BC-001".to_string(),
name: "Boundary".to_string(),
category: "boundary".to_string(),
points: 4,
severity: TestSeverity::High,
code: " pass".to_string(),
},
GeneratedTest {
id: "INV-001".to_string(),
name: "Invariant".to_string(),
category: "invariant".to_string(),
points: 5,
severity: TestSeverity::Critical,
code: " pass".to_string(),
},
GeneratedTest {
id: "INV-002".to_string(),
name: "Invariant2".to_string(),
category: "invariant".to_string(),
points: 5,
severity: TestSeverity::High,
code: " pass".to_string(),
},
],
total_points: 14,
};
let code = suite.to_code();
assert!(code.contains("BOUNDARY"));
assert!(code.contains("INVARIANT"));
assert!(code.contains("def test_bc_001"));
assert!(code.contains("def test_inv_001"));
assert!(code.contains("def test_inv_002"));
assert!(code.contains("Points: 4"));
assert!(code.contains("Points: 5"));
}
#[test]
fn test_suite_summary_multiple_categories() {
let suite = GeneratedSuite {
spec_name: "summary-test".to_string(),
language: TargetLanguage::Rust,
tests: vec![
GeneratedTest {
id: "BC-001".to_string(),
name: "T1".to_string(),
category: "boundary".to_string(),
points: 4,
severity: TestSeverity::High,
code: String::new(),
},
GeneratedTest {
id: "NUM-001".to_string(),
name: "T2".to_string(),
category: "numerical".to_string(),
points: 7,
severity: TestSeverity::High,
code: String::new(),
},
GeneratedTest {
id: "NUM-002".to_string(),
name: "T3".to_string(),
category: "numerical".to_string(),
points: 6,
severity: TestSeverity::Medium,
code: String::new(),
},
],
total_points: 17,
};
let summary = suite.summary();
assert_eq!(summary.total_tests, 3);
assert_eq!(summary.total_points, 17);
assert_eq!(*summary.points_by_category.get("boundary").expect("key not found"), 4);
assert_eq!(*summary.points_by_category.get("numerical").expect("key not found"), 13);
}
#[test]
fn test_generated_suite_empty_tests() {
let suite = GeneratedSuite {
spec_name: "empty".to_string(),
language: TargetLanguage::Rust,
tests: vec![],
total_points: 0,
};
let code = suite.to_code();
assert!(code.contains("empty"));
assert!(code.contains("Total Points: 0"));
let summary = suite.summary();
assert_eq!(summary.total_tests, 0);
assert!(summary.points_by_category.is_empty());
}
#[test]
fn test_engine_template_accessor() {
let engine = FalsifyEngine::new();
let template = engine.template();
assert_eq!(template.total_points(), 100);
assert!(!template.categories.is_empty());
assert!(template.get_category("boundary").is_some());
assert!(template.get_category("nonexistent").is_none());
}
}