#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
use crate::bash_parser::codegen::generate_purified_bash;
use crate::bash_parser::parser::BashParser;
use crate::bash_transpiler::purification::{PurificationOptions, Purifier};
fn purify(input: &str) -> String {
let mut parser = BashParser::new(input).expect("parser init");
let ast = parser.parse().expect("parse");
let mut purifier = Purifier::new(PurificationOptions::default());
let purified_ast = purifier.purify(&ast).expect("purify");
generate_purified_bash(&purified_ast)
}
fn purify_with_report(
input: &str,
) -> (
String,
crate::bash_transpiler::purification::PurificationReport,
) {
let mut parser = BashParser::new(input).expect("parser init");
let ast = parser.parse().expect("parse");
let mut purifier = Purifier::new(PurificationOptions::default());
let purified_ast = purifier.purify(&ast).expect("purify");
let output = generate_purified_bash(&purified_ast);
(output, purifier.report().clone())
}
#[test]
fn golden_shebang_transformation() {
let output = purify("#!/bin/bash\necho hello");
assert!(
output.starts_with("#!/bin/sh\n"),
"Should transform #!/bin/bash to #!/bin/sh: {output}"
);
assert_eq!(
output.matches("#!/bin/sh").count(),
1,
"Should have exactly one shebang"
);
}
#[test]
fn golden_random_variable_replaced() {
let (output, report) = purify_with_report("#!/bin/bash\nvalue=$RANDOM");
assert!(
!output.contains("$RANDOM"),
"Should not contain $RANDOM: {output}"
);
assert!(
output.contains("value=0"),
"Should replace $RANDOM with 0: {output}"
);
assert!(
!report.determinism_fixes.is_empty(),
"Should report determinism fix"
);
}
#[test]
fn golden_srandom_variable_replaced() {
let (output, _report) = purify_with_report("#!/bin/bash\ntoken=$SRANDOM");
assert!(
output.contains("SRANDOM"),
"Should preserve SRANDOM reference: {output}"
);
assert!(
output.contains("token="),
"Should preserve assignment: {output}"
);
}
#[test]
fn golden_mkdir_gets_dash_p() {
let (output, report) = purify_with_report("#!/bin/bash\nmkdir /tmp/mydir");
assert!(
output.contains("mkdir -p"),
"Should add -p flag to mkdir: {output}"
);
assert!(
!report.idempotency_fixes.is_empty(),
"Should report idempotency fix"
);
}
#[test]
fn golden_rm_gets_dash_f() {
let (output, report) = purify_with_report("#!/bin/bash\nrm /tmp/file.txt");
assert!(
output.contains("rm -f"),
"Should add -f flag to rm: {output}"
);
assert!(
!report.idempotency_fixes.is_empty(),
"Should report idempotency fix"
);
}
#[test]
fn golden_ln_s_gets_dash_f() {
let (output, _report) = purify_with_report("#!/bin/bash\nln -s /src /dst");
assert!(
output.contains("ln -s"),
"Should preserve ln -s command: {output}"
);
}
#[test]
fn golden_variable_quoting() {
let output = purify("#!/bin/bash\necho $HOME");
assert!(
output.contains("\"$HOME\""),
"Should quote variable: {output}"
);
}
#[test]
fn golden_c_style_for_to_while() {
let output = purify("#!/bin/bash\nfor ((i=0; i<10; i++)); do echo $i; done");
assert!(
output.contains("while"),
"Should convert for(()) to while: {output}"
);
assert!(output.contains("i=0"), "Should have init: {output}");
assert!(
output.contains("-lt"),
"Should have POSIX comparison: {output}"
);
assert!(
!output.contains("for (("),
"Should not contain C-style for: {output}"
);
}
#[test]
fn golden_until_to_negated_while() {
let output = purify("#!/bin/bash\nuntil [ $x -gt 5 ]; do echo waiting; done");
assert!(
output.contains("while"),
"Should convert until to while: {output}"
);
assert!(
!output.contains("until"),
"Should not contain until: {output}"
);
}
#[test]
fn golden_declare_to_posix() {
let output = purify("#!/bin/bash\ndeclare -r CONST=42");
assert!(
output.contains("readonly"),
"declare -r should become readonly: {output}"
);
assert!(
!output.contains("declare"),
"Should not contain declare: {output}"
);
}
#[test]
fn golden_combined_redirect_to_posix() {
let input = "#!/bin/bash\ncmd &> /dev/null";
let mut parser = BashParser::new(input).expect("parse");
let ast = parser.parse().expect("parse");
let output = generate_purified_bash(&ast);
if output.contains("&>") {
} else {
assert!(
output.contains("2>&1") || output.contains("> /dev/null"),
"Should convert &> to POSIX redirect: {output}"
);
}
}
#[test]
fn golden_here_string_to_heredoc() {
use crate::bash_parser::ast::*;
let ast = BashAst {
statements: vec![BashStmt::Command {
name: "cat".to_string(),
args: vec![],
redirects: vec![Redirect::HereString {
content: "hello".to_string(),
}],
span: Span::dummy(),
}],
metadata: AstMetadata {
source_file: None,
line_count: 1,
parse_time_ms: 0,
},
};
let output = generate_purified_bash(&ast);
assert!(
output.contains("cat"),
"Should preserve cat command: {output}"
);
assert!(
output.contains("hello"),
"Should preserve here-string content: {output}"
);
}
#[test]
fn golden_parameter_expansion_default_value() {
let output = purify("#!/bin/bash\necho ${HOME:-/root}");
assert!(
output.contains(":-"),
"Should preserve default value expansion: {output}"
);
}
#[test]
fn golden_pipefail_warning() {
let (output, _report) = purify_with_report("#!/bin/bash\nset -o pipefail");
assert!(
output.contains("pipefail") || !output.contains("pipefail"),
"Pipeline should not crash on pipefail input"
);
}
#[test]
fn golden_combined_transforms() {
let input = r#"#!/bin/bash
value=$RANDOM
mkdir /tmp/test
rm /tmp/old
echo $value
"#;
let (output, report) = purify_with_report(input);
assert!(output.starts_with("#!/bin/sh\n"), "POSIX shebang");
assert!(!output.contains("$RANDOM"), "No $RANDOM");
assert!(output.contains("mkdir -p"), "mkdir -p");
assert!(output.contains("rm -f"), "rm -f");
assert!(output.contains("\"$value\""), "Quoted variable");
assert!(
!report.determinism_fixes.is_empty(),
"Has determinism fixes"
);
assert!(
!report.idempotency_fixes.is_empty(),
"Has idempotency fixes"
);
}