use std::path::Path;
#[derive(Debug, Clone)]
pub struct TestGeneratorOptions {
pub property_tests: bool,
pub property_test_count: usize,
}
impl Default for TestGeneratorOptions {
fn default() -> Self {
Self {
property_tests: false,
property_test_count: 100,
}
}
}
pub struct TestGenerator {
options: TestGeneratorOptions,
}
impl TestGenerator {
pub fn new(options: TestGeneratorOptions) -> Self {
Self { options }
}
#[allow(clippy::expect_used)]
fn get_script_name(script_path: &Path) -> &str {
script_path
.file_name()
.expect("Script path should have a file name")
.to_str()
.expect("File name should be valid UTF-8")
}
pub fn generate_tests(&self, script_path: &Path, _purified_content: &str) -> String {
let mut test_suite = String::new();
test_suite.push_str("#!/bin/sh\n");
test_suite.push_str("# Test Suite for ");
test_suite.push_str(Self::get_script_name(script_path));
test_suite.push('\n');
test_suite.push_str("# Generated by bashrs purify --with-tests\n\n");
test_suite.push_str("set -e # Exit on first failure\n\n");
test_suite.push_str(&self.generate_determinism_test(script_path));
test_suite.push('\n');
test_suite.push_str(&self.generate_idempotency_test(script_path));
test_suite.push('\n');
test_suite.push_str(&self.generate_posix_compliance_test(script_path));
test_suite.push('\n');
if self.options.property_tests {
test_suite.push_str(&self.generate_property_determinism_test(script_path));
test_suite.push('\n');
}
test_suite.push_str(&self.generate_test_runner());
test_suite
}
fn generate_determinism_test(&self, script_path: &Path) -> String {
let script_name = Self::get_script_name(script_path);
format!(
r#"# Test 1: Determinism - Same inputs produce same outputs
test_determinism() {{
echo "Testing: Determinism..."
# Run script twice with same inputs
output1=$(./{} 2>&1 || true)
output2=$(./{} 2>&1 || true)
if [ "$output1" = "$output2" ]; then
echo " ✅ PASS: Outputs are identical"
return 0
else
echo " ❌ FAIL: Outputs differ"
echo " Run 1: $output1"
echo " Run 2: $output2"
return 1
fi
}}
"#,
script_name, script_name
)
}
fn generate_idempotency_test(&self, script_path: &Path) -> String {
let script_name = Self::get_script_name(script_path);
format!(
r#"# Test 2: Idempotency - Safe to re-run
test_idempotency() {{
echo "Testing: Idempotency..."
# First run
./{} >/dev/null 2>&1 || {{
echo " ⚠️ SKIP: Script requires specific setup"
return 0
}}
# Second run (should not fail)
./{} >/dev/null 2>&1 || {{
echo " ❌ FAIL: Second run failed (not idempotent)"
return 1
}}
echo " ✅ PASS: Script is idempotent"
return 0
}}
"#,
script_name, script_name
)
}
fn generate_posix_compliance_test(&self, script_path: &Path) -> String {
let script_name = Self::get_script_name(script_path);
format!(
r#"# Test 3: POSIX Compliance
test_posix_compliance() {{
echo "Testing: POSIX Compliance..."
if command -v shellcheck >/dev/null 2>&1; then
shellcheck -s sh ./{} || {{
echo " ❌ FAIL: shellcheck found POSIX violations"
return 1
}}
echo " ✅ PASS: POSIX compliant (shellcheck)"
else
echo " ⚠️ SKIP: shellcheck not installed"
fi
return 0
}}
"#,
script_name
)
}
fn generate_property_determinism_test(&self, script_path: &Path) -> String {
let script_name = Self::get_script_name(script_path);
let test_count = self.options.property_test_count;
format!(
r#"# Property Test 4: Determinism holds for all inputs ({} cases)
test_property_determinism() {{
echo "Testing: Determinism Property ({} cases)..."
passed=0
failed=0
for i in $(seq 1 {}); do
# Generate test input
test_input="test_case_$i"
# Run script twice with same input
output1=$(./{} "$test_input" 2>&1 || true)
output2=$(./{} "$test_input" 2>&1 || true)
if [ "$output1" = "$output2" ]; then
passed=$((passed + 1))
else
failed=$((failed + 1))
echo " ❌ Case $i failed (input=$test_input)"
fi
done
if [ $failed -eq 0 ]; then
echo " ✅ PASS: Determinism property ($passed/{} cases)"
return 0
else
echo " ❌ FAIL: $failed/{} cases failed"
return 1
fi
}}
"#,
test_count, test_count, test_count, script_name, script_name, test_count, test_count
)
}
fn generate_test_runner(&self) -> String {
let mut runner = String::new();
runner.push_str("# Test Runner\n");
runner.push_str("echo \"================================================\"\n");
runner.push_str("echo \"bashrs purify --with-tests\"\n");
runner.push_str("echo \"Date: $(date -u +%Y-%m-%dT%H:%M:%SZ)\"\n");
runner.push_str("echo \"================================================\"\n");
runner.push_str("echo \"\"\n\n");
runner.push_str("# Run all tests\n");
runner.push_str("failed_tests=\"\"\n\n");
runner.push_str("test_determinism || failed_tests=\"$failed_tests test_determinism\"\n");
runner.push_str("test_idempotency || failed_tests=\"$failed_tests test_idempotency\"\n");
runner.push_str(
"test_posix_compliance || failed_tests=\"$failed_tests test_posix_compliance\"\n",
);
if self.options.property_tests {
runner.push_str("test_property_determinism || failed_tests=\"$failed_tests test_property_determinism\"\n");
}
runner.push_str("\necho \"\"\n");
runner.push_str("echo \"================================================\"\n\n");
runner.push_str("if [ -z \"$failed_tests\" ]; then\n");
runner.push_str(" echo \"✅ All tests passed!\"\n");
runner.push_str(" exit 0\n");
runner.push_str("else\n");
runner.push_str(" echo \"❌ Failed tests:$failed_tests\"\n");
runner.push_str(" exit 1\n");
runner.push_str("fi\n");
runner
}
}