1use anyhow::{Context, Result};
21use std::path::{Path, PathBuf};
22use std::process::Command;
23use tempfile::TempDir;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct ExecutionOutput {
28 pub stdout: String,
30 pub stderr: String,
32 pub exit_code: i32,
34}
35
36#[derive(Debug, Clone)]
38pub struct DiffTestResult {
39 pub c_output: ExecutionOutput,
41 pub rust_output: ExecutionOutput,
43 pub stdout_matches: bool,
45 pub exit_code_matches: bool,
47 pub divergences: Vec<String>,
49}
50
51impl DiffTestResult {
52 pub fn passed(&self) -> bool {
54 self.stdout_matches && self.exit_code_matches
55 }
56}
57
58#[derive(Debug, Clone)]
60pub struct DiffTestConfig {
61 pub timeout_secs: u64,
63 pub gcc_path: String,
65 pub rustc_path: String,
67 pub compare_stderr: bool,
69}
70
71impl Default for DiffTestConfig {
72 fn default() -> Self {
73 Self {
74 timeout_secs: 5,
75 gcc_path: "gcc".to_string(),
76 rustc_path: "rustc".to_string(),
77 compare_stderr: false,
78 }
79 }
80}
81
82pub fn compile_c(c_code: &str, config: &DiffTestConfig) -> Result<(TempDir, PathBuf)> {
86 let tmp = TempDir::new().context("Failed to create temp directory for C compilation")?;
87 let src = tmp.path().join("input.c");
88 let bin = tmp.path().join("c_binary");
89
90 std::fs::write(&src, c_code).context("Failed to write C source to temp file")?;
91
92 let output = Command::new(&config.gcc_path)
93 .arg("-o")
94 .arg(&bin)
95 .arg("-x")
96 .arg("c")
97 .arg("-std=c99")
98 .arg("-lm")
99 .arg(&src)
100 .output()
101 .with_context(|| format!("Failed to run gcc ({})", config.gcc_path))?;
102
103 if !output.status.success() {
104 let stderr = String::from_utf8_lossy(&output.stderr);
105 anyhow::bail!("gcc compilation failed:\n{}", stderr);
106 }
107
108 Ok((tmp, bin))
109}
110
111pub fn compile_rust(rust_code: &str, config: &DiffTestConfig) -> Result<(TempDir, PathBuf)> {
115 let tmp = TempDir::new().context("Failed to create temp directory for Rust compilation")?;
116 let src = tmp.path().join("input.rs");
117 let bin = tmp.path().join("rust_binary");
118
119 std::fs::write(&src, rust_code).context("Failed to write Rust source to temp file")?;
120
121 let output = Command::new(&config.rustc_path)
122 .arg("--edition=2021")
123 .arg("-o")
124 .arg(&bin)
125 .arg(&src)
126 .output()
127 .with_context(|| format!("Failed to run rustc ({})", config.rustc_path))?;
128
129 if !output.status.success() {
130 let stderr = String::from_utf8_lossy(&output.stderr);
131 anyhow::bail!("rustc compilation failed:\n{}", stderr);
132 }
133
134 Ok((tmp, bin))
135}
136
137pub fn run_binary(binary: &Path) -> Result<ExecutionOutput> {
139 let output = Command::new(binary)
140 .output()
141 .with_context(|| format!("Failed to execute binary: {}", binary.display()))?;
142
143 Ok(ExecutionOutput {
144 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
145 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
146 exit_code: output.status.code().unwrap_or(-1),
147 })
148}
149
150pub fn diff_test(c_code: &str, rust_code: &str, config: &DiffTestConfig) -> Result<DiffTestResult> {
153 let (_c_dir, c_bin) =
155 compile_c(c_code, config).context("C compilation failed during diff test")?;
156 let (_rs_dir, rs_bin) =
157 compile_rust(rust_code, config).context("Rust compilation failed during diff test")?;
158
159 let c_output = run_binary(&c_bin).context("Failed to run C binary")?;
161 let rust_output = run_binary(&rs_bin).context("Failed to run Rust binary")?;
162
163 let stdout_matches = c_output.stdout == rust_output.stdout;
165 let exit_code_matches = c_output.exit_code == rust_output.exit_code;
166
167 let mut divergences = Vec::new();
168
169 if !stdout_matches {
170 divergences.push(format!(
171 "stdout differs:\n C: {:?}\n Rust: {:?}",
172 c_output.stdout, rust_output.stdout
173 ));
174 }
175
176 if !exit_code_matches {
177 divergences.push(format!(
178 "exit code differs: C={}, Rust={}",
179 c_output.exit_code, rust_output.exit_code
180 ));
181 }
182
183 if config.compare_stderr && c_output.stderr != rust_output.stderr {
184 divergences.push(format!(
185 "stderr differs:\n C: {:?}\n Rust: {:?}",
186 c_output.stderr, rust_output.stderr
187 ));
188 }
189
190 Ok(DiffTestResult { c_output, rust_output, stdout_matches, exit_code_matches, divergences })
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
202 fn test_compile_c_valid() {
203 let config = DiffTestConfig::default();
204 let result = compile_c("int main() { return 0; }", &config);
205 assert!(result.is_ok(), "Valid C should compile: {:?}", result.err());
206 let (_dir, bin) = result.unwrap();
207 assert!(bin.exists(), "Binary should exist after compilation");
208 }
209
210 #[test]
211 fn test_compile_c_invalid() {
212 let config = DiffTestConfig::default();
213 let result = compile_c("int main( { }", &config);
214 assert!(result.is_err(), "Invalid C should fail compilation");
215 let err_msg = result.unwrap_err().to_string();
216 assert!(
217 err_msg.contains("gcc compilation failed"),
218 "Error should mention gcc: {}",
219 err_msg
220 );
221 }
222
223 #[test]
224 fn test_compile_c_bad_gcc_path() {
225 let config =
226 DiffTestConfig { gcc_path: "/nonexistent/gcc".to_string(), ..Default::default() };
227 let result = compile_c("int main() { return 0; }", &config);
228 assert!(result.is_err(), "Bad gcc path should error");
229 }
230
231 #[test]
232 fn test_compile_rust_valid() {
233 let config = DiffTestConfig::default();
234 let result = compile_rust("fn main() {}", &config);
235 assert!(result.is_ok(), "Valid Rust should compile: {:?}", result.err());
236 let (_dir, bin) = result.unwrap();
237 assert!(bin.exists(), "Binary should exist after compilation");
238 }
239
240 #[test]
241 fn test_compile_rust_invalid() {
242 let config = DiffTestConfig::default();
243 let result = compile_rust("fn main( {}", &config);
244 assert!(result.is_err(), "Invalid Rust should fail compilation");
245 let err_msg = result.unwrap_err().to_string();
246 assert!(
247 err_msg.contains("rustc compilation failed"),
248 "Error should mention rustc: {}",
249 err_msg
250 );
251 }
252
253 #[test]
254 fn test_compile_rust_bad_rustc_path() {
255 let config =
256 DiffTestConfig { rustc_path: "/nonexistent/rustc".to_string(), ..Default::default() };
257 let result = compile_rust("fn main() {}", &config);
258 assert!(result.is_err(), "Bad rustc path should error");
259 }
260
261 #[test]
266 fn test_s5_p1_integer_arithmetic() {
267 let c_code = r#"
268#include <stdio.h>
269int add(int a, int b) { return a + b; }
270int main() {
271 printf("%d\n", add(2, 3));
272 return 0;
273}
274"#;
275 let rust_code = r#"
276fn add(a: i32, b: i32) -> i32 { a + b }
277fn main() {
278 println!("{}", add(2, 3));
279}
280"#;
281 let config = DiffTestConfig::default();
282 let result = diff_test(c_code, rust_code, &config).expect("diff_test should succeed");
283 assert!(
284 result.stdout_matches,
285 "Integer arithmetic stdout should match: {:?}",
286 result.divergences
287 );
288 assert!(result.exit_code_matches, "Exit codes should match: {:?}", result.divergences);
289 assert!(result.passed());
290 }
291
292 #[test]
293 fn test_s5_p2_array_indexing() {
294 let c_code = r#"
295#include <stdio.h>
296int main() {
297 int arr[] = {10, 20, 30};
298 printf("%d\n", arr[1]);
299 return 0;
300}
301"#;
302 let rust_code = r#"
303fn main() {
304 let arr = [10, 20, 30];
305 println!("{}", arr[1]);
306}
307"#;
308 let config = DiffTestConfig::default();
309 let result = diff_test(c_code, rust_code, &config).expect("diff_test should succeed");
310 assert!(
311 result.stdout_matches,
312 "Array indexing stdout should match: {:?}",
313 result.divergences
314 );
315 assert!(result.passed());
316 }
317
318 #[test]
319 fn test_s5_p3_string_output() {
320 let c_code = r#"
321#include <stdio.h>
322int main() {
323 printf("hello world\n");
324 return 0;
325}
326"#;
327 let rust_code = r#"
328fn main() {
329 println!("hello world");
330}
331"#;
332 let config = DiffTestConfig::default();
333 let result = diff_test(c_code, rust_code, &config).expect("diff_test should succeed");
334 assert!(result.stdout_matches, "String output should match: {:?}", result.divergences);
335 assert!(result.passed());
336 }
337
338 #[test]
343 fn test_empty_stdout() {
344 let c_code = "int main() { return 0; }";
345 let rust_code = "fn main() {}";
346 let config = DiffTestConfig::default();
347 let result = diff_test(c_code, rust_code, &config).expect("diff_test should succeed");
348 assert!(result.stdout_matches, "Empty stdout should match");
349 assert_eq!(result.c_output.stdout, "");
350 assert_eq!(result.rust_output.stdout, "");
351 assert!(result.passed());
352 }
353
354 #[test]
355 fn test_multiline_output() {
356 let c_code = r#"
357#include <stdio.h>
358int main() {
359 printf("line1\n");
360 printf("line2\n");
361 printf("line3\n");
362 return 0;
363}
364"#;
365 let rust_code = r#"
366fn main() {
367 println!("line1");
368 println!("line2");
369 println!("line3");
370}
371"#;
372 let config = DiffTestConfig::default();
373 let result = diff_test(c_code, rust_code, &config).expect("diff_test should succeed");
374 assert!(result.stdout_matches, "Multiline output should match: {:?}", result.divergences);
375 assert!(result.passed());
376 }
377
378 #[test]
379 fn test_nonzero_exit_code() {
380 let c_code = "int main() { return 42; }";
381 let rust_code = "fn main() { std::process::exit(42); }";
382 let config = DiffTestConfig::default();
383 let result = diff_test(c_code, rust_code, &config).expect("diff_test should succeed");
384 assert_eq!(result.c_output.exit_code, 42);
385 assert_eq!(result.rust_output.exit_code, 42);
386 assert!(result.exit_code_matches);
387 assert!(result.passed());
388 }
389
390 #[test]
391 fn test_stdout_divergence_detected() {
392 let c_code = r#"
393#include <stdio.h>
394int main() { printf("from C\n"); return 0; }
395"#;
396 let rust_code = r#"
397fn main() { println!("from Rust"); }
398"#;
399 let config = DiffTestConfig::default();
400 let result = diff_test(c_code, rust_code, &config).expect("diff_test should succeed");
401 assert!(!result.stdout_matches, "Different outputs should diverge");
402 assert!(!result.divergences.is_empty());
403 assert!(!result.passed());
404 }
405
406 #[test]
407 fn test_exit_code_divergence_detected() {
408 let c_code = "int main() { return 0; }";
409 let rust_code = "fn main() { std::process::exit(1); }";
410 let config = DiffTestConfig::default();
411 let result = diff_test(c_code, rust_code, &config).expect("diff_test should succeed");
412 assert!(!result.exit_code_matches, "Different exit codes should diverge");
413 assert!(!result.passed());
414 }
415
416 #[test]
421 fn test_default_config() {
422 let config = DiffTestConfig::default();
423 assert_eq!(config.timeout_secs, 5);
424 assert_eq!(config.gcc_path, "gcc");
425 assert_eq!(config.rustc_path, "rustc");
426 assert!(!config.compare_stderr);
427 }
428
429 #[test]
430 fn test_compare_stderr_flag() {
431 let c_code = "int main() { return 0; }";
432 let rust_code = "fn main() {}";
433 let config = DiffTestConfig { compare_stderr: true, ..Default::default() };
434 let result = diff_test(c_code, rust_code, &config).expect("diff_test should succeed");
435 assert!(result.passed());
436 }
437
438 #[test]
439 fn test_diff_test_result_passed() {
440 let result = DiffTestResult {
441 c_output: ExecutionOutput {
442 stdout: "ok\n".to_string(),
443 stderr: String::new(),
444 exit_code: 0,
445 },
446 rust_output: ExecutionOutput {
447 stdout: "ok\n".to_string(),
448 stderr: String::new(),
449 exit_code: 0,
450 },
451 stdout_matches: true,
452 exit_code_matches: true,
453 divergences: vec![],
454 };
455 assert!(result.passed());
456 }
457
458 #[test]
459 fn test_diff_test_result_failed() {
460 let result = DiffTestResult {
461 c_output: ExecutionOutput {
462 stdout: "a\n".to_string(),
463 stderr: String::new(),
464 exit_code: 0,
465 },
466 rust_output: ExecutionOutput {
467 stdout: "b\n".to_string(),
468 stderr: String::new(),
469 exit_code: 0,
470 },
471 stdout_matches: false,
472 exit_code_matches: true,
473 divergences: vec!["stdout differs".to_string()],
474 };
475 assert!(!result.passed());
476 }
477}