use std::path::Path;
#[derive(Debug, Clone)]
pub struct MakefileTestGeneratorOptions {
pub property_tests: bool,
pub property_test_count: usize,
}
impl Default for MakefileTestGeneratorOptions {
fn default() -> Self {
Self {
property_tests: false,
property_test_count: 100,
}
}
}
pub struct MakefileTestGenerator {
options: MakefileTestGeneratorOptions,
}
impl MakefileTestGenerator {
pub fn new(options: MakefileTestGeneratorOptions) -> Self {
Self { options }
}
#[allow(clippy::expect_used)]
fn get_makefile_name(makefile_path: &Path) -> &str {
makefile_path
.file_name()
.expect("Makefile path should have a file name")
.to_str()
.expect("File name should be valid UTF-8")
}
pub fn generate_tests(&self, makefile_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_makefile_name(makefile_path));
test_suite.push('\n');
test_suite.push_str("# Generated by bashrs make 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(makefile_path));
test_suite.push('\n');
test_suite.push_str(&self.generate_idempotency_test(makefile_path));
test_suite.push('\n');
test_suite.push_str(&self.generate_posix_compliance_test(makefile_path));
test_suite.push('\n');
if self.options.property_tests {
test_suite.push_str(&self.generate_property_determinism_test(makefile_path));
test_suite.push('\n');
}
test_suite.push_str(&self.generate_test_runner());
test_suite
}
fn generate_determinism_test(&self, makefile_path: &Path) -> String {
let makefile_name = Self::get_makefile_name(makefile_path);
format!(
r#"# Test: Determinism - same make invocation produces same output
test_determinism() {{
printf "Testing determinism for {}...\n"
# Run make twice and capture output
make -f "{}" > /tmp/output1.txt 2>&1 || true
make -f "{}" > /tmp/output2.txt 2>&1 || true
# Sort outputs before comparing (handles make's parallel execution)
sort /tmp/output1.txt > /tmp/output1_sorted.txt
sort /tmp/output2.txt > /tmp/output2_sorted.txt
# Compare sorted outputs
if diff /tmp/output1_sorted.txt /tmp/output2_sorted.txt > /dev/null; then
printf "✓ Determinism test passed\n"
rm -f /tmp/output1.txt /tmp/output2.txt /tmp/output1_sorted.txt /tmp/output2_sorted.txt
return 0
else
printf "✗ Determinism test failed - outputs differ\n"
printf "First run (sorted):\n"
cat /tmp/output1_sorted.txt
printf "\nSecond run (sorted):\n"
cat /tmp/output2_sorted.txt
rm -f /tmp/output1.txt /tmp/output2.txt /tmp/output1_sorted.txt /tmp/output2_sorted.txt
return 1
fi
}}
"#,
makefile_name, makefile_name, makefile_name
)
}
fn generate_idempotency_test(&self, makefile_path: &Path) -> String {
let makefile_name = Self::get_makefile_name(makefile_path);
format!(
r#"# Test: Idempotency - safe to re-run multiple times
test_idempotency() {{
printf "Testing idempotency for {}...\n"
# Run make three times
make -f "{}" > /dev/null 2>&1 || true
make -f "{}" > /dev/null 2>&1 || exit_code1=$?
make -f "{}" > /dev/null 2>&1 || exit_code2=$?
# Second and third runs should succeed (exit code 0)
if [ "${{exit_code1:-0}}" -eq 0 ] && [ "${{exit_code2:-0}}" -eq 0 ]; then
printf "✓ Idempotency test passed\n"
return 0
else
printf "✗ Idempotency test failed - not safe to re-run\n"
return 1
fi
}}
"#,
makefile_name, makefile_name, makefile_name, makefile_name
)
}
fn generate_posix_compliance_test(&self, makefile_path: &Path) -> String {
let makefile_name = Self::get_makefile_name(makefile_path);
format!(
r#"# Test: POSIX Compliance - Makefile is POSIX-compatible
test_posix_compliance() {{
printf "Testing POSIX compliance for {}...\n"
# Check if Makefile can be parsed by POSIX make
# (Most systems have GNU make, so we just verify it doesn't error)
if make -f "{}" --version > /dev/null 2>&1; then
printf "✓ POSIX compliance test passed\n"
return 0
else
printf "⚠ Could not verify POSIX compliance (make may not be available)\n"
return 0 # Don't fail if make is not available
fi
}}
"#,
makefile_name, makefile_name
)
}
fn generate_property_determinism_test(&self, makefile_path: &Path) -> String {
let makefile_name = Self::get_makefile_name(makefile_path);
let count = self.options.property_test_count;
format!(
r#"# Test: Property-Based Determinism - {count} test cases
test_property_determinism() {{
printf "Testing property-based determinism ({count} cases) for {}...\n"
failed=0
passed=0
# Run make multiple times and verify determinism
i=1
while [ "$i" -le {count} ]; do
make -f "{}" > "/tmp/prop_output_${{i}}.txt" 2>&1 || true
sort "/tmp/prop_output_${{i}}.txt" > "/tmp/prop_output_${{i}}_sorted.txt"
if [ "$i" -gt 1 ]; then
if diff "/tmp/prop_output_1_sorted.txt" "/tmp/prop_output_${{i}}_sorted.txt" > /dev/null 2>&1; then
passed=$((passed + 1))
else
failed=$((failed + 1))
fi
fi
i=$((i + 1))
done
# Cleanup
rm -f /tmp/prop_output_*.txt /tmp/prop_output_*_sorted.txt
if [ "$failed" -eq 0 ]; then
printf "✓ Property-based determinism test passed ($passed/{count} cases)\n"
return 0
else
printf "✗ Property-based determinism test failed ($failed/{count} cases)\n"
return 1
fi
}}
"#,
makefile_name, makefile_name
)
}
fn generate_test_runner(&self) -> String {
let mut script = String::from("# Test Runner\nrun_all_tests() {\n");
script.push_str(" printf \"\\n=== Running Test Suite ===\\n\\n\"\n\n");
script.push_str(" failed=0\n passed=0\n\n");
script.push_str(&self.generate_test_invocation("determinism"));
script.push_str(&self.generate_test_invocation("idempotency"));
script.push_str(&self.generate_test_invocation("posix_compliance"));
if self.options.property_tests {
script.push_str(&self.generate_test_invocation("property_determinism"));
}
script.push_str(&self.generate_test_summary());
script.push_str("}\n\n# Run tests\nrun_all_tests\n");
script
}
fn generate_test_invocation(&self, test_name: &str) -> String {
format!(
r#" # Run {0} test
if test_{0}; then
passed=$((passed + 1))
else
failed=$((failed + 1))
fi
printf "\n"
"#,
test_name
)
}
fn generate_test_summary(&self) -> String {
r#" printf "\n=== Test Summary ===\n"
printf "Passed: %d\n" "$passed"
printf "Failed: %d\n" "$failed"
if [ "$failed" -eq 0 ]; then
printf "\n✓ All tests passed!\n"
return 0
else
printf "\n✗ Some tests failed\n"
return 1
fi
"#
.to_string()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
use super::*;
use std::path::PathBuf;
#[test]
fn test_options_default_property_tests_disabled() {
let opts = MakefileTestGeneratorOptions::default();
assert!(!opts.property_tests);
}
#[test]
fn test_options_default_property_test_count() {
let opts = MakefileTestGeneratorOptions::default();
assert_eq!(opts.property_test_count, 100);
}
#[test]
fn test_options_custom_values() {
let opts = MakefileTestGeneratorOptions {
property_tests: true,
property_test_count: 50,
};
assert!(opts.property_tests);
assert_eq!(opts.property_test_count, 50);
}
#[test]
fn test_options_clone() {
let opts = MakefileTestGeneratorOptions {
property_tests: true,
property_test_count: 200,
};
let cloned = opts.clone();
assert_eq!(cloned.property_tests, opts.property_tests);
assert_eq!(cloned.property_test_count, opts.property_test_count);
}
#[test]
fn test_options_debug() {
let opts = MakefileTestGeneratorOptions::default();
let dbg = format!("{opts:?}");
assert!(dbg.contains("MakefileTestGeneratorOptions"));
}
#[test]
fn test_generator_new() {
let opts = MakefileTestGeneratorOptions::default();
let gen = MakefileTestGenerator::new(opts);
assert!(!gen.options.property_tests);
}
#[test]
fn test_get_makefile_name_simple() {
let path = PathBuf::from("Makefile");
assert_eq!(MakefileTestGenerator::get_makefile_name(&path), "Makefile");
}
#[test]
fn test_get_makefile_name_with_directory() {
let path = PathBuf::from("/home/user/project/Makefile.purified");
assert_eq!(
MakefileTestGenerator::get_makefile_name(&path),
"Makefile.purified"
);
}
#[test]
fn test_get_makefile_name_nested() {
let path = PathBuf::from("a/b/c/GNUmakefile");
assert_eq!(
MakefileTestGenerator::get_makefile_name(&path),
"GNUmakefile"
);
}
#[test]
fn test_generate_tests_contains_shebang() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let path = PathBuf::from("Makefile");
let output = gen.generate_tests(&path, "all:\n\techo hello");
assert!(output.starts_with("#!/bin/sh"));
}
#[test]
fn test_generate_tests_contains_header() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let path = PathBuf::from("Makefile");
let output = gen.generate_tests(&path, "");
assert!(output.contains("Test Suite for Makefile"));
assert!(output.contains("Generated by bashrs"));
}
#[test]
fn test_generate_tests_contains_set_e() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let path = PathBuf::from("Makefile");
let output = gen.generate_tests(&path, "");
assert!(output.contains("set -e"));
}
#[test]
fn test_generate_tests_contains_determinism_test() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let path = PathBuf::from("Makefile");
let output = gen.generate_tests(&path, "");
assert!(output.contains("test_determinism"));
}
#[test]
fn test_generate_tests_contains_idempotency_test() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let path = PathBuf::from("Makefile");
let output = gen.generate_tests(&path, "");
assert!(output.contains("test_idempotency"));
}
#[test]
fn test_generate_tests_contains_posix_compliance_test() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let path = PathBuf::from("Makefile");
let output = gen.generate_tests(&path, "");
assert!(output.contains("test_posix_compliance"));
}
#[test]
fn test_generate_tests_no_property_tests_by_default() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let path = PathBuf::from("Makefile");
let output = gen.generate_tests(&path, "");
assert!(
!output.contains("test_property_determinism"),
"Property tests should not be included by default"
);
}
#[test]
fn test_generate_tests_contains_test_runner() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let path = PathBuf::from("Makefile");
let output = gen.generate_tests(&path, "");
assert!(output.contains("run_all_tests"));
}
#[test]
fn test_generate_tests_with_property_tests() {
let opts = MakefileTestGeneratorOptions {
property_tests: true,
property_test_count: 50,
};
let gen = MakefileTestGenerator::new(opts);
let path = PathBuf::from("Makefile");
let output = gen.generate_tests(&path, "");
assert!(
output.contains("test_property_determinism"),
"Property tests should be included when enabled"
);
assert!(output.contains("50"), "Should use custom test count");
}
#[test]
fn test_generate_tests_property_tests_in_runner() {
let opts = MakefileTestGeneratorOptions {
property_tests: true,
property_test_count: 100,
};
let gen = MakefileTestGenerator::new(opts);
let path = PathBuf::from("Makefile");
let output = gen.generate_tests(&path, "");
let runner_portion = output.split("run_all_tests").collect::<Vec<_>>();
assert!(
runner_portion.len() >= 2,
"Should have run_all_tests section"
);
assert!(
runner_portion[1].contains("property_determinism"),
"Runner should include property test"
);
}
#[test]
fn test_determinism_test_references_makefile() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let path = PathBuf::from("MyMakefile");
let output = gen.generate_determinism_test(&path);
assert!(output.contains("MyMakefile"));
assert!(output.contains("Testing determinism"));
assert!(output.contains("make -f"));
assert!(output.contains("diff"));
}
#[test]
fn test_determinism_test_cleanup() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let path = PathBuf::from("Makefile");
let output = gen.generate_determinism_test(&path);
assert!(output.contains("rm -f"), "Should clean up temp files");
}
#[test]
fn test_idempotency_test_runs_three_times() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let path = PathBuf::from("Makefile");
let output = gen.generate_idempotency_test(&path);
let make_count = output.matches("make -f").count();
assert_eq!(make_count, 3, "Should run make 3 times");
}
#[test]
fn test_idempotency_test_checks_exit_codes() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let path = PathBuf::from("Makefile");
let output = gen.generate_idempotency_test(&path);
assert!(output.contains("exit_code1"));
assert!(output.contains("exit_code2"));
}
#[test]
fn test_posix_compliance_test_content() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let path = PathBuf::from("Makefile");
let output = gen.generate_posix_compliance_test(&path);
assert!(output.contains("POSIX compliance"));
assert!(output.contains("make -f"));
}
#[test]
fn test_posix_compliance_test_graceful_fallback() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let path = PathBuf::from("Makefile");
let output = gen.generate_posix_compliance_test(&path);
assert!(
output.contains("return 0"),
"Should return 0 even if make is unavailable"
);
}
#[test]
fn test_property_determinism_test_count() {
let opts = MakefileTestGeneratorOptions {
property_tests: true,
property_test_count: 75,
};
let gen = MakefileTestGenerator::new(opts);
let path = PathBuf::from("Makefile");
let output = gen.generate_property_determinism_test(&path);
assert!(output.contains("75"), "Should use count 75");
assert!(output.contains("test_property_determinism"));
assert!(output.contains("while"));
}
#[test]
fn test_property_determinism_test_cleanup() {
let opts = MakefileTestGeneratorOptions {
property_tests: true,
property_test_count: 10,
};
let gen = MakefileTestGenerator::new(opts);
let path = PathBuf::from("Makefile");
let output = gen.generate_property_determinism_test(&path);
assert!(output.contains("rm -f"), "Should clean up temp files");
}
#[test]
fn test_runner_structure() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let output = gen.generate_test_runner();
assert!(output.contains("run_all_tests"));
assert!(output.contains("Test Suite"));
assert!(output.contains("passed=0"));
assert!(output.contains("failed=0"));
}
#[test]
fn test_runner_includes_core_tests() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let output = gen.generate_test_runner();
assert!(output.contains("test_determinism"));
assert!(output.contains("test_idempotency"));
assert!(output.contains("test_posix_compliance"));
}
#[test]
fn test_runner_with_property_tests() {
let opts = MakefileTestGeneratorOptions {
property_tests: true,
property_test_count: 100,
};
let gen = MakefileTestGenerator::new(opts);
let output = gen.generate_test_runner();
assert!(output.contains("test_property_determinism"));
}
#[test]
fn test_runner_without_property_tests() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let output = gen.generate_test_runner();
assert!(!output.contains("test_property_determinism"));
}
#[test]
fn test_invocation_format() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let output = gen.generate_test_invocation("my_test");
assert!(output.contains("test_my_test"));
assert!(output.contains("passed=$((passed + 1))"));
assert!(output.contains("failed=$((failed + 1))"));
}
#[test]
fn test_summary_content() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
let output = gen.generate_test_summary();
assert!(output.contains("Test Summary"));
assert!(output.contains("Passed"));
assert!(output.contains("Failed"));
assert!(output.contains("All tests passed"));
assert!(output.contains("Some tests failed"));
}
#[test]
fn test_full_output_is_valid_shell() {
let opts = MakefileTestGeneratorOptions {
property_tests: true,
property_test_count: 5,
};
let gen = MakefileTestGenerator::new(opts);
let path = PathBuf::from("/tmp/Makefile.purified");
let content = "all:\n\techo hello\nclean:\n\trm -f *.o";
let output = gen.generate_tests(&path, content);
assert!(output.starts_with("#!/bin/sh"));
assert!(output.contains("set -e"));
assert!(output.ends_with("run_all_tests\n"));
assert!(output.contains("test_determinism()"));
assert!(output.contains("test_idempotency()"));
assert!(output.contains("test_posix_compliance()"));
assert!(output.contains("test_property_determinism()"));
assert!(output.contains("Makefile.purified"));
}
#[test]
fn test_different_makefile_names() {
let gen = MakefileTestGenerator::new(MakefileTestGeneratorOptions::default());
for name in &["Makefile", "GNUmakefile", "makefile.mk", "build.mk"] {
let path = PathBuf::from(name);
let output = gen.generate_tests(&path, "");
assert!(
output.contains(name),
"Output should reference makefile name '{name}'"
);
}
}
}