Skip to main content

ryo_app/spec_dsl/
verification.rs

1//! Verification layer for spec-based testing
2//!
3//! Provides verification points that can be checked after each phase.
4
5use thiserror::Error;
6
7use super::types::VerificationPointSpec;
8
9/// Verification error
10#[derive(Debug, Clone, Error)]
11pub enum VerificationError {
12    #[error("Module not found: {0}")]
13    ModuleNotFound(String),
14
15    #[error("Type not found: {0}")]
16    TypeNotFound(String),
17
18    #[error("Missing derive on {ty}: {derive}")]
19    MissingDerive { ty: String, derive: String },
20
21    #[error("Missing field on {ty}: {field}")]
22    MissingField { ty: String, field: String },
23
24    #[error("Method not found: {ty}::{method}")]
25    MethodNotFound { ty: String, method: String },
26
27    #[error("Compilation failed: {0}")]
28    CompilationFailed(String),
29
30    #[error("Warning detected: {0}")]
31    WarningDetected(String),
32}
33
34/// Verification point abstraction
35#[derive(Debug, Clone)]
36pub enum VerificationPoint {
37    /// Check that a module exists
38    ModuleExists(Vec<String>),
39
40    /// Check that types exist
41    TypeExists(Vec<String>),
42
43    /// Check that a type has specific derives
44    HasDerive { ty: String, derives: Vec<String> },
45
46    /// Check that a struct has a specific field
47    HasField {
48        ty: String,
49        field: String,
50        field_type: String,
51    },
52
53    /// Check that a type has a specific method
54    MethodExists { ty: String, method: String },
55
56    /// Check that code compiles
57    Compiles,
58
59    /// Check for no warnings (with allowed list)
60    NoWarnings { allowed: Vec<String> },
61}
62
63impl From<VerificationPointSpec> for VerificationPoint {
64    fn from(spec: VerificationPointSpec) -> Self {
65        match spec {
66            VerificationPointSpec::ModuleExists { paths } => VerificationPoint::ModuleExists(paths),
67            VerificationPointSpec::TypeExists { types } => VerificationPoint::TypeExists(types),
68            VerificationPointSpec::HasDerive { ty, derives } => {
69                VerificationPoint::HasDerive { ty, derives }
70            }
71            VerificationPointSpec::HasField {
72                ty,
73                field,
74                field_type,
75            } => VerificationPoint::HasField {
76                ty,
77                field,
78                field_type,
79            },
80            VerificationPointSpec::MethodExists { ty, method } => {
81                VerificationPoint::MethodExists { ty, method }
82            }
83            VerificationPointSpec::Compiles => VerificationPoint::Compiles,
84            VerificationPointSpec::NoWarnings { allowed } => {
85                VerificationPoint::NoWarnings { allowed }
86            }
87        }
88    }
89}
90
91/// Result of running a verification
92#[derive(Debug, Clone)]
93pub struct VerificationResult {
94    /// Phase name
95    pub phase: String,
96
97    /// Total points checked
98    pub total: usize,
99
100    /// Passed points
101    pub passed: usize,
102
103    /// Failed points with errors
104    pub failures: Vec<(VerificationPoint, VerificationError)>,
105}
106
107impl VerificationResult {
108    /// Check if all verifications passed
109    pub fn all_passed(&self) -> bool {
110        self.failures.is_empty()
111    }
112}
113
114/// Verification runner
115pub struct VerificationRunner {
116    /// Project root path
117    project_root: std::path::PathBuf,
118}
119
120impl VerificationRunner {
121    /// Create a new verification runner
122    pub fn new(project_root: impl Into<std::path::PathBuf>) -> Self {
123        Self {
124            project_root: project_root.into(),
125        }
126    }
127
128    /// Run verifications for a phase
129    pub fn run(&self, phase: &str, points: &[VerificationPoint]) -> VerificationResult {
130        let mut passed = 0;
131        let mut failures = Vec::new();
132
133        for point in points {
134            match self.verify_point(point) {
135                Ok(()) => passed += 1,
136                Err(e) => failures.push((point.clone(), e)),
137            }
138        }
139
140        VerificationResult {
141            phase: phase.to_string(),
142            total: points.len(),
143            passed,
144            failures,
145        }
146    }
147
148    /// Verify a single point
149    fn verify_point(&self, point: &VerificationPoint) -> Result<(), VerificationError> {
150        match point {
151            VerificationPoint::ModuleExists(paths) => {
152                for path in paths {
153                    self.check_module_exists(path)?;
154                }
155                Ok(())
156            }
157            VerificationPoint::TypeExists(types) => {
158                for ty in types {
159                    self.check_type_exists(ty)?;
160                }
161                Ok(())
162            }
163            VerificationPoint::HasDerive { ty, derives } => {
164                for derive in derives {
165                    self.check_has_derive(ty, derive)?;
166                }
167                Ok(())
168            }
169            VerificationPoint::HasField {
170                ty,
171                field,
172                field_type,
173            } => self.check_has_field(ty, field, field_type),
174            VerificationPoint::MethodExists { ty, method } => self.check_method_exists(ty, method),
175            VerificationPoint::Compiles => self.check_compiles(),
176            VerificationPoint::NoWarnings { allowed } => self.check_no_warnings(allowed),
177        }
178    }
179
180    /// Check if a module exists
181    fn check_module_exists(&self, path: &str) -> Result<(), VerificationError> {
182        // Convert module path to file path
183        let file_path = if path.contains("::") {
184            let parts: Vec<&str> = path.split("::").collect();
185            format!("src/{}.rs", parts.join("/"))
186        } else {
187            format!("src/{}.rs", path)
188        };
189
190        let full_path = self.project_root.join(&file_path);
191
192        // Also check for mod.rs pattern
193        let mod_path = if path.contains("::") {
194            let parts: Vec<&str> = path.split("::").collect();
195            format!("src/{}/mod.rs", parts.join("/"))
196        } else {
197            format!("src/{}/mod.rs", path)
198        };
199
200        let full_mod_path = self.project_root.join(&mod_path);
201
202        if full_path.exists() || full_mod_path.exists() {
203            Ok(())
204        } else {
205            Err(VerificationError::ModuleNotFound(path.to_string()))
206        }
207    }
208
209    /// Check if a type exists (basic file-based check)
210    fn check_type_exists(&self, ty: &str) -> Result<(), VerificationError> {
211        // This would use ryo-analysis in a full implementation
212        // For now, we just check that the type name appears in the source
213        let src_path = self.project_root.join("src");
214
215        if !src_path.exists() {
216            return Err(VerificationError::TypeNotFound(ty.to_string()));
217        }
218
219        // Simple grep for the type definition
220        // In production, use SymbolRegistry
221        for entry in walkdir::WalkDir::new(&src_path)
222            .into_iter()
223            .filter_map(|e| e.ok())
224            .filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
225        {
226            if let Ok(content) = std::fs::read_to_string(entry.path()) {
227                // Check for struct or enum definition
228                let patterns = [
229                    format!("struct {} ", ty),
230                    format!("struct {}(", ty),
231                    format!("struct {} {{", ty),
232                    format!("enum {} ", ty),
233                    format!("enum {} {{", ty),
234                ];
235
236                for pattern in &patterns {
237                    if content.contains(pattern) {
238                        return Ok(());
239                    }
240                }
241            }
242        }
243
244        Err(VerificationError::TypeNotFound(ty.to_string()))
245    }
246
247    /// Check if a type has a derive
248    fn check_has_derive(&self, ty: &str, derive: &str) -> Result<(), VerificationError> {
249        // Would use ryo-analysis in full implementation
250        let src_path = self.project_root.join("src");
251
252        for entry in walkdir::WalkDir::new(&src_path)
253            .into_iter()
254            .filter_map(|e| e.ok())
255            .filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
256        {
257            if let Ok(content) = std::fs::read_to_string(entry.path()) {
258                // Find type definition and check preceding derive
259                if let Some(pos) = content.find(&format!("struct {}", ty)) {
260                    let before = &content[..pos];
261                    if let Some(derive_pos) = before.rfind("#[derive(") {
262                        let derive_block = &before[derive_pos..];
263                        if derive_block.contains(derive) {
264                            return Ok(());
265                        }
266                    }
267                }
268
269                if let Some(pos) = content.find(&format!("enum {}", ty)) {
270                    let before = &content[..pos];
271                    if let Some(derive_pos) = before.rfind("#[derive(") {
272                        let derive_block = &before[derive_pos..];
273                        if derive_block.contains(derive) {
274                            return Ok(());
275                        }
276                    }
277                }
278            }
279        }
280
281        Err(VerificationError::MissingDerive {
282            ty: ty.to_string(),
283            derive: derive.to_string(),
284        })
285    }
286
287    /// Check if a struct has a field
288    fn check_has_field(
289        &self,
290        ty: &str,
291        field: &str,
292        _field_type: &str,
293    ) -> Result<(), VerificationError> {
294        // Would use ryo-analysis in full implementation
295        let src_path = self.project_root.join("src");
296
297        for entry in walkdir::WalkDir::new(&src_path)
298            .into_iter()
299            .filter_map(|e| e.ok())
300            .filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
301        {
302            if let Ok(content) = std::fs::read_to_string(entry.path()) {
303                // Find struct definition
304                if let Some(pos) = content.find(&format!("struct {} {{", ty)) {
305                    // Find the closing brace
306                    if let Some(end) = content[pos..].find('}') {
307                        let struct_body = &content[pos..pos + end];
308                        if struct_body.contains(&field.to_string()) {
309                            return Ok(());
310                        }
311                    }
312                }
313            }
314        }
315
316        Err(VerificationError::MissingField {
317            ty: ty.to_string(),
318            field: field.to_string(),
319        })
320    }
321
322    /// Check if a type has a method
323    fn check_method_exists(&self, ty: &str, method: &str) -> Result<(), VerificationError> {
324        // Would use ryo-analysis in full implementation
325        let src_path = self.project_root.join("src");
326
327        for entry in walkdir::WalkDir::new(&src_path)
328            .into_iter()
329            .filter_map(|e| e.ok())
330            .filter(|e| e.path().extension().is_some_and(|ext| ext == "rs"))
331        {
332            if let Ok(content) = std::fs::read_to_string(entry.path()) {
333                // Find impl block for the type
334                if content.contains(&format!("impl {}", ty))
335                    && content.contains(&format!("fn {}(", method))
336                {
337                    return Ok(());
338                }
339            }
340        }
341
342        Err(VerificationError::MethodNotFound {
343            ty: ty.to_string(),
344            method: method.to_string(),
345        })
346    }
347
348    /// Check that code compiles
349    fn check_compiles(&self) -> Result<(), VerificationError> {
350        let output = std::process::Command::new("cargo")
351            .arg("check")
352            .current_dir(&self.project_root)
353            .output();
354
355        match output {
356            Ok(out) if out.status.success() => Ok(()),
357            Ok(out) => {
358                let stderr = String::from_utf8_lossy(&out.stderr);
359                Err(VerificationError::CompilationFailed(stderr.to_string()))
360            }
361            Err(e) => Err(VerificationError::CompilationFailed(e.to_string())),
362        }
363    }
364
365    /// Check for no warnings
366    fn check_no_warnings(&self, allowed: &[String]) -> Result<(), VerificationError> {
367        let output = std::process::Command::new("cargo")
368            .args(["check", "--message-format=short"])
369            .current_dir(&self.project_root)
370            .output();
371
372        match output {
373            Ok(out) => {
374                let stderr = String::from_utf8_lossy(&out.stderr);
375
376                // Filter out allowed warnings
377                for line in stderr.lines() {
378                    if line.contains("warning:") {
379                        let is_allowed = allowed.iter().any(|a| line.contains(a));
380                        if !is_allowed {
381                            return Err(VerificationError::WarningDetected(line.to_string()));
382                        }
383                    }
384                }
385
386                Ok(())
387            }
388            Err(e) => Err(VerificationError::CompilationFailed(e.to_string())),
389        }
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    #[test]
398    fn test_verification_result() {
399        let result = VerificationResult {
400            phase: "test".to_string(),
401            total: 3,
402            passed: 3,
403            failures: vec![],
404        };
405
406        assert!(result.all_passed());
407    }
408
409    #[test]
410    fn test_verification_point_from_spec() {
411        let spec = VerificationPointSpec::ModuleExists {
412            paths: vec!["user".to_string(), "product".to_string()],
413        };
414
415        let point: VerificationPoint = spec.into();
416
417        if let VerificationPoint::ModuleExists(paths) = point {
418            assert_eq!(paths.len(), 2);
419        } else {
420            panic!("Expected ModuleExists");
421        }
422    }
423}