1use anyhow::{Context as AnyhowContext, Result};
4
5#[cfg(feature = "native")]
6use tracing::{info, warn};
7
8#[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
21pub 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 pub async fn trace_and_compare(
34 &self,
35 original_binary: &std::path::Path,
36 transpiled_binary: &std::path::Path,
37 ) -> Result<bool> {
38 let original_trace = Self::trace_binary(original_binary)?;
40
41 let transpiled_trace = Self::trace_binary(transpiled_binary)?;
43
44 Ok(Self::compare_traces(&original_trace, &transpiled_trace))
46 }
47
48 pub fn trace_binary(binary: &std::path::Path) -> Result<Vec<String>> {
50 use std::process::Command;
51
52 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 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 pub fn compare_traces(trace1: &[String], trace2: &[String]) -> bool {
77 if trace1.len() != trace2.len() {
80 return false;
81 }
82
83 for (call1, call2) in trace1.iter().zip(trace2.iter()) {
85 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 self.trace_syscalls {
109 info!("Tracing syscalls with Renacer");
110
111 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 self.run_tests {
153 info!("Running original test suite");
154 }
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 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(), "close(4)".to_string(),
230 ];
231 assert!(!ValidationStage::compare_traces(&trace1, &trace2));
232 }
233
234 #[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 #[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 assert_eq!(result.metadata.get("validation_completed"), Some(&serde_json::json!(true)));
267 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 assert_eq!(result.metadata.get("validation_completed"), Some(&serde_json::json!(true)));
280 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 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 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 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 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 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 #[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 #[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 #[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 #[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 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 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}