1pub mod categories;
23pub mod generator;
24pub mod parser;
25pub mod report;
26pub mod template;
27
28pub use generator::{FalsifyGenerator, GeneratedTest, TargetLanguage};
29pub use parser::SpecParser;
30#[allow(unused_imports)]
31pub use parser::{ParsedRequirement, ParsedSpec};
32#[allow(unused_imports)]
33pub use report::{FalsificationReport, FalsificationSummary, TestOutcome};
34pub use template::FalsificationTemplate;
35#[allow(unused_imports)]
36pub use template::{CategoryTemplate, TestTemplate};
37
38#[derive(Debug)]
42pub struct FalsifyEngine {
43 template: FalsificationTemplate,
45 parser: SpecParser,
47 generator: FalsifyGenerator,
49}
50
51impl FalsifyEngine {
52 pub fn new() -> Self {
54 Self {
55 template: FalsificationTemplate::default(),
56 parser: SpecParser::new(),
57 generator: FalsifyGenerator::new(),
58 }
59 }
60
61 pub fn generate_from_spec(
63 &self,
64 spec_path: &std::path::Path,
65 language: TargetLanguage,
66 ) -> anyhow::Result<GeneratedSuite> {
67 let spec = self.parser.parse_file(spec_path)?;
69
70 let tests = self.generator.generate(&spec, &self.template, language)?;
72
73 Ok(GeneratedSuite {
74 spec_name: spec.name.clone(),
75 language,
76 tests,
77 total_points: self.template.total_points(),
78 })
79 }
80
81 pub fn generate_with_points(
83 &self,
84 spec_path: &std::path::Path,
85 language: TargetLanguage,
86 target_points: u32,
87 ) -> anyhow::Result<GeneratedSuite> {
88 let spec = self.parser.parse_file(spec_path)?;
89 let template = self.template.scale_to_points(target_points);
90 let tests = self.generator.generate(&spec, &template, language)?;
91
92 Ok(GeneratedSuite {
93 spec_name: spec.name.clone(),
94 language,
95 tests,
96 total_points: target_points,
97 })
98 }
99
100 pub fn template(&self) -> &FalsificationTemplate {
102 &self.template
103 }
104}
105
106impl Default for FalsifyEngine {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112#[derive(Debug)]
114pub struct GeneratedSuite {
115 pub spec_name: String,
117 pub language: TargetLanguage,
119 pub tests: Vec<GeneratedTest>,
121 pub total_points: u32,
123}
124
125impl GeneratedSuite {
126 pub fn to_code(&self) -> String {
128 self.generator_code()
129 }
130
131 fn generator_code(&self) -> String {
133 match self.language {
134 TargetLanguage::Rust => self.to_rust(),
135 TargetLanguage::Python => self.to_python(),
136 }
137 }
138
139 fn to_rust(&self) -> String {
141 let mut out = String::new();
142 out.push_str(&format!("//! Falsification Suite: {}\n", self.spec_name));
143 out.push_str(&format!("//! Total Points: {}\n", self.total_points));
144 out.push_str("//! Generated by batuta oracle falsify\n\n");
145 out.push_str("#![cfg(test)]\n\n");
146 out.push_str("use proptest::prelude::*;\n\n");
147
148 let mut current_category = String::new();
150 for test in &self.tests {
151 if test.category != current_category {
152 current_category = test.category.clone();
153 out.push_str(&format!(
154 "\n// {:=<60}\n",
155 format!(" {} ", current_category.to_uppercase())
156 ));
157 }
158
159 out.push_str(&format!("\n/// {}: {}\n", test.id, test.name));
160 out.push_str(&format!("/// Points: {}\n", test.points));
161 out.push_str(&test.code);
162 out.push('\n');
163 }
164
165 out
166 }
167
168 fn to_python(&self) -> String {
170 let mut out = String::new();
171 out.push_str(&format!("\"\"\"Falsification Suite: {}\n\n", self.spec_name));
172 out.push_str(&format!("Total Points: {}\n", self.total_points));
173 out.push_str("Generated by batuta oracle falsify\n\"\"\"\n\n");
174 out.push_str("import pytest\n");
175 out.push_str("from hypothesis import given, strategies as st\n\n");
176
177 let mut current_category = String::new();
179 for test in &self.tests {
180 if test.category != current_category {
181 current_category = test.category.clone();
182 out.push_str(&format!(
183 "\n# {:=<60}\n",
184 format!(" {} ", current_category.to_uppercase())
185 ));
186 }
187
188 out.push_str(&format!("\ndef test_{}():\n", test.id.to_lowercase().replace('-', "_")));
189 out.push_str(&format!(" \"\"\"{}: {}\n", test.id, test.name));
190 out.push_str(&format!(" Points: {}\n \"\"\"\n", test.points));
191 out.push_str(&test.code);
192 out.push('\n');
193 }
194
195 out
196 }
197
198 pub fn tests_by_category(&self) -> std::collections::HashMap<String, Vec<&GeneratedTest>> {
200 let mut map = std::collections::HashMap::new();
201 for test in &self.tests {
202 map.entry(test.category.clone()).or_insert_with(Vec::new).push(test);
203 }
204 map
205 }
206
207 pub fn summary(&self) -> SuiteSummary {
209 let mut points_by_category = std::collections::HashMap::new();
210 for test in &self.tests {
211 *points_by_category.entry(test.category.clone()).or_insert(0u32) += test.points;
212 }
213
214 SuiteSummary {
215 spec_name: self.spec_name.clone(),
216 total_tests: self.tests.len(),
217 total_points: self.total_points,
218 points_by_category,
219 }
220 }
221}
222
223#[derive(Debug)]
225pub struct SuiteSummary {
226 pub spec_name: String,
227 pub total_tests: usize,
228 pub total_points: u32,
229 pub points_by_category: std::collections::HashMap<String, u32>,
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use template::TestSeverity;
236
237 #[test]
238 fn test_falsify_engine_creation() {
239 let engine = FalsifyEngine::new();
240 assert_eq!(engine.template.total_points(), 100);
241 }
242
243 #[test]
244 fn test_template_categories() {
245 let engine = FalsifyEngine::new();
246 let template = engine.template();
247 assert!(!template.categories.is_empty());
248 }
249
250 #[test]
251 fn test_falsify_engine_default() {
252 let engine = FalsifyEngine::default();
253 assert_eq!(engine.template.total_points(), 100);
254 }
255
256 #[test]
257 fn test_generated_suite_to_code_rust() {
258 let suite = GeneratedSuite {
259 spec_name: "test-spec".to_string(),
260 language: TargetLanguage::Rust,
261 tests: vec![GeneratedTest {
262 id: "BC-001".to_string(),
263 name: "Boundary test".to_string(),
264 category: "boundary".to_string(),
265 points: 4,
266 severity: TestSeverity::High,
267 code: "#[test]\nfn test_boundary() {}".to_string(),
268 }],
269 total_points: 100,
270 };
271 let code = suite.to_code();
272 assert!(code.contains("test-spec"));
273 assert!(code.contains("BC-001"));
274 assert!(code.contains("proptest"));
275 }
276
277 #[test]
278 fn test_generated_suite_to_code_python() {
279 let suite = GeneratedSuite {
280 spec_name: "test-spec".to_string(),
281 language: TargetLanguage::Python,
282 tests: vec![GeneratedTest {
283 id: "BC-001".to_string(),
284 name: "Boundary test".to_string(),
285 category: "boundary".to_string(),
286 points: 4,
287 severity: TestSeverity::High,
288 code: " pass".to_string(),
289 }],
290 total_points: 100,
291 };
292 let code = suite.to_code();
293 assert!(code.contains("test-spec"));
294 assert!(code.contains("pytest"));
295 assert!(code.contains("hypothesis"));
296 }
297
298 #[test]
299 fn test_generated_suite_tests_by_category() {
300 let suite = GeneratedSuite {
301 spec_name: "test".to_string(),
302 language: TargetLanguage::Rust,
303 tests: vec![
304 GeneratedTest {
305 id: "BC-001".to_string(),
306 name: "Test 1".to_string(),
307 category: "boundary".to_string(),
308 points: 4,
309 severity: TestSeverity::High,
310 code: String::new(),
311 },
312 GeneratedTest {
313 id: "INV-001".to_string(),
314 name: "Test 2".to_string(),
315 category: "invariant".to_string(),
316 points: 5,
317 severity: TestSeverity::Critical,
318 code: String::new(),
319 },
320 GeneratedTest {
321 id: "BC-002".to_string(),
322 name: "Test 3".to_string(),
323 category: "boundary".to_string(),
324 points: 4,
325 severity: TestSeverity::Medium,
326 code: String::new(),
327 },
328 ],
329 total_points: 13,
330 };
331 let by_cat = suite.tests_by_category();
332 assert_eq!(by_cat.len(), 2);
333 assert_eq!(by_cat.get("boundary").expect("key not found").len(), 2);
334 assert_eq!(by_cat.get("invariant").expect("key not found").len(), 1);
335 }
336
337 #[test]
338 fn test_generated_suite_summary() {
339 let suite = GeneratedSuite {
340 spec_name: "my-spec".to_string(),
341 language: TargetLanguage::Rust,
342 tests: vec![
343 GeneratedTest {
344 id: "BC-001".to_string(),
345 name: "Test 1".to_string(),
346 category: "boundary".to_string(),
347 points: 4,
348 severity: TestSeverity::High,
349 code: String::new(),
350 },
351 GeneratedTest {
352 id: "BC-002".to_string(),
353 name: "Test 2".to_string(),
354 category: "boundary".to_string(),
355 points: 4,
356 severity: TestSeverity::High,
357 code: String::new(),
358 },
359 ],
360 total_points: 8,
361 };
362 let summary = suite.summary();
363 assert_eq!(summary.spec_name, "my-spec");
364 assert_eq!(summary.total_tests, 2);
365 assert_eq!(summary.total_points, 8);
366 assert_eq!(*summary.points_by_category.get("boundary").expect("key not found"), 8);
367 }
368
369 #[test]
370 fn test_suite_summary_fields() {
371 let summary = SuiteSummary {
372 spec_name: "test".to_string(),
373 total_tests: 10,
374 total_points: 100,
375 points_by_category: std::collections::HashMap::new(),
376 };
377 assert_eq!(summary.spec_name, "test");
378 assert_eq!(summary.total_tests, 10);
379 assert_eq!(summary.total_points, 100);
380 }
381
382 #[test]
383 fn test_generated_suite_rust_code_format() {
384 let suite = GeneratedSuite {
385 spec_name: "spec".to_string(),
386 language: TargetLanguage::Rust,
387 tests: vec![
388 GeneratedTest {
389 id: "BC-001".to_string(),
390 name: "First".to_string(),
391 category: "boundary".to_string(),
392 points: 4,
393 severity: TestSeverity::High,
394 code: "// code".to_string(),
395 },
396 GeneratedTest {
397 id: "INV-001".to_string(),
398 name: "Second".to_string(),
399 category: "invariant".to_string(),
400 points: 5,
401 severity: TestSeverity::Critical,
402 code: "// more".to_string(),
403 },
404 ],
405 total_points: 9,
406 };
407 let code = suite.to_code();
408 assert!(code.contains("BOUNDARY"));
410 assert!(code.contains("INVARIANT"));
411 assert!(code.contains("#![cfg(test)]"));
413 }
414
415 #[test]
416 fn test_generated_suite_python_code_format() {
417 let suite = GeneratedSuite {
418 spec_name: "spec".to_string(),
419 language: TargetLanguage::Python,
420 tests: vec![GeneratedTest {
421 id: "BC-001".to_string(),
422 name: "Test".to_string(),
423 category: "boundary".to_string(),
424 points: 4,
425 severity: TestSeverity::High,
426 code: " assert True".to_string(),
427 }],
428 total_points: 4,
429 };
430 let code = suite.to_code();
431 assert!(code.contains("def test_bc_001"));
433 assert!(code.contains("BC-001: Test"));
435 }
436
437 #[test]
442 fn test_generate_from_spec_with_file() {
443 let dir = tempfile::TempDir::new().expect("tempdir creation failed");
444 let spec_file = dir.path().join("test-spec.md");
445 std::fs::write(
446 &spec_file,
447 r#"# My Test Spec
448module: my_module
449
450## Requirements
451- MUST handle empty input gracefully
452- SHOULD return error on invalid data
453- The function MUST NOT panic on any input
454
455## Functions
456fn process_data(input: &[u8]) -> Result<Vec<u8>, Error>
457
458## Types
459struct DataProcessor { buffer: Vec<u8> }
460"#,
461 )
462 .expect("unexpected failure");
463
464 let engine = FalsifyEngine::new();
465 let suite = engine
466 .generate_from_spec(&spec_file, TargetLanguage::Rust)
467 .expect("unexpected failure");
468
469 assert_eq!(suite.spec_name, "test-spec");
470 assert_eq!(suite.language, TargetLanguage::Rust);
471 assert!(!suite.tests.is_empty());
472 assert_eq!(suite.total_points, 100);
473
474 let code = suite.to_code();
476 assert!(code.contains("test-spec"));
477 assert!(code.contains("my_module"));
478 }
479
480 #[test]
481 fn test_generate_from_spec_python() {
482 let dir = tempfile::TempDir::new().expect("tempdir creation failed");
483 let spec_file = dir.path().join("py-spec.md");
484 std::fs::write(
485 &spec_file,
486 "module: test_mod\n- MUST handle empty input\n- SHOULD validate",
487 )
488 .expect("unexpected failure");
489
490 let engine = FalsifyEngine::new();
491 let suite = engine
492 .generate_from_spec(&spec_file, TargetLanguage::Python)
493 .expect("unexpected failure");
494
495 assert_eq!(suite.language, TargetLanguage::Python);
496 let code = suite.to_code();
497 assert!(code.contains("pytest"));
498 assert!(code.contains("hypothesis"));
499 }
500
501 #[test]
502 fn test_generate_from_spec_nonexistent_file() {
503 let engine = FalsifyEngine::new();
504 let result = engine
505 .generate_from_spec(std::path::Path::new("/nonexistent/file.md"), TargetLanguage::Rust);
506 assert!(result.is_err());
507 }
508
509 #[test]
510 fn test_generate_with_points() {
511 let dir = tempfile::TempDir::new().expect("tempdir creation failed");
512 let spec_file = dir.path().join("points-spec.md");
513 std::fs::write(
514 &spec_file,
515 "module: scaled_module\n- MUST work correctly\n- SHOULD be fast",
516 )
517 .expect("unexpected failure");
518
519 let engine = FalsifyEngine::new();
520 let suite = engine
521 .generate_with_points(&spec_file, TargetLanguage::Rust, 50)
522 .expect("unexpected failure");
523
524 assert_eq!(suite.spec_name, "points-spec");
525 assert_eq!(suite.total_points, 50);
526 assert!(!suite.tests.is_empty());
527 }
528
529 #[test]
530 fn test_generate_with_points_200() {
531 let dir = tempfile::TempDir::new().expect("tempdir creation failed");
532 let spec_file = dir.path().join("large-spec.md");
533 std::fs::write(&spec_file, "module: large_mod\n- MUST handle edge cases")
534 .expect("fs write failed");
535
536 let engine = FalsifyEngine::new();
537 let suite = engine
538 .generate_with_points(&spec_file, TargetLanguage::Python, 200)
539 .expect("unexpected failure");
540
541 assert_eq!(suite.total_points, 200);
542 assert_eq!(suite.language, TargetLanguage::Python);
543 let code = suite.to_code();
544 assert!(code.contains("pytest"));
545 assert!(!suite.tests.is_empty());
546 }
547
548 #[test]
549 fn test_generate_with_points_100_no_scaling() {
550 let dir = tempfile::TempDir::new().expect("tempdir creation failed");
551 let spec_file = dir.path().join("same-spec.md");
552 std::fs::write(&spec_file, "module: same_mod\n- MUST be tested").expect("fs write failed");
553
554 let engine = FalsifyEngine::new();
555 let suite = engine
556 .generate_with_points(&spec_file, TargetLanguage::Rust, 100)
557 .expect("unexpected failure");
558
559 assert_eq!(suite.total_points, 100);
561 }
562
563 #[test]
564 fn test_generate_with_points_nonexistent_file() {
565 let engine = FalsifyEngine::new();
566 let result = engine.generate_with_points(
567 std::path::Path::new("/nonexistent/spec.md"),
568 TargetLanguage::Rust,
569 50,
570 );
571 assert!(result.is_err());
572 }
573
574 #[test]
579 fn test_rust_code_multiple_categories_same_category() {
580 let suite = GeneratedSuite {
581 spec_name: "multi".to_string(),
582 language: TargetLanguage::Rust,
583 tests: vec![
584 GeneratedTest {
585 id: "BC-001".to_string(),
586 name: "First boundary".to_string(),
587 category: "boundary".to_string(),
588 points: 4,
589 severity: TestSeverity::High,
590 code: "// bc1".to_string(),
591 },
592 GeneratedTest {
593 id: "BC-002".to_string(),
594 name: "Second boundary".to_string(),
595 category: "boundary".to_string(),
596 points: 4,
597 severity: TestSeverity::Medium,
598 code: "// bc2".to_string(),
599 },
600 ],
601 total_points: 8,
602 };
603 let code = suite.to_code();
604 let boundary_count = code.matches("BOUNDARY").count();
606 assert_eq!(boundary_count, 1, "BOUNDARY header should appear once");
607 assert!(code.contains("BC-001"));
608 assert!(code.contains("BC-002"));
609 assert!(code.contains("Points: 4"));
610 }
611
612 #[test]
613 fn test_python_code_multiple_categories() {
614 let suite = GeneratedSuite {
615 spec_name: "pytest".to_string(),
616 language: TargetLanguage::Python,
617 tests: vec![
618 GeneratedTest {
619 id: "BC-001".to_string(),
620 name: "Boundary".to_string(),
621 category: "boundary".to_string(),
622 points: 4,
623 severity: TestSeverity::High,
624 code: " pass".to_string(),
625 },
626 GeneratedTest {
627 id: "INV-001".to_string(),
628 name: "Invariant".to_string(),
629 category: "invariant".to_string(),
630 points: 5,
631 severity: TestSeverity::Critical,
632 code: " pass".to_string(),
633 },
634 GeneratedTest {
635 id: "INV-002".to_string(),
636 name: "Invariant2".to_string(),
637 category: "invariant".to_string(),
638 points: 5,
639 severity: TestSeverity::High,
640 code: " pass".to_string(),
641 },
642 ],
643 total_points: 14,
644 };
645 let code = suite.to_code();
646 assert!(code.contains("BOUNDARY"));
647 assert!(code.contains("INVARIANT"));
648 assert!(code.contains("def test_bc_001"));
649 assert!(code.contains("def test_inv_001"));
650 assert!(code.contains("def test_inv_002"));
651 assert!(code.contains("Points: 4"));
653 assert!(code.contains("Points: 5"));
654 }
655
656 #[test]
657 fn test_suite_summary_multiple_categories() {
658 let suite = GeneratedSuite {
659 spec_name: "summary-test".to_string(),
660 language: TargetLanguage::Rust,
661 tests: vec![
662 GeneratedTest {
663 id: "BC-001".to_string(),
664 name: "T1".to_string(),
665 category: "boundary".to_string(),
666 points: 4,
667 severity: TestSeverity::High,
668 code: String::new(),
669 },
670 GeneratedTest {
671 id: "NUM-001".to_string(),
672 name: "T2".to_string(),
673 category: "numerical".to_string(),
674 points: 7,
675 severity: TestSeverity::High,
676 code: String::new(),
677 },
678 GeneratedTest {
679 id: "NUM-002".to_string(),
680 name: "T3".to_string(),
681 category: "numerical".to_string(),
682 points: 6,
683 severity: TestSeverity::Medium,
684 code: String::new(),
685 },
686 ],
687 total_points: 17,
688 };
689 let summary = suite.summary();
690 assert_eq!(summary.total_tests, 3);
691 assert_eq!(summary.total_points, 17);
692 assert_eq!(*summary.points_by_category.get("boundary").expect("key not found"), 4);
693 assert_eq!(*summary.points_by_category.get("numerical").expect("key not found"), 13);
694 }
695
696 #[test]
697 fn test_generated_suite_empty_tests() {
698 let suite = GeneratedSuite {
699 spec_name: "empty".to_string(),
700 language: TargetLanguage::Rust,
701 tests: vec![],
702 total_points: 0,
703 };
704 let code = suite.to_code();
705 assert!(code.contains("empty"));
706 assert!(code.contains("Total Points: 0"));
707 let summary = suite.summary();
708 assert_eq!(summary.total_tests, 0);
709 assert!(summary.points_by_category.is_empty());
710 }
711
712 #[test]
713 fn test_engine_template_accessor() {
714 let engine = FalsifyEngine::new();
715 let template = engine.template();
716 assert_eq!(template.total_points(), 100);
717 assert!(!template.categories.is_empty());
718 assert!(template.get_category("boundary").is_some());
720 assert!(template.get_category("nonexistent").is_none());
721 }
722}