use crate::{transpile, Config};
use std::collections::HashMap;
use std::fs;
use std::process::Command;
use tempfile::TempDir;
#[derive(Debug, Clone, PartialEq, Eq)]
struct ScriptState {
exit_code: i32,
stdout: String,
stderr: String,
files: HashMap<String, String>,
env_vars: HashMap<String, String>,
}
fn execute_and_capture_state(script: &str, working_dir: &TempDir) -> ScriptState {
let script_path = working_dir.path().join("script.sh");
fs::write(&script_path, script).expect("Failed to write script");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&script_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&script_path, perms).unwrap();
}
let output = Command::new("sh")
.arg(&script_path)
.current_dir(working_dir.path())
.output()
.expect("Failed to execute script");
let mut files = HashMap::new();
if let Ok(entries) = fs::read_dir(working_dir.path()) {
for entry in entries.flatten() {
if let Ok(path) = entry.path().strip_prefix(working_dir.path()) {
if path.to_str() != Some("script.sh") {
if let Ok(content) = fs::read(entry.path()) {
let hash = blake3::hash(&content).to_string();
files.insert(path.display().to_string(), hash);
}
}
}
}
}
ScriptState {
exit_code: output.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
files,
env_vars: HashMap::new(), }
}
#[test]
fn test_if_else_idempotent_true_branch() {
let source = r#"
fn main() {
let condition = true;
if condition {
write_file("result.txt", "true branch");
} else {
write_file("result.txt", "false branch");
}
}
fn write_file(path: &str, content: &str) {
let noop = true;
}
"#;
let config = Config::default();
let shell = transpile(source, &config).unwrap();
let temp_dir = TempDir::new().unwrap();
let state1 = execute_and_capture_state(&shell, &temp_dir);
let temp_dir2 = TempDir::new().unwrap();
let state2 = execute_and_capture_state(&shell, &temp_dir2);
assert_eq!(state1.exit_code, state2.exit_code, "Exit codes differ");
assert_eq!(state1.stdout, state2.stdout, "Stdout differs");
assert_eq!(state1.files.len(), state2.files.len(), "File count differs");
}
#[test]
fn test_if_else_idempotent_false_branch() {
let source = r#"
fn main() {
let condition = false;
if condition {
write_file("result.txt", "true branch");
} else {
write_file("result.txt", "false branch");
}
}
fn write_file(path: &str, content: &str) {
let noop = true;
}
"#;
let config = Config::default();
let shell = transpile(source, &config).unwrap();
let temp_dir = TempDir::new().unwrap();
let state1 = execute_and_capture_state(&shell, &temp_dir);
let temp_dir2 = TempDir::new().unwrap();
let state2 = execute_and_capture_state(&shell, &temp_dir2);
assert_eq!(state1.exit_code, state2.exit_code, "Exit codes differ");
assert_eq!(state1.stdout, state2.stdout, "Stdout differs");
}
#[test]
fn test_nested_if_else_idempotent() {
let source = r#"
fn main() {
let outer = true;
let inner = false;
if outer {
if inner {
write_file("nested.txt", "both true");
} else {
write_file("nested.txt", "outer true, inner false");
}
} else {
write_file("nested.txt", "outer false");
}
}
fn write_file(path: &str, content: &str) {
let noop = true;
}
"#;
let config = Config::default();
let shell = transpile(source, &config).unwrap();
let states: Vec<ScriptState> = (0..3)
.map(|_| {
let temp = TempDir::new().unwrap();
execute_and_capture_state(&shell, &temp)
})
.collect();
for i in 1..states.len() {
assert_eq!(
states[0].exit_code, states[i].exit_code,
"Exit code differs on run {}",
i
);
assert_eq!(
states[0].stdout, states[i].stdout,
"Stdout differs on run {}",
i
);
assert_eq!(
states[0].files.len(),
states[i].files.len(),
"File count differs on run {}",
i
);
}
}
#[test]
fn test_multiple_if_statements_idempotent() {
let source = r#"
fn main() {
let check1 = true;
let check2 = false;
let check3 = true;
if check1 {
write_file("file1.txt", "check1 passed");
}
if check2 {
write_file("file2.txt", "check2 passed");
}
if check3 {
write_file("file3.txt", "check3 passed");
}
}
fn write_file(path: &str, content: &str) {
let noop = true;
}
"#;
let config = Config::default();
let shell = transpile(source, &config).unwrap();
let temp_dir1 = TempDir::new().unwrap();
let state1 = execute_and_capture_state(&shell, &temp_dir1);
let temp_dir2 = TempDir::new().unwrap();
let state2 = execute_and_capture_state(&shell, &temp_dir2);
assert_eq!(state1, state2, "Multiple if statements not idempotent");
}
#[test]
fn test_early_exit_idempotent() {
let source = r#"
fn main() {
let should_execute = true;
if should_execute {
let marker = "branch_executed";
}
if !should_execute {
let unreachable = "should_not_execute";
}
}
"#;
let config = Config::default();
let shell = transpile(source, &config).unwrap();
let temp_dir1 = TempDir::new().unwrap();
let state1 = execute_and_capture_state(&shell, &temp_dir1);
let temp_dir2 = TempDir::new().unwrap();
let state2 = execute_and_capture_state(&shell, &temp_dir2);
assert_eq!(
state1.exit_code, 0,
"First run should complete successfully"
);
assert_eq!(
state2.exit_code, 0,
"Second run should complete successfully"
);
assert_eq!(state1, state2, "Conditional execution not idempotent");
}
#[test]
fn test_variable_assignment_in_branches_idempotent() {
let source = r#"
fn main() {
let condition = true;
let result = "default";
if condition {
let result = "modified";
write_file("var.txt", result);
} else {
write_file("var.txt", result);
}
}
fn write_file(path: &str, content: &str) {
let noop = true;
}
"#;
let config = Config::default();
let shell = transpile(source, &config).unwrap();
let temp_dir1 = TempDir::new().unwrap();
let state1 = execute_and_capture_state(&shell, &temp_dir1);
let temp_dir2 = TempDir::new().unwrap();
let state2 = execute_and_capture_state(&shell, &temp_dir2);
assert_eq!(
state1, state2,
"Variable assignment in branches not idempotent"
);
}
#[test]
fn test_if_else_if_chain_idempotent() {
let source = r#"
fn main() {
let value = 2;
if value == 1 {
write_file("result.txt", "one");
} else if value == 2 {
write_file("result.txt", "two");
} else if value == 3 {
write_file("result.txt", "three");
} else {
write_file("result.txt", "other");
}
}
fn write_file(path: &str, content: &str) {
let noop = true;
}
"#;
let config = Config::default();
let shell = transpile(source, &config).unwrap();
let states: Vec<ScriptState> = (0..5)
.map(|_| {
let temp = TempDir::new().unwrap();
execute_and_capture_state(&shell, &temp)
})
.collect();
for (i, state) in states.iter().enumerate().skip(1) {
assert_eq!(&states[0], state, "State differs on run {}", i);
}
}
#[test]
include!("idempotence_tests_main.rs");