Skip to main content

batuta/oracle/falsify/
generator.rs

1//! Test Generator
2//!
3//! Generates Rust and Python test code from specifications and templates.
4
5use super::parser::ParsedSpec;
6use super::template::{FalsificationTemplate, TestSeverity, TestTemplate};
7use serde::{Deserialize, Serialize};
8
9/// Target language for generated tests
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum TargetLanguage {
12    Rust,
13    Python,
14}
15
16/// A generated test
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct GeneratedTest {
19    /// Test ID (e.g., "BC-001")
20    pub id: String,
21    /// Test name
22    pub name: String,
23    /// Category
24    pub category: String,
25    /// Point value
26    pub points: u32,
27    /// Severity
28    pub severity: TestSeverity,
29    /// Generated code
30    pub code: String,
31}
32
33/// Test generator
34#[derive(Debug)]
35pub struct FalsifyGenerator {
36    /// Module name placeholder
37    module_placeholder: String,
38    /// Function name placeholder
39    function_placeholder: String,
40    /// Type placeholder
41    type_placeholder: String,
42}
43
44impl FalsifyGenerator {
45    /// Create a new generator
46    pub fn new() -> Self {
47        Self {
48            module_placeholder: "{{module}}".to_string(),
49            function_placeholder: "{{function}}".to_string(),
50            type_placeholder: "{{type}}".to_string(),
51        }
52    }
53
54    /// Generate tests from spec and template
55    pub fn generate(
56        &self,
57        spec: &ParsedSpec,
58        template: &FalsificationTemplate,
59        language: TargetLanguage,
60    ) -> anyhow::Result<Vec<GeneratedTest>> {
61        let mut tests = Vec::new();
62
63        for category in &template.categories {
64            for test_template in &category.tests {
65                let code = self.generate_test_code(spec, test_template, language)?;
66
67                tests.push(GeneratedTest {
68                    id: test_template.id.clone(),
69                    name: test_template.name.clone(),
70                    category: category.name.clone(),
71                    points: test_template.points,
72                    severity: test_template.severity,
73                    code,
74                });
75            }
76        }
77
78        Ok(tests)
79    }
80
81    /// Generate code for a single test
82    fn generate_test_code(
83        &self,
84        spec: &ParsedSpec,
85        template: &TestTemplate,
86        language: TargetLanguage,
87    ) -> anyhow::Result<String> {
88        let code_template = match language {
89            TargetLanguage::Rust => template.rust_template.as_deref().unwrap_or(DEFAULT_RUST),
90            TargetLanguage::Python => template.python_template.as_deref().unwrap_or(DEFAULT_PYTHON),
91        };
92
93        let code = self.substitute_placeholders(code_template, spec, template);
94        Ok(code)
95    }
96
97    /// Substitute placeholders in template
98    fn substitute_placeholders(
99        &self,
100        template: &str,
101        spec: &ParsedSpec,
102        test_template: &TestTemplate,
103    ) -> String {
104        let mut code = template.to_string();
105
106        // Module substitution
107        code = code.replace(&self.module_placeholder, &spec.module);
108        code = code.replace("{{module}}", &spec.module);
109
110        // Function substitution (use first function if available)
111        let function = spec.functions.first().map(|s| s.as_str()).unwrap_or("function");
112        code = code.replace(&self.function_placeholder, function);
113        code = code.replace("{{function}}", function);
114
115        // Type substitution (use first type if available)
116        let type_name = spec.types.first().map(|s| s.as_str()).unwrap_or("T");
117        code = code.replace(&self.type_placeholder, type_name);
118        code = code.replace("{{type}}", type_name);
119
120        // ID substitution
121        let id_lower = test_template.id.to_lowercase().replace('-', "_");
122        code = code.replace("{{id_lower}}", &id_lower);
123        code = code.replace("{{id}}", &test_template.id);
124
125        // Max size substitution
126        code = code.replace("{{max_size}}", "1_000_000");
127
128        // Strategy substitution for Python hypothesis
129        let strategy = match type_name {
130            "String" | "str" => "text",
131            "Vec" | "list" => "lists(integers())",
132            "f32" | "f64" | "float" => "floats(-1e6, 1e6)",
133            "i32" | "i64" | "int" => "integers(-1000000, 1000000)",
134            _ => "builds(lambda: None)",
135        };
136        code = code.replace("{{strategy}}", strategy);
137
138        code
139    }
140}
141
142impl Default for FalsifyGenerator {
143    fn default() -> Self {
144        Self::new()
145    }
146}
147
148const DEFAULT_RUST: &str = r#"#[test]
149#[ignore = "Stub: implement falsification for {{id}}"]
150fn falsify_{{id_lower}}_default() {
151    // Placeholder for {{id}} - replace with actual falsification logic
152    assert!(false, "Not yet implemented: falsification test for {{id}}");
153}"#;
154
155const DEFAULT_PYTHON: &str = r"    # STUB: Test placeholder for {{id}} - replace with actual falsification
156    pass
157";
158
159#[cfg(test)]
160mod tests {
161    use super::super::parser::SpecParser;
162    use super::*;
163    use std::path::Path;
164
165    #[test]
166    fn test_generator_creation() {
167        let gen = FalsifyGenerator::new();
168        assert!(!gen.module_placeholder.is_empty());
169    }
170
171    #[test]
172    fn test_generate_from_spec() {
173        let parser = SpecParser::new();
174        let content = r#"
175# Test Spec
176module: test_module
177
178## Functions
179fn test_function(input: &[u8]) -> Result<Vec<u8>, Error>
180"#;
181        let spec = parser.parse(content, Path::new("test.md")).expect("unexpected failure");
182        let template = FalsificationTemplate::default();
183        let gen = FalsifyGenerator::new();
184
185        let tests =
186            gen.generate(&spec, &template, TargetLanguage::Rust).expect("unexpected failure");
187        assert!(!tests.is_empty());
188
189        // Check that we got tests (module substitution may vary based on template)
190        for test in &tests {
191            // Tests should have valid IDs
192            assert!(!test.id.is_empty(), "Test ID should not be empty");
193        }
194    }
195
196    #[test]
197    fn test_placeholder_substitution() {
198        let gen = FalsifyGenerator::new();
199        let template = "fn test_{{module}}_{{function}}()";
200
201        let spec = ParsedSpec {
202            name: "test".to_string(),
203            module: "my_module".to_string(),
204            requirements: vec![],
205            types: vec!["MyType".to_string()],
206            functions: vec!["my_func".to_string()],
207            tolerances: None,
208        };
209
210        let test_template = super::super::template::TestTemplate {
211            id: "TEST-001".to_string(),
212            name: "Test".to_string(),
213            description: "Test".to_string(),
214            severity: TestSeverity::Medium,
215            points: 5,
216            rust_template: Some(template.to_string()),
217            python_template: None,
218        };
219
220        let result = gen.substitute_placeholders(template, &spec, &test_template);
221        assert!(result.contains("my_module"));
222        assert!(result.contains("my_func"));
223    }
224
225    #[test]
226    fn test_generator_default() {
227        let gen = FalsifyGenerator::default();
228        assert_eq!(gen.module_placeholder, "{{module}}");
229    }
230
231    #[test]
232    fn test_target_language_variants() {
233        assert_ne!(TargetLanguage::Rust, TargetLanguage::Python);
234        assert_eq!(TargetLanguage::Rust, TargetLanguage::Rust);
235    }
236
237    #[test]
238    fn test_generated_test_fields() {
239        let test = GeneratedTest {
240            id: "BC-001".to_string(),
241            name: "Boundary Test".to_string(),
242            category: "boundary".to_string(),
243            points: 4,
244            severity: TestSeverity::High,
245            code: "#[test]\nfn test_boundary() {}".to_string(),
246        };
247        assert_eq!(test.id, "BC-001");
248        assert_eq!(test.points, 4);
249        assert_eq!(test.severity, TestSeverity::High);
250    }
251
252    #[test]
253    fn test_generate_python() {
254        let parser = SpecParser::new();
255        let content = "module: test\n- MUST work";
256        let spec = parser.parse(content, Path::new("test.md")).expect("unexpected failure");
257        let template = FalsificationTemplate::default();
258        let gen = FalsifyGenerator::new();
259
260        let tests =
261            gen.generate(&spec, &template, TargetLanguage::Python).expect("unexpected failure");
262        assert!(!tests.is_empty());
263    }
264
265    #[test]
266    fn test_placeholder_id_substitution() {
267        let gen = FalsifyGenerator::new();
268        let template = "test_{{id_lower}} {{id}}";
269
270        let spec = ParsedSpec {
271            name: "test".to_string(),
272            module: "mod".to_string(),
273            requirements: vec![],
274            types: vec![],
275            functions: vec![],
276            tolerances: None,
277        };
278
279        let test_template = super::super::template::TestTemplate {
280            id: "BC-001".to_string(),
281            name: "Test".to_string(),
282            description: "Test".to_string(),
283            severity: TestSeverity::Medium,
284            points: 5,
285            rust_template: Some(template.to_string()),
286            python_template: None,
287        };
288
289        let result = gen.substitute_placeholders(template, &spec, &test_template);
290        assert!(result.contains("bc_001"));
291        assert!(result.contains("BC-001"));
292    }
293
294    #[test]
295    fn test_placeholder_type_substitution() {
296        let gen = FalsifyGenerator::new();
297        let template = "type: {{type}}";
298
299        let spec = ParsedSpec {
300            name: "test".to_string(),
301            module: "mod".to_string(),
302            requirements: vec![],
303            types: vec!["MyStruct".to_string()],
304            functions: vec![],
305            tolerances: None,
306        };
307
308        let test_template = super::super::template::TestTemplate {
309            id: "TEST-001".to_string(),
310            name: "Test".to_string(),
311            description: "Test".to_string(),
312            severity: TestSeverity::Low,
313            points: 2,
314            rust_template: Some(template.to_string()),
315            python_template: None,
316        };
317
318        let result = gen.substitute_placeholders(template, &spec, &test_template);
319        assert!(result.contains("MyStruct"));
320    }
321
322    #[test]
323    fn test_placeholder_max_size_substitution() {
324        let gen = FalsifyGenerator::new();
325        let template = "size: {{max_size}}";
326
327        let spec = ParsedSpec {
328            name: "test".to_string(),
329            module: "mod".to_string(),
330            requirements: vec![],
331            types: vec![],
332            functions: vec![],
333            tolerances: None,
334        };
335
336        let test_template = super::super::template::TestTemplate {
337            id: "TEST-001".to_string(),
338            name: "Test".to_string(),
339            description: "Test".to_string(),
340            severity: TestSeverity::Medium,
341            points: 5,
342            rust_template: Some(template.to_string()),
343            python_template: None,
344        };
345
346        let result = gen.substitute_placeholders(template, &spec, &test_template);
347        assert!(result.contains("1_000_000"));
348    }
349
350    #[test]
351    fn test_strategy_substitution_string() {
352        let gen = FalsifyGenerator::new();
353        let template = "{{strategy}}";
354
355        let spec = ParsedSpec {
356            name: "test".to_string(),
357            module: "mod".to_string(),
358            requirements: vec![],
359            types: vec!["String".to_string()],
360            functions: vec![],
361            tolerances: None,
362        };
363
364        let test_template = super::super::template::TestTemplate {
365            id: "TEST-001".to_string(),
366            name: "Test".to_string(),
367            description: "Test".to_string(),
368            severity: TestSeverity::Medium,
369            points: 5,
370            rust_template: Some(template.to_string()),
371            python_template: None,
372        };
373
374        let result = gen.substitute_placeholders(template, &spec, &test_template);
375        assert!(result.contains("text"));
376    }
377
378    #[test]
379    fn test_strategy_substitution_float() {
380        let gen = FalsifyGenerator::new();
381        let template = "{{strategy}}";
382
383        let spec = ParsedSpec {
384            name: "test".to_string(),
385            module: "mod".to_string(),
386            requirements: vec![],
387            types: vec!["f64".to_string()],
388            functions: vec![],
389            tolerances: None,
390        };
391
392        let test_template = super::super::template::TestTemplate {
393            id: "TEST-001".to_string(),
394            name: "Test".to_string(),
395            description: "Test".to_string(),
396            severity: TestSeverity::Medium,
397            points: 5,
398            rust_template: Some(template.to_string()),
399            python_template: None,
400        };
401
402        let result = gen.substitute_placeholders(template, &spec, &test_template);
403        assert!(result.contains("floats"));
404    }
405
406    #[test]
407    fn test_fallback_defaults() {
408        let gen = FalsifyGenerator::new();
409        let template = "{{function}} {{type}}";
410
411        let spec = ParsedSpec {
412            name: "test".to_string(),
413            module: "mod".to_string(),
414            requirements: vec![],
415            types: vec![],
416            functions: vec![],
417            tolerances: None,
418        };
419
420        let test_template = super::super::template::TestTemplate {
421            id: "TEST-001".to_string(),
422            name: "Test".to_string(),
423            description: "Test".to_string(),
424            severity: TestSeverity::Medium,
425            points: 5,
426            rust_template: Some(template.to_string()),
427            python_template: None,
428        };
429
430        let result = gen.substitute_placeholders(template, &spec, &test_template);
431        assert!(result.contains("function"));
432        assert!(result.contains('T'));
433    }
434}