fn test_header_and_footer_structure() {
let config = Config::default();
let emitter = PosixEmitter::new();
let ir = ShellIR::Noop;
let result = emitter.emit(&ir).unwrap();
assert!(result.starts_with("#!/bin/sh"));
assert!(result.contains("# Generated by Rash"));
assert!(result.contains("set -euf"));
assert!(result.contains("IFS=' \t\n'"));
assert!(result.contains("export LC_ALL=C"));
assert!(result.contains("main() {"));
assert!(result.contains("trap 'rm -rf"));
assert!(result.contains("main \"$@\""));
}
#[test]
fn test_runtime_functions_included() {
let config = Config::default();
let emitter = PosixEmitter::new();
let ir = ShellIR::Sequence(vec![
ShellIR::Exec {
cmd: Command::new("rash_require").arg(ShellValue::String("curl".to_string())),
effects: EffectSet::pure(),
},
ShellIR::Exec {
cmd: Command::new("rash_download_verified")
.arg(ShellValue::String("https://example.com/file".to_string()))
.arg(ShellValue::String("abc123".to_string())),
effects: EffectSet::pure(),
},
]);
let result = emitter.emit(&ir).unwrap();
assert!(result.contains("rash_require() {"));
assert!(result.contains("rash_download_verified() {"));
assert!(result.contains("curl -fsSL"));
assert!(result.contains("sha256sum"));
assert!(result.contains("wget"));
}
#[test]
fn test_test_expression_emission() {
let config = Config::default();
let emitter = PosixEmitter::new();
let result = emitter
.emit_test_expression(&ShellValue::Bool(true))
.unwrap();
assert_eq!(result, "true");
let result = emitter
.emit_test_expression(&ShellValue::Bool(false))
.unwrap();
assert_eq!(result, "false");
let result = emitter
.emit_test_expression(&ShellValue::Variable("var".to_string()))
.unwrap();
assert_eq!(result, "test -n \"$var\"");
let result = emitter
.emit_test_expression(&ShellValue::String("true".to_string()))
.unwrap();
assert_eq!(result, "true");
let result = emitter
.emit_test_expression(&ShellValue::String("false".to_string()))
.unwrap();
assert_eq!(result, "false");
}
#[test]
fn test_string_escaping() {
use super::escape::*;
assert_eq!(escape_shell_string("hello"), "hello");
assert_eq!(escape_shell_string("simple123"), "simple123");
assert_eq!(escape_shell_string("hello world"), "'hello world'");
assert_eq!(escape_shell_string(""), "''");
assert_eq!(escape_shell_string("don't"), "'don'\"'\"'t'");
}
#[test]
fn test_variable_name_escaping() {
use super::escape::*;
assert_eq!(escape_variable_name("valid_name"), "valid_name");
assert_eq!(escape_variable_name("_underscore"), "_underscore");
assert_eq!(escape_variable_name("name123"), "name123");
assert_eq!(escape_variable_name("invalid-name"), "invalid_name");
assert_eq!(escape_variable_name("123invalid"), "_23invalid");
assert_eq!(escape_variable_name("my.var"), "my_var");
}
#[test]
fn test_command_name_escaping() {
use super::escape::*;
assert_eq!(escape_command_name("ls"), "ls");
assert_eq!(escape_command_name("/bin/ls"), "/bin/ls");
assert_eq!(escape_command_name("my-tool"), "my-tool");
assert_eq!(escape_command_name("my command"), "'my command'");
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn test_string_escaping_preserves_content(s in ".*") {
use super::escape::*;
let escaped = escape_shell_string(&s);
if s.chars().all(|c| c.is_alphanumeric() || "_.-/+=:@".contains(c)) && !s.is_empty() {
assert!(escaped == s || escaped == format!("'{s}'"));
} else {
assert!(escaped.starts_with('\'') && escaped.ends_with('\'') || escaped == "''");
}
}
#[test]
fn test_variable_name_escaping_produces_valid_identifiers(name in "[a-zA-Z_][a-zA-Z0-9_-]*") {
use super::escape::*;
let escaped = escape_variable_name(&name);
assert!(escaped.chars().next().unwrap().is_alphabetic() || escaped.starts_with('_'));
assert!(escaped.chars().all(|c| c.is_alphanumeric() || c == '_'));
}
#[test]
fn prop_shell_values_emit_valid_code(
s in "[a-zA-Z0-9 _.-]{0,100}",
b in prop::bool::ANY,
var_name in "[a-zA-Z_][a-zA-Z0-9_]{0,20}"
) {
let config = Config::default();
let emitter = PosixEmitter::new();
let test_values = vec![
ShellValue::String(s),
ShellValue::Bool(b),
ShellValue::Variable(var_name),
];
for value in test_values {
let result = emitter.emit_shell_value(&value);
prop_assert!(result.is_ok(), "Failed to emit shell value: {:?}", value);
if let Ok(code) = result {
prop_assert!(!code.trim().is_empty());
prop_assert!(!code.contains("$(rm"), "Potential command injection in: {}", code);
prop_assert!(!code.contains("; rm"), "Potential command injection in: {}", code);
}
}
}
#[test]
fn prop_commands_emit_valid_shell(
cmd_name in "[a-zA-Z][a-zA-Z0-9_-]{0,20}",
arg_count in 0usize..5usize
) {
let config = Config::default();
let emitter = PosixEmitter::new();
let args: Vec<ShellValue> = (0..arg_count)
.map(|i| ShellValue::String(format!("arg{i}")))
.collect();
let cmd = Command {
program: cmd_name.clone(),
args,
};
let ir = ShellIR::Exec {
cmd,
effects: EffectSet::pure(),
};
let result = emitter.emit(&ir);
prop_assert!(result.is_ok(), "Failed to emit command: {}", cmd_name);
if let Ok(shell_code) = result {
prop_assert!(shell_code.contains(&cmd_name));
let single_quotes = shell_code.chars().filter(|&c| c == '\'').count();
prop_assert!(single_quotes % 2 == 0, "Unbalanced single quotes in: {}", shell_code);
prop_assert!(shell_code.contains("#!/bin/sh"));
prop_assert!(shell_code.contains("set -euf"));
}
}
#[test]
fn prop_let_statements_valid(
var_name in "[a-zA-Z_][a-zA-Z0-9_]{0,30}",
value in "[a-zA-Z0-9 _.-]{0,100}"
) {
let config = Config::default();
let emitter = PosixEmitter::new();
let ir = ShellIR::Let {
name: var_name.clone(),
value: ShellValue::String(value),
effects: EffectSet::pure(),
};
let result = emitter.emit(&ir);
prop_assert!(result.is_ok(), "Failed to emit let statement for: {}", var_name);
if let Ok(shell_code) = result {
let escaped_name = super::escape::escape_variable_name(&var_name);
prop_assert!(shell_code.contains(&escaped_name));
prop_assert!(!shell_code.contains("readonly ="), "Invalid assignment syntax");
}
}
#[test]
fn prop_if_statements_balanced(condition in prop::bool::ANY) {
let config = Config::default();
let emitter = PosixEmitter::new();
let ir = ShellIR::If {
test: ShellValue::Bool(condition),
then_branch: Box::new(ShellIR::Noop),
else_branch: Some(Box::new(ShellIR::Noop)),
};
let result = emitter.emit(&ir);
prop_assert!(result.is_ok(), "Failed to emit if statement");
if let Ok(shell_code) = result {
if let Some(main_start) = shell_code.find("main() {") {
if let Some(main_end) = shell_code[main_start..].find("# Cleanup") {
let main_content = &shell_code[main_start..main_start + main_end];
let if_count = main_content.matches("if ").count();
let fi_count = main_content.matches("fi").count();
prop_assert_eq!(if_count, fi_count, "Unbalanced if/fi in main function");
prop_assert!(main_content.contains("then"));
prop_assert!(main_content.contains("else"));
}
}
}
}
#[test]
fn prop_concatenation_preserves_order(
parts in prop::collection::vec("[a-zA-Z0-9]{1,10}", 1..5)
) {
let config = Config::default();
let emitter = PosixEmitter::new();
let shell_values: Vec<ShellValue> = parts.iter()
.map(|s| ShellValue::String(s.clone()))
.collect();
let concat_value = ShellValue::Concat(shell_values);
let result = emitter.emit_shell_value(&concat_value);
prop_assert!(result.is_ok(), "Failed to emit concatenation");
if let Ok(shell_code) = result {
let mut last_pos = 0;
for part in &parts {
if let Some(pos) = shell_code[last_pos..].find(part) {
last_pos += pos + part.len();
} else {
prop_assert!(false, "Part '{}' not found in order in: {}", part, shell_code);
}
}
}
}
#[test]
fn prop_emission_deterministic(var_name in "[a-zA-Z_][a-zA-Z0-9_]{0,20}") {
let config = Config::default();
let emitter1 = PosixEmitter::new();
let emitter2 = PosixEmitter::new();
let ir = ShellIR::Let {
name: var_name,
value: ShellValue::String("test".to_string()),
effects: EffectSet::pure(),
};
let result1 = emitter1.emit(&ir);
let result2 = emitter2.emit(&ir);
match (&result1, &result2) {
(Ok(code1), Ok(code2)) => prop_assert_eq!(code1, code2, "Non-deterministic emission detected"),
(Err(_), Err(_)) => {}, _ => prop_assert!(false, "Inconsistent success/failure between runs"),
}
}
#[test]
fn prop_special_chars_escaped(s in r#"['"$`\\;&|()<> \t\n]*"#) {
use super::escape::*;
let escaped = escape_shell_string(&s);
if s.chars().any(|c| "'\"$`\\;&|()<> \t\n".contains(c)) && !s.is_empty() {
prop_assert!(
escaped.starts_with('\'') || escaped.starts_with('"'),
"Special characters not properly escaped in: '{}' -> '{}'", s, escaped
);
}
}
#[test]
fn prop_exit_codes_valid(code in 0i32..256i32) {
let config = Config::default();
let emitter = PosixEmitter::new();
let ir = ShellIR::Exit {
code: code as u8,
message: Some("test message".to_string()),
};
let result = emitter.emit(&ir);
prop_assert!(result.is_ok(), "Failed to emit exit statement with code: {}", code);
if let Ok(shell_code) = result {
let exit_string = format!("exit {code}");
prop_assert!(shell_code.contains(&exit_string));
}
}
}
#[rstest]
#[case(ShellValue::String("test".to_string()), "test")]
#[case(ShellValue::Bool(true), "true")]
#[case(ShellValue::Bool(false), "false")]
#[case(ShellValue::Variable("var".to_string()), "\"$var\"")]
fn test_shell_value_emission_cases(#[case] value: ShellValue, #[case] expected: &str) {
let config = Config::default();
let emitter = PosixEmitter::new();
let result = emitter.emit_shell_value(&value).unwrap();
assert_eq!(result, expected);
}
#[test]
fn test_complex_nested_emission() {
let config = Config::default();
let emitter = PosixEmitter::new();
let ir = ShellIR::Sequence(vec![
ShellIR::Let {
name: "prefix".to_string(),
value: ShellValue::String("/usr/local".to_string()),
effects: EffectSet::pure(),
},
ShellIR::If {
test: ShellValue::Variable("install_mode".to_string()),
then_branch: Box::new(ShellIR::Sequence(vec![
ShellIR::Exec {
cmd: Command {
program: "mkdir".to_string(),
args: vec![ShellValue::Variable("prefix".to_string())],
},
effects: EffectSet::default(),
},
ShellIR::Exec {
cmd: Command {
program: "echo".to_string(),
args: vec![ShellValue::Concat(vec![
ShellValue::String("Installing to ".to_string()),
ShellValue::Variable("prefix".to_string()),
])],
},
effects: EffectSet::pure(),
},
])),
else_branch: Some(Box::new(ShellIR::Exit {
code: 1,
message: Some("Installation cancelled".to_string()),
})),
},
]);
let result = emitter.emit(&ir).unwrap();
assert!(result.contains("prefix='/usr/local'"));
assert!(!result.contains("readonly"));
assert!(result.contains("if test -n \"$install_mode\"; then"));
assert!(result.contains("mkdir \"$prefix\""));
assert!(result.contains("echo \"Installing to ${prefix}\""));
assert!(result.contains("else"));
assert!(result.contains("echo 'Installation cancelled' >&2"));
assert!(result.contains("exit 1"));
assert!(result.contains("fi"));
}