ricecoder_refactoring/validation/
engine.rs

1//! Validation engine for refactoring operations
2
3use crate::error::Result;
4use crate::types::ValidationResult;
5use std::path::Path;
6use std::process::Command;
7
8/// Validates refactoring results
9pub struct ValidationEngine;
10
11impl ValidationEngine {
12    /// Create a new validation engine
13    pub fn new() -> Self {
14        Self
15    }
16}
17
18impl Default for ValidationEngine {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24/// Result of test execution
25#[derive(Debug, Clone)]
26pub struct TestExecutionResult {
27    /// Whether all tests passed
28    pub passed: bool,
29    /// Number of tests run
30    pub tests_run: usize,
31    /// Number of tests passed
32    pub tests_passed: usize,
33    /// Number of tests failed
34    pub tests_failed: usize,
35    /// Test output
36    pub output: String,
37    /// Test errors
38    pub errors: Vec<String>,
39}
40
41impl ValidationEngine {
42    /// Validate code syntax
43    pub fn validate_syntax(code: &str, language: &str) -> Result<ValidationResult> {
44        let mut errors = vec![];
45        let mut warnings = vec![];
46
47        // Basic syntax validation based on language
48        match language {
49            "rust" => {
50                Self::validate_rust_syntax(code, &mut errors, &mut warnings);
51            }
52            "typescript" | "javascript" => {
53                Self::validate_typescript_syntax(code, &mut errors, &mut warnings);
54            }
55            "python" => {
56                Self::validate_python_syntax(code, &mut errors, &mut warnings);
57            }
58            _ => {
59                // Generic validation
60                Self::validate_generic_syntax(code, &mut errors, &mut warnings);
61            }
62        }
63
64        Ok(ValidationResult {
65            passed: errors.is_empty(),
66            errors,
67            warnings,
68        })
69    }
70
71    /// Validate Rust syntax
72    fn validate_rust_syntax(code: &str, errors: &mut Vec<String>, warnings: &mut Vec<String>) {
73        let open_braces = code.matches('{').count();
74        let close_braces = code.matches('}').count();
75        let open_parens = code.matches('(').count();
76        let close_parens = code.matches(')').count();
77
78        if open_braces != close_braces {
79            errors.push(format!(
80                "Brace mismatch: {} open, {} close",
81                open_braces, close_braces
82            ));
83        }
84
85        if open_parens != close_parens {
86            errors.push(format!(
87                "Parenthesis mismatch: {} open, {} close",
88                open_parens, close_parens
89            ));
90        }
91
92        if code.contains("unsafe") {
93            warnings.push("Code contains unsafe block".to_string());
94        }
95    }
96
97    /// Validate TypeScript syntax
98    fn validate_typescript_syntax(code: &str, errors: &mut Vec<String>, warnings: &mut Vec<String>) {
99        let open_braces = code.matches('{').count();
100        let close_braces = code.matches('}').count();
101        let open_parens = code.matches('(').count();
102        let close_parens = code.matches(')').count();
103
104        if open_braces != close_braces {
105            errors.push(format!(
106                "Brace mismatch: {} open, {} close",
107                open_braces, close_braces
108            ));
109        }
110
111        if open_parens != close_parens {
112            errors.push(format!(
113                "Parenthesis mismatch: {} open, {} close",
114                open_parens, close_parens
115            ));
116        }
117
118        if code.contains("any") {
119            warnings.push("Code uses 'any' type".to_string());
120        }
121    }
122
123    /// Validate Python syntax
124    fn validate_python_syntax(code: &str, errors: &mut Vec<String>, warnings: &mut Vec<String>) {
125        let open_parens = code.matches('(').count();
126        let close_parens = code.matches(')').count();
127        let open_brackets = code.matches('[').count();
128        let close_brackets = code.matches(']').count();
129
130        if open_parens != close_parens {
131            errors.push(format!(
132                "Parenthesis mismatch: {} open, {} close",
133                open_parens, close_parens
134            ));
135        }
136
137        if open_brackets != close_brackets {
138            errors.push(format!(
139                "Bracket mismatch: {} open, {} close",
140                open_brackets, close_brackets
141            ));
142        }
143
144        if code.contains("exec(") {
145            warnings.push("Code uses exec() function".to_string());
146        }
147    }
148
149    /// Generic syntax validation
150    fn validate_generic_syntax(code: &str, errors: &mut Vec<String>, _warnings: &mut Vec<String>) {
151        let open_braces = code.matches('{').count();
152        let close_braces = code.matches('}').count();
153        let open_parens = code.matches('(').count();
154        let close_parens = code.matches(')').count();
155
156        if open_braces != close_braces {
157            errors.push(format!(
158                "Brace mismatch: {} open, {} close",
159                open_braces, close_braces
160            ));
161        }
162
163        if open_parens != close_parens {
164            errors.push(format!(
165                "Parenthesis mismatch: {} open, {} close",
166                open_parens, close_parens
167            ));
168        }
169    }
170
171    /// Validate semantic correctness
172    pub fn validate_semantics(code: &str, _language: &str) -> Result<ValidationResult> {
173        let mut errors = vec![];
174        let warnings = vec![];
175
176        // Basic semantic checks
177        if code.is_empty() {
178            errors.push("Code cannot be empty".to_string());
179        }
180
181        Ok(ValidationResult {
182            passed: errors.is_empty(),
183            errors,
184            warnings,
185        })
186    }
187
188    /// Run automated tests for a project
189    ///
190    /// This method attempts to run tests for the specified language/project.
191    /// It supports Rust (cargo test), TypeScript/JavaScript (npm test), and Python (pytest).
192    pub fn run_tests(project_path: &Path, language: &str) -> Result<TestExecutionResult> {
193        match language {
194            "rust" => Self::run_rust_tests(project_path),
195            "typescript" | "javascript" => Self::run_npm_tests(project_path),
196            "python" => Self::run_python_tests(project_path),
197            _ => Self::run_generic_tests(project_path),
198        }
199    }
200
201    /// Run Rust tests using cargo
202    fn run_rust_tests(project_path: &Path) -> Result<TestExecutionResult> {
203        let output = Command::new("cargo")
204            .arg("test")
205            .arg("--")
206            .arg("--test-threads=1")
207            .current_dir(project_path)
208            .output()
209            .map_err(|e| crate::error::RefactoringError::ValidationFailed(
210                format!("Failed to run cargo tests: {}", e)
211            ))?;
212
213        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
214        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
215
216        let passed = output.status.success();
217        let (tests_run, tests_passed, tests_failed) = Self::parse_rust_test_output(&stdout);
218
219        let mut errors = vec![];
220        if !passed && !stderr.is_empty() {
221            errors.push(stderr);
222        }
223
224        Ok(TestExecutionResult {
225            passed,
226            tests_run,
227            tests_passed,
228            tests_failed,
229            output: stdout,
230            errors,
231        })
232    }
233
234    /// Run npm tests
235    fn run_npm_tests(project_path: &Path) -> Result<TestExecutionResult> {
236        let output = Command::new("npm")
237            .arg("test")
238            .current_dir(project_path)
239            .output()
240            .map_err(|e| crate::error::RefactoringError::ValidationFailed(
241                format!("Failed to run npm tests: {}", e)
242            ))?;
243
244        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
245        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
246
247        let passed = output.status.success();
248        let (tests_run, tests_passed, tests_failed) = Self::parse_npm_test_output(&stdout);
249
250        let mut errors = vec![];
251        if !passed && !stderr.is_empty() {
252            errors.push(stderr);
253        }
254
255        Ok(TestExecutionResult {
256            passed,
257            tests_run,
258            tests_passed,
259            tests_failed,
260            output: stdout,
261            errors,
262        })
263    }
264
265    /// Run Python tests using pytest
266    fn run_python_tests(project_path: &Path) -> Result<TestExecutionResult> {
267        let output = Command::new("pytest")
268            .arg("-v")
269            .current_dir(project_path)
270            .output()
271            .map_err(|e| crate::error::RefactoringError::ValidationFailed(
272                format!("Failed to run pytest: {}", e)
273            ))?;
274
275        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
276        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
277
278        let passed = output.status.success();
279        let (tests_run, tests_passed, tests_failed) = Self::parse_pytest_output(&stdout);
280
281        let mut errors = vec![];
282        if !passed && !stderr.is_empty() {
283            errors.push(stderr);
284        }
285
286        Ok(TestExecutionResult {
287            passed,
288            tests_run,
289            tests_passed,
290            tests_failed,
291            output: stdout,
292            errors,
293        })
294    }
295
296    /// Run generic tests (fallback)
297    fn run_generic_tests(_project_path: &Path) -> Result<TestExecutionResult> {
298        Ok(TestExecutionResult {
299            passed: true,
300            tests_run: 0,
301            tests_passed: 0,
302            tests_failed: 0,
303            output: "No test runner available for this language".to_string(),
304            errors: vec![],
305        })
306    }
307
308    /// Parse Rust test output to extract test counts
309    fn parse_rust_test_output(output: &str) -> (usize, usize, usize) {
310        let mut tests_run = 0;
311        let mut tests_passed = 0;
312        let mut tests_failed = 0;
313
314        for line in output.lines() {
315            if line.contains("test result:") {
316                // Parse lines like "test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out"
317                if let Some(passed_part) = line.split("passed;").next() {
318                    if let Some(num_str) = passed_part.split_whitespace().last() {
319                        if let Ok(num) = num_str.parse::<usize>() {
320                            tests_passed = num;
321                        }
322                    }
323                }
324
325                if let Some(failed_part) = line.split("failed;").next() {
326                    if let Some(num_str) = failed_part.split_whitespace().last() {
327                        if let Ok(num) = num_str.parse::<usize>() {
328                            tests_failed = num;
329                        }
330                    }
331                }
332
333                tests_run = tests_passed + tests_failed;
334            }
335        }
336
337        (tests_run, tests_passed, tests_failed)
338    }
339
340    /// Parse npm test output to extract test counts
341    fn parse_npm_test_output(output: &str) -> (usize, usize, usize) {
342        let mut tests_passed = 0;
343        let mut tests_failed = 0;
344
345        for line in output.lines() {
346            // Try to extract numbers from common test output formats
347            // Handles formats like "3 passed, 2 failed" or "5 passed"
348            let parts: Vec<&str> = line.split(|c: char| c == ',' || c.is_whitespace()).collect();
349            let mut i = 0;
350            while i < parts.len() {
351                if let Ok(num) = parts[i].parse::<usize>() {
352                    if i + 1 < parts.len() {
353                        match parts[i + 1] {
354                            "passed" => tests_passed = num,
355                            "failed" => tests_failed = num,
356                            _ => {}
357                        }
358                    }
359                }
360                i += 1;
361            }
362        }
363
364        let tests_run = tests_passed + tests_failed;
365        (tests_run, tests_passed, tests_failed)
366    }
367
368    /// Parse pytest output to extract test counts
369    fn parse_pytest_output(output: &str) -> (usize, usize, usize) {
370        let mut tests_passed = 0;
371        let mut tests_failed = 0;
372
373        for line in output.lines() {
374            // Parse lines like "5 passed in 0.12s" or "3 failed, 2 passed"
375            let parts: Vec<&str> = line.split(|c: char| c == ',' || c.is_whitespace()).collect();
376            let mut i = 0;
377            while i < parts.len() {
378                if let Ok(num) = parts[i].parse::<usize>() {
379                    if i + 1 < parts.len() {
380                        match parts[i + 1] {
381                            "passed" => tests_passed = num,
382                            "failed" => tests_failed = num,
383                            _ => {}
384                        }
385                    }
386                }
387                i += 1;
388            }
389        }
390
391        let tests_run = tests_passed + tests_failed;
392        (tests_run, tests_passed, tests_failed)
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    // ===== Syntax Validation Tests =====
401
402    #[test]
403    fn test_validate_rust_syntax_valid() -> Result<()> {
404        let code = "fn main() { println!(\"Hello\"); }";
405        let result = ValidationEngine::validate_syntax(code, "rust")?;
406        assert!(result.passed);
407        assert!(result.errors.is_empty());
408        Ok(())
409    }
410
411    #[test]
412    fn test_validate_rust_syntax_invalid_braces() -> Result<()> {
413        let code = "fn main() { println!(\"Hello\"); ";
414        let result = ValidationEngine::validate_syntax(code, "rust")?;
415        assert!(!result.passed);
416        assert!(!result.errors.is_empty());
417        assert!(result.errors[0].contains("Brace mismatch"));
418        Ok(())
419    }
420
421    #[test]
422    fn test_validate_rust_syntax_invalid_parens() -> Result<()> {
423        let code = "fn main() { println!(\"Hello\"; }";
424        let result = ValidationEngine::validate_syntax(code, "rust")?;
425        assert!(!result.passed);
426        assert!(!result.errors.is_empty());
427        assert!(result.errors[0].contains("Parenthesis mismatch"));
428        Ok(())
429    }
430
431    #[test]
432    fn test_validate_rust_syntax_unsafe_warning() -> Result<()> {
433        let code = "unsafe { let x = 5; }";
434        let result = ValidationEngine::validate_syntax(code, "rust")?;
435        assert!(result.passed);
436        assert!(!result.warnings.is_empty());
437        assert!(result.warnings[0].contains("unsafe"));
438        Ok(())
439    }
440
441    #[test]
442    fn test_validate_typescript_syntax_valid() -> Result<()> {
443        let code = "function main() { console.log(\"Hello\"); }";
444        let result = ValidationEngine::validate_syntax(code, "typescript")?;
445        assert!(result.passed);
446        assert!(result.errors.is_empty());
447        Ok(())
448    }
449
450    #[test]
451    fn test_validate_typescript_syntax_invalid_braces() -> Result<()> {
452        let code = "function main() { console.log(\"Hello\"); ";
453        let result = ValidationEngine::validate_syntax(code, "typescript")?;
454        assert!(!result.passed);
455        assert!(!result.errors.is_empty());
456        Ok(())
457    }
458
459    #[test]
460    fn test_validate_typescript_syntax_any_warning() -> Result<()> {
461        let code = "let x: any = 5;";
462        let result = ValidationEngine::validate_syntax(code, "typescript")?;
463        assert!(result.passed);
464        assert!(!result.warnings.is_empty());
465        assert!(result.warnings[0].contains("any"));
466        Ok(())
467    }
468
469    #[test]
470    fn test_validate_python_syntax_valid() -> Result<()> {
471        let code = "def main():\n    print(\"Hello\")";
472        let result = ValidationEngine::validate_syntax(code, "python")?;
473        assert!(result.passed);
474        assert!(result.errors.is_empty());
475        Ok(())
476    }
477
478    #[test]
479    fn test_validate_python_syntax_invalid_parens() -> Result<()> {
480        let code = "def main():\n    print(\"Hello\"";
481        let result = ValidationEngine::validate_syntax(code, "python")?;
482        assert!(!result.passed);
483        assert!(!result.errors.is_empty());
484        Ok(())
485    }
486
487    #[test]
488    fn test_validate_python_syntax_invalid_brackets() -> Result<()> {
489        let code = "x = [1, 2, 3";
490        let result = ValidationEngine::validate_syntax(code, "python")?;
491        assert!(!result.passed);
492        assert!(!result.errors.is_empty());
493        Ok(())
494    }
495
496    #[test]
497    fn test_validate_python_syntax_exec_warning() -> Result<()> {
498        let code = "exec(\"print('hello')\")";
499        let result = ValidationEngine::validate_syntax(code, "python")?;
500        assert!(result.passed);
501        assert!(!result.warnings.is_empty());
502        assert!(result.warnings[0].contains("exec()"));
503        Ok(())
504    }
505
506    #[test]
507    fn test_validate_generic_syntax_valid() -> Result<()> {
508        let code = "some code { with (parens) }";
509        let result = ValidationEngine::validate_syntax(code, "unknown")?;
510        assert!(result.passed);
511        Ok(())
512    }
513
514    #[test]
515    fn test_validate_generic_syntax_invalid() -> Result<()> {
516        let code = "some code { with (parens }";
517        let result = ValidationEngine::validate_syntax(code, "unknown")?;
518        assert!(!result.passed);
519        Ok(())
520    }
521
522    // ===== Semantic Validation Tests =====
523
524    #[test]
525    fn test_validate_semantics_empty() -> Result<()> {
526        let result = ValidationEngine::validate_semantics("", "rust")?;
527        assert!(!result.passed);
528        assert!(!result.errors.is_empty());
529        assert!(result.errors[0].contains("empty"));
530        Ok(())
531    }
532
533    #[test]
534    fn test_validate_semantics_valid() -> Result<()> {
535        let result = ValidationEngine::validate_semantics("fn main() {}", "rust")?;
536        assert!(result.passed);
537        assert!(result.errors.is_empty());
538        Ok(())
539    }
540
541    #[test]
542    fn test_validate_semantics_valid_typescript() -> Result<()> {
543        let result = ValidationEngine::validate_semantics("function main() {}", "typescript")?;
544        assert!(result.passed);
545        Ok(())
546    }
547
548    #[test]
549    fn test_validate_semantics_valid_python() -> Result<()> {
550        let result = ValidationEngine::validate_semantics("def main(): pass", "python")?;
551        assert!(result.passed);
552        Ok(())
553    }
554
555    // ===== Test Output Parsing Tests =====
556
557    #[test]
558    fn test_parse_rust_test_output_success() {
559        let output = "test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out";
560        let (tests_run, tests_passed, tests_failed) = ValidationEngine::parse_rust_test_output(output);
561        assert_eq!(tests_run, 5);
562        assert_eq!(tests_passed, 5);
563        assert_eq!(tests_failed, 0);
564    }
565
566    #[test]
567    fn test_parse_rust_test_output_with_failures() {
568        let output = "test result: FAILED. 3 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out";
569        let (tests_run, tests_passed, tests_failed) = ValidationEngine::parse_rust_test_output(output);
570        assert_eq!(tests_run, 5);
571        assert_eq!(tests_passed, 3);
572        assert_eq!(tests_failed, 2);
573    }
574
575    #[test]
576    fn test_parse_rust_test_output_no_tests() {
577        let output = "test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out";
578        let (tests_run, tests_passed, tests_failed) = ValidationEngine::parse_rust_test_output(output);
579        assert_eq!(tests_run, 0);
580        assert_eq!(tests_passed, 0);
581        assert_eq!(tests_failed, 0);
582    }
583
584    #[test]
585    fn test_parse_npm_test_output_success() {
586        let output = "5 passed";
587        let (_tests_run, tests_passed, tests_failed) = ValidationEngine::parse_npm_test_output(output);
588        assert_eq!(tests_passed, 5);
589        assert_eq!(tests_failed, 0);
590    }
591
592    #[test]
593    fn test_parse_npm_test_output_with_failures() {
594        let output = "3 passed, 2 failed";
595        let (_tests_run, tests_passed, tests_failed) = ValidationEngine::parse_npm_test_output(output);
596        assert_eq!(tests_passed, 3);
597        assert_eq!(tests_failed, 2);
598    }
599
600    #[test]
601    fn test_parse_pytest_output_success() {
602        let output = "5 passed in 0.12s";
603        let (_tests_run, tests_passed, tests_failed) = ValidationEngine::parse_pytest_output(output);
604        assert_eq!(tests_passed, 5);
605        assert_eq!(tests_failed, 0);
606    }
607
608    #[test]
609    fn test_parse_pytest_output_with_failures() {
610        let output = "3 failed, 2 passed in 0.15s";
611        let (_tests_run, tests_passed, tests_failed) = ValidationEngine::parse_pytest_output(output);
612        assert_eq!(tests_passed, 2);
613        assert_eq!(tests_failed, 3);
614    }
615
616    // ===== Test Execution Result Tests =====
617
618    #[test]
619    fn test_test_execution_result_creation() {
620        let result = TestExecutionResult {
621            passed: true,
622            tests_run: 5,
623            tests_passed: 5,
624            tests_failed: 0,
625            output: "All tests passed".to_string(),
626            errors: vec![],
627        };
628
629        assert!(result.passed);
630        assert_eq!(result.tests_run, 5);
631        assert_eq!(result.tests_passed, 5);
632        assert_eq!(result.tests_failed, 0);
633        assert!(result.errors.is_empty());
634    }
635
636    #[test]
637    fn test_test_execution_result_with_errors() {
638        let result = TestExecutionResult {
639            passed: false,
640            tests_run: 5,
641            tests_passed: 3,
642            tests_failed: 2,
643            output: "Some tests failed".to_string(),
644            errors: vec!["Test 1 failed".to_string(), "Test 2 failed".to_string()],
645        };
646
647        assert!(!result.passed);
648        assert_eq!(result.tests_run, 5);
649        assert_eq!(result.tests_passed, 3);
650        assert_eq!(result.tests_failed, 2);
651        assert_eq!(result.errors.len(), 2);
652    }
653}