use super::purification::{PurificationOptions, Purifier};
use crate::bash_parser::codegen::generate_purified_bash;
use crate::bash_parser::parser::BashParser;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig {
cases: 100,
max_shrink_iters: 1000,
.. ProptestConfig::default()
})]
#[test]
fn prop_purification_is_deterministic(bash_code in "#!/bin/bash\n[a-z_][a-z0-9_]{0,10}=[0-9]{1,3}") {
let mut parser1 = BashParser::new(&bash_code).unwrap();
let ast1 = parser1.parse().unwrap();
let mut parser2 = BashParser::new(&bash_code).unwrap();
let ast2 = parser2.parse().unwrap();
let mut purifier1 = Purifier::new(PurificationOptions::default());
let purified_ast1 = purifier1.purify(&ast1).unwrap();
let mut purifier2 = Purifier::new(PurificationOptions::default());
let purified_ast2 = purifier2.purify(&ast2).unwrap();
let output1 = generate_purified_bash(&purified_ast1);
let output2 = generate_purified_bash(&purified_ast2);
prop_assert_eq!(output1, output2, "Purification must be deterministic");
}
#[test]
fn prop_purification_is_idempotent(bash_code in "#!/bin/bash\n[a-z_][a-z0-9_]{0,10}=[0-9]{1,3}") {
let mut parser1 = BashParser::new(&bash_code).unwrap();
let ast1 = parser1.parse().unwrap();
let mut purifier1 = Purifier::new(PurificationOptions::default());
let purified_ast1 = purifier1.purify(&ast1).unwrap();
let output1 = generate_purified_bash(&purified_ast1);
let mut parser2 = BashParser::new(&output1).unwrap();
let ast2 = parser2.parse().unwrap();
let mut purifier2 = Purifier::new(PurificationOptions::default());
let purified_ast2 = purifier2.purify(&ast2).unwrap();
let output2 = generate_purified_bash(&purified_ast2);
prop_assert_eq!(output1, output2, "Purification must be idempotent");
}
#[test]
fn prop_purified_has_posix_shebang(bash_code in "#!/bin/bash\n[a-z_][a-z0-9_]{0,10}=[0-9]{1,3}") {
let mut parser = BashParser::new(&bash_code).unwrap();
let ast = parser.parse().unwrap();
let mut purifier = Purifier::new(PurificationOptions::default());
let purified_ast = purifier.purify(&ast).unwrap();
let output = generate_purified_bash(&purified_ast);
prop_assert!(
output.starts_with("#!/bin/sh"),
"Purified output must have POSIX shebang, got: {}",
output.lines().next().unwrap_or("")
);
}
#[test]
fn prop_variable_assignments_preserved(
var_name in "[a-z_][a-z0-9_]{0,10}",
value in "[1-9][0-9]{0,2}" ) {
let bash_code = format!("#!/bin/bash\n{}={}", var_name, value);
let mut parser = BashParser::new(&bash_code).unwrap();
let ast = parser.parse().unwrap();
let mut purifier = Purifier::new(PurificationOptions::default());
let purified_ast = purifier.purify(&ast).unwrap();
let output = generate_purified_bash(&purified_ast);
let expected = format!("{}={}", var_name, value);
prop_assert!(
output.contains(&expected),
"Variable assignment {} should be preserved in output:\n{}",
expected, output
);
}
#[test]
fn prop_no_random_in_purified_output(
var_name in "[a-z_][a-z0-9_]{0,10}"
) {
let bash_code = format!("#!/bin/bash\n{}=$RANDOM", var_name);
if let Ok(mut parser) = BashParser::new(&bash_code) {
if let Ok(ast) = parser.parse() {
let mut purifier = Purifier::new(PurificationOptions::default());
if let Ok(purified_ast) = purifier.purify(&ast) {
let output = generate_purified_bash(&purified_ast);
prop_assert!(
!output.contains("$RANDOM") && !output.contains("${RANDOM}"),
"Purified output must not contain $RANDOM, got:\n{}",
output
);
}
}
}
}
#[test]
fn prop_comments_preserved(
comment_text in "[a-zA-Z0-9 ]{1,20}"
) {
let bash_code = format!("#!/bin/bash\n# {}\nx=42", comment_text);
let mut parser = BashParser::new(&bash_code).unwrap();
let ast = parser.parse().unwrap();
let mut purifier = Purifier::new(PurificationOptions::default());
let purified_ast = purifier.purify(&ast).unwrap();
let output = generate_purified_bash(&purified_ast);
prop_assert!(
output.contains(&comment_text),
"Comment text '{}' should be preserved in output:\n{}",
comment_text, output
);
}
#[test]
fn prop_empty_script_valid(_input in "#!/bin/bash\n") {
let bash_code = "#!/bin/bash\n";
let mut parser = BashParser::new(bash_code).unwrap();
let ast = parser.parse().unwrap();
let mut purifier = Purifier::new(PurificationOptions::default());
let purified_ast = purifier.purify(&ast).unwrap();
let output = generate_purified_bash(&purified_ast);
prop_assert!(
output.starts_with("#!/bin/sh"),
"Empty purified script must have POSIX shebang"
);
prop_assert!(
output.len() >= "#!/bin/sh\n".len(),
"Purified output should not be empty"
);
}
#[test]
fn prop_multiple_assignments_preserved(
var1 in "[a-z]",
val1 in "[1-9]",
var2 in "[a-z]",
val2 in "[1-9]"
) {
prop_assume!(var1 != var2 || val1 != val2);
let bash_code = format!("#!/bin/bash\n{}={}\n{}={}", var1, val1, var2, val2);
let mut parser = BashParser::new(&bash_code).unwrap();
let ast = parser.parse().unwrap();
let mut purifier = Purifier::new(PurificationOptions::default());
let purified_ast = purifier.purify(&ast).unwrap();
let output = generate_purified_bash(&purified_ast);
prop_assert!(
output.contains(&format!("{}={}", var1, val1)),
"First assignment {}={} should be preserved",
var1, val1
);
prop_assert!(
output.contains(&format!("{}={}", var2, val2)),
"Second assignment {}={} should be preserved",
var2, val2
);
if var1 != var2 || val1 != val2 {
let assignment1 = format!("{}={}", var1, val1);
let assignment2 = format!("{}={}", var2, val2);
let pos1 = output.find(&assignment1);
let pos2 = output.rfind(&assignment2);
if let (Some(p1), Some(p2)) = (pos1, pos2) {
if assignment1 != assignment2 {
prop_assert!(
p1 < p2,
"Assignment order should be preserved: {} before {}",
var1, var2
);
}
}
}
}
}
#[cfg(test)]
mod purification_property_tests_prop_stateme {
use super::*;
use proptest::prelude::*;
include!("purification_property_tests_prop_stateme.rs");
}