Skip to main content

batuta/pipeline/stages/
validation.rs

1//! Validation stage - verifies semantic equivalence.
2
3use anyhow::{Context as AnyhowContext, Result};
4
5#[cfg(feature = "native")]
6use tracing::{info, warn};
7
8// Stub macros for WASM build
9#[cfg(not(feature = "native"))]
10macro_rules! info {
11    ($($arg:tt)*) => {{}};
12}
13
14#[cfg(not(feature = "native"))]
15macro_rules! warn {
16    ($($arg:tt)*) => {{}};
17}
18
19use crate::pipeline::types::{PipelineContext, PipelineStage, ValidationResult};
20
21/// Validation stage - verifies semantic equivalence
22pub struct ValidationStage {
23    pub(crate) trace_syscalls: bool,
24    pub(crate) run_tests: bool,
25}
26
27impl ValidationStage {
28    pub fn new(trace_syscalls: bool, run_tests: bool) -> Self {
29        Self { trace_syscalls, run_tests }
30    }
31
32    /// Trace syscalls from both binaries and compare them for semantic equivalence
33    pub async fn trace_and_compare(
34        &self,
35        original_binary: &std::path::Path,
36        transpiled_binary: &std::path::Path,
37    ) -> Result<bool> {
38        // Trace original binary
39        let original_trace = Self::trace_binary(original_binary)?;
40
41        // Trace transpiled binary
42        let transpiled_trace = Self::trace_binary(transpiled_binary)?;
43
44        // Compare traces
45        Ok(Self::compare_traces(&original_trace, &transpiled_trace))
46    }
47
48    /// Trace a binary using Renacer and capture syscall output
49    pub fn trace_binary(binary: &std::path::Path) -> Result<Vec<String>> {
50        use std::process::Command;
51
52        // Run the binary with renacer tracing
53        // Note: This is a simplified approach - ideally we'd use the renacer library directly
54        let output = Command::new("renacer")
55            .arg(binary.to_string_lossy().to_string())
56            .output()
57            .context("Failed to run renacer")?;
58
59        if !output.status.success() {
60            let stderr = String::from_utf8_lossy(&output.stderr);
61            anyhow::bail!("Renacer failed: {}", stderr);
62        }
63
64        Ok(Self::parse_syscall_output(&output.stdout))
65    }
66
67    /// Parse raw renacer stdout bytes into a list of syscall strings.
68    ///
69    /// Filters out renacer's own messages (lines starting with `[`).
70    pub fn parse_syscall_output(stdout: &[u8]) -> Vec<String> {
71        let text = String::from_utf8_lossy(stdout);
72        text.lines().filter(|line| !line.starts_with('[')).map(|s| s.to_string()).collect()
73    }
74
75    /// Compare two syscall traces for semantic equivalence
76    pub fn compare_traces(trace1: &[String], trace2: &[String]) -> bool {
77        // For now, simple comparison - ideally would normalize and filter
78        // non-deterministic syscalls (timestamps, PIDs, etc.)
79        if trace1.len() != trace2.len() {
80            return false;
81        }
82
83        // Compare each syscall (ignoring arguments that may vary)
84        for (call1, call2) in trace1.iter().zip(trace2.iter()) {
85            // Extract syscall name (before the '(' character)
86            let name1 = call1.split('(').next().unwrap_or("");
87            let name2 = call2.split('(').next().unwrap_or("");
88
89            if name1 != name2 {
90                return false;
91            }
92        }
93
94        true
95    }
96}
97
98#[async_trait::async_trait]
99impl PipelineStage for ValidationStage {
100    fn name(&self) -> &'static str {
101        "Validation"
102    }
103
104    async fn execute(&self, mut ctx: PipelineContext) -> Result<PipelineContext> {
105        info!("Validating semantic equivalence");
106
107        // If syscall tracing is enabled, use Renacer to verify equivalence
108        if self.trace_syscalls {
109            info!("Tracing syscalls with Renacer");
110
111            // Find original and transpiled binaries
112            let original_binary = ctx.input_path.join("original_binary");
113            let transpiled_binary = ctx.output_path.join("target/release/transpiled");
114
115            if original_binary.exists() && transpiled_binary.exists() {
116                match self.trace_and_compare(&original_binary, &transpiled_binary).await {
117                    Ok(equivalent) => {
118                        ctx.validation_results.push(ValidationResult {
119                            stage: self.name().to_string(),
120                            passed: equivalent,
121                            message: if equivalent {
122                                "Syscall traces match - semantic equivalence verified"
123                            } else {
124                                "Syscall traces differ - semantic equivalence NOT verified"
125                            }
126                            .to_string(),
127                            details: None,
128                        });
129
130                        ctx.metadata.insert(
131                            "syscall_equivalence".to_string(),
132                            serde_json::json!(equivalent),
133                        );
134                    }
135                    Err(e) => {
136                        warn!("Syscall tracing failed: {}", e);
137                        ctx.validation_results.push(ValidationResult {
138                            stage: self.name().to_string(),
139                            passed: false,
140                            message: format!("Syscall tracing error: {}", e),
141                            details: None,
142                        });
143                    }
144                }
145            } else {
146                info!("Binaries not found for comparison, skipping syscall trace");
147            }
148        }
149
150        // If run_tests is enabled, run the original test suite
151        // PIPELINE-004: Test suite execution planned for validation phase
152        if self.run_tests {
153            info!("Running original test suite");
154            // Test execution deferred - requires renacer integration
155        }
156
157        ctx.metadata.insert("validation_completed".to_string(), serde_json::json!(true));
158
159        Ok(ctx)
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_validation_stage_new() {
169        let stage = ValidationStage::new(true, false);
170        assert!(stage.trace_syscalls);
171        assert!(!stage.run_tests);
172    }
173
174    #[test]
175    fn test_validation_stage_name() {
176        let stage = ValidationStage::new(false, false);
177        assert_eq!(stage.name(), "Validation");
178    }
179
180    #[test]
181    fn test_compare_traces_identical() {
182        let trace1 = vec!["read(3, buf, 1024)".to_string(), "write(1, msg, 12)".to_string()];
183        let trace2 = vec!["read(3, buf, 1024)".to_string(), "write(1, msg, 12)".to_string()];
184        assert!(ValidationStage::compare_traces(&trace1, &trace2));
185    }
186
187    #[test]
188    fn test_compare_traces_same_syscalls_different_args() {
189        let trace1 = vec!["read(3, buf, 1024)".to_string(), "write(1, msg, 12)".to_string()];
190        let trace2 = vec!["read(4, buf2, 2048)".to_string(), "write(2, msg2, 24)".to_string()];
191        // Should match because syscall names are the same
192        assert!(ValidationStage::compare_traces(&trace1, &trace2));
193    }
194
195    #[test]
196    fn test_compare_traces_different_syscalls() {
197        let trace1 = vec!["read(3, buf, 1024)".to_string(), "write(1, msg, 12)".to_string()];
198        let trace2 = vec!["open(path, flags)".to_string(), "close(3)".to_string()];
199        assert!(!ValidationStage::compare_traces(&trace1, &trace2));
200    }
201
202    #[test]
203    fn test_compare_traces_different_lengths() {
204        let trace1 = vec!["read(3, buf, 1024)".to_string()];
205        let trace2 = vec!["read(3, buf, 1024)".to_string(), "write(1, msg, 12)".to_string()];
206        assert!(!ValidationStage::compare_traces(&trace1, &trace2));
207    }
208
209    #[test]
210    fn test_compare_traces_empty() {
211        let trace1: Vec<String> = vec![];
212        let trace2: Vec<String> = vec![];
213        assert!(ValidationStage::compare_traces(&trace1, &trace2));
214    }
215
216    #[test]
217    fn test_compare_traces_no_parentheses() {
218        let trace1 = vec!["syscall1".to_string()];
219        let trace2 = vec!["syscall1".to_string()];
220        assert!(ValidationStage::compare_traces(&trace1, &trace2));
221    }
222
223    #[test]
224    fn test_compare_traces_partial_match() {
225        let trace1 = vec!["read(3)".to_string(), "write(1)".to_string(), "close(3)".to_string()];
226        let trace2 = vec![
227            "read(4)".to_string(),
228            "read(5)".to_string(), // Different syscall
229            "close(4)".to_string(),
230        ];
231        assert!(!ValidationStage::compare_traces(&trace1, &trace2));
232    }
233
234    // =========================================================================
235    // Coverage: ValidationStage fields and state
236    // =========================================================================
237
238    #[test]
239    fn test_validation_stage_both_flags() {
240        let stage = ValidationStage::new(true, true);
241        assert!(stage.trace_syscalls);
242        assert!(stage.run_tests);
243    }
244
245    #[test]
246    fn test_validation_stage_no_flags() {
247        let stage = ValidationStage::new(false, false);
248        assert!(!stage.trace_syscalls);
249        assert!(!stage.run_tests);
250    }
251
252    // =========================================================================
253    // Coverage: execute() async path - no trace, no tests
254    // =========================================================================
255
256    #[tokio::test]
257    async fn test_execute_no_trace_no_tests() {
258        let stage = ValidationStage::new(false, false);
259        let ctx = PipelineContext::new(
260            std::path::PathBuf::from("/tmp/input"),
261            std::path::PathBuf::from("/tmp/output"),
262        );
263
264        let result = stage.execute(ctx).await.expect("async operation failed");
265        // Should add validation_completed metadata
266        assert_eq!(result.metadata.get("validation_completed"), Some(&serde_json::json!(true)));
267        // No validation results since both flags are off
268        assert!(result.validation_results.is_empty());
269    }
270
271    #[tokio::test]
272    async fn test_execute_with_trace_no_binaries() {
273        let stage = ValidationStage::new(true, false);
274        let tempdir = tempfile::tempdir().expect("tempdir creation failed");
275        let ctx = PipelineContext::new(tempdir.path().to_path_buf(), tempdir.path().to_path_buf());
276
277        let result = stage.execute(ctx).await.expect("async operation failed");
278        // Binaries don't exist, so tracing is skipped
279        assert_eq!(result.metadata.get("validation_completed"), Some(&serde_json::json!(true)));
280        // No syscall_equivalence metadata since binaries not found
281        assert!(!result.metadata.contains_key("syscall_equivalence"));
282    }
283
284    #[tokio::test]
285    async fn test_execute_with_run_tests_flag() {
286        let stage = ValidationStage::new(false, true);
287        let ctx = PipelineContext::new(
288            std::path::PathBuf::from("/tmp/input"),
289            std::path::PathBuf::from("/tmp/output"),
290        );
291
292        let result = stage.execute(ctx).await.expect("async operation failed");
293        // run_tests is set but the implementation is deferred
294        assert_eq!(result.metadata.get("validation_completed"), Some(&serde_json::json!(true)));
295    }
296
297    #[tokio::test]
298    async fn test_execute_with_trace_binaries_not_found() {
299        // Create input/output dirs but no binaries
300        let input_dir = tempfile::tempdir().expect("tempdir creation failed");
301        let output_dir = tempfile::tempdir().expect("tempdir creation failed");
302
303        let stage = ValidationStage::new(true, true);
304        let ctx =
305            PipelineContext::new(input_dir.path().to_path_buf(), output_dir.path().to_path_buf());
306
307        let result = stage.execute(ctx).await.expect("async operation failed");
308        // Binaries not found -> tracing skipped
309        assert!(!result.metadata.contains_key("syscall_equivalence"));
310    }
311
312    #[tokio::test]
313    async fn test_execute_with_trace_binary_exists_but_renacer_not_found() {
314        let input_dir = tempfile::tempdir().expect("tempdir creation failed");
315        let output_dir = tempfile::tempdir().expect("tempdir creation failed");
316
317        // Create the expected binary paths
318        std::fs::write(input_dir.path().join("original_binary"), "#!/bin/sh\nexit 0")
319            .expect("unexpected failure");
320        let target_dir = output_dir.path().join("target/release");
321        std::fs::create_dir_all(&target_dir).expect("mkdir failed");
322        std::fs::write(target_dir.join("transpiled"), "#!/bin/sh\nexit 0")
323            .expect("fs write failed");
324
325        let stage = ValidationStage::new(true, false);
326        let ctx =
327            PipelineContext::new(input_dir.path().to_path_buf(), output_dir.path().to_path_buf());
328
329        let result = stage.execute(ctx).await.expect("async operation failed");
330        // Renacer is not installed, so trace_and_compare should fail
331        // This should produce a validation result with passed=false
332        assert!(!result.validation_results.is_empty());
333        assert!(!result.validation_results[0].passed);
334        assert!(
335            result.validation_results[0].message.contains("error")
336                || result.validation_results[0].message.contains("renacer")
337                || result.validation_results[0].message.contains("Syscall tracing error")
338        );
339    }
340
341    // =========================================================================
342    // Coverage: trace_binary error path
343    // =========================================================================
344
345    #[test]
346    fn test_trace_binary_not_found() {
347        let result = ValidationStage::trace_binary(std::path::Path::new("/nonexistent/binary"));
348        assert!(result.is_err());
349        let err = result.unwrap_err().to_string();
350        assert!(err.contains("renacer") || err.contains("Failed"));
351    }
352
353    // =========================================================================
354    // Coverage: compare_traces additional edge cases
355    // =========================================================================
356
357    #[test]
358    fn test_compare_traces_single_element_match() {
359        let trace1 = vec!["open(file.txt)".to_string()];
360        let trace2 = vec!["open(other.txt)".to_string()];
361        assert!(ValidationStage::compare_traces(&trace1, &trace2));
362    }
363
364    #[test]
365    fn test_compare_traces_single_element_mismatch() {
366        let trace1 = vec!["open(file.txt)".to_string()];
367        let trace2 = vec!["close(3)".to_string()];
368        assert!(!ValidationStage::compare_traces(&trace1, &trace2));
369    }
370
371    #[test]
372    fn test_compare_traces_many_syscalls() {
373        let trace1: Vec<String> = (0..100).map(|i| format!("syscall_{}(arg1, arg2)", i)).collect();
374        let trace2: Vec<String> =
375            (0..100).map(|i| format!("syscall_{}(different, args)", i)).collect();
376        assert!(ValidationStage::compare_traces(&trace1, &trace2));
377    }
378
379    #[test]
380    fn test_compare_traces_empty_strings() {
381        let trace1 = vec!["".to_string()];
382        let trace2 = vec!["".to_string()];
383        assert!(ValidationStage::compare_traces(&trace1, &trace2));
384    }
385
386    #[test]
387    fn test_compare_traces_one_empty_vs_nonempty() {
388        let trace1 = vec!["read(3)".to_string()];
389        let trace2: Vec<String> = vec![];
390        assert!(!ValidationStage::compare_traces(&trace1, &trace2));
391    }
392
393    // =========================================================================
394    // Coverage: PipelineContext output and validation_results
395    // =========================================================================
396
397    #[test]
398    fn test_pipeline_context_output_all_passed() {
399        let mut ctx = PipelineContext::new(
400            std::path::PathBuf::from("/input"),
401            std::path::PathBuf::from("/output"),
402        );
403        ctx.validation_results.push(ValidationResult {
404            stage: "test".to_string(),
405            passed: true,
406            message: "ok".to_string(),
407            details: None,
408        });
409        let output = ctx.output();
410        assert!(output.validation_passed);
411    }
412
413    // =========================================================================
414    // Coverage: parse_syscall_output (extracted from trace_binary success path)
415    // =========================================================================
416
417    #[test]
418    fn test_parse_syscall_output_basic() {
419        let stdout = b"read(3, buf, 1024)\nwrite(1, msg, 12)\n";
420        let result = ValidationStage::parse_syscall_output(stdout);
421        assert_eq!(result.len(), 2);
422        assert_eq!(result[0], "read(3, buf, 1024)");
423        assert_eq!(result[1], "write(1, msg, 12)");
424    }
425
426    #[test]
427    fn test_parse_syscall_output_filters_renacer_messages() {
428        let stdout =
429            b"[renacer] tracing pid 1234\nread(3, buf, 1024)\n[renacer] done\nwrite(1, msg, 12)\n";
430        let result = ValidationStage::parse_syscall_output(stdout);
431        assert_eq!(result.len(), 2);
432        assert_eq!(result[0], "read(3, buf, 1024)");
433        assert_eq!(result[1], "write(1, msg, 12)");
434    }
435
436    #[test]
437    fn test_parse_syscall_output_empty() {
438        let stdout = b"";
439        let result = ValidationStage::parse_syscall_output(stdout);
440        assert!(result.is_empty());
441    }
442
443    #[test]
444    fn test_parse_syscall_output_only_renacer_messages() {
445        let stdout = b"[renacer] starting\n[renacer] done\n";
446        let result = ValidationStage::parse_syscall_output(stdout);
447        assert!(result.is_empty());
448    }
449
450    #[test]
451    fn test_parse_syscall_output_utf8_lossy() {
452        // Invalid UTF-8 should be handled gracefully
453        let stdout = b"read(3, \xff\xfe, 10)\nwrite(1, ok, 2)\n";
454        let result = ValidationStage::parse_syscall_output(stdout);
455        assert_eq!(result.len(), 2);
456        // First line has replacement characters for invalid UTF-8
457        assert!(result[0].contains("read"));
458        assert_eq!(result[1], "write(1, ok, 2)");
459    }
460
461    #[test]
462    fn test_pipeline_context_output_one_failed() {
463        let mut ctx = PipelineContext::new(
464            std::path::PathBuf::from("/input"),
465            std::path::PathBuf::from("/output"),
466        );
467        ctx.validation_results.push(ValidationResult {
468            stage: "test1".to_string(),
469            passed: true,
470            message: "ok".to_string(),
471            details: None,
472        });
473        ctx.validation_results.push(ValidationResult {
474            stage: "test2".to_string(),
475            passed: false,
476            message: "failed".to_string(),
477            details: None,
478        });
479        let output = ctx.output();
480        assert!(!output.validation_passed);
481    }
482}