#![allow(clippy::unwrap_used)] #![allow(clippy::expect_used)]
use bashrs::make_parser::{
generators::{generate_purified_makefile_with_options, MakefileGeneratorOptions},
parser::parse_makefile,
purify::purify_makefile,
};
use proptest::prelude::*;
fn makefile_strategy() -> impl Strategy<Value = String> {
prop::collection::vec(
prop::string::string_regex("[a-zA-Z_][a-zA-Z0-9_]*").unwrap(),
1..5,
)
.prop_map(|targets| {
let mut makefile = String::new();
for target in targets {
makefile.push_str(&format!(".PHONY: {}\n", target));
makefile.push_str(&format!("{}:\n", target));
makefile.push_str("\t@echo \"Building\"\n");
makefile.push_str("\t@echo \"Done\"\n");
makefile.push('\n');
}
makefile
})
}
proptest! {
#[test]
fn prop_preserve_formatting_always_adds_blank_lines(makefile in makefile_strategy()) {
let ast = parse_makefile(&makefile).expect("Valid makefile should parse");
let purified = purify_makefile(&ast);
let options_compact = MakefileGeneratorOptions::default();
let output_compact = generate_purified_makefile_with_options(&purified.ast, &options_compact);
let options_preserve = MakefileGeneratorOptions {
preserve_formatting: true,
..Default::default()
};
let output_preserve = generate_purified_makefile_with_options(&purified.ast, &options_preserve);
let blank_lines_compact = output_compact.matches("\n\n").count();
let blank_lines_preserve = output_preserve.matches("\n\n").count();
prop_assert!(
blank_lines_preserve >= blank_lines_compact,
"Preserve formatting should have >= blank lines. Compact: {}, Preserve: {}",
blank_lines_compact,
blank_lines_preserve
);
}
#[test]
fn prop_max_line_length_always_respected(
makefile in makefile_strategy(),
max_len in 40usize..120usize
) {
let ast = parse_makefile(&makefile).expect("Valid makefile should parse");
let purified = purify_makefile(&ast);
let options = MakefileGeneratorOptions {
max_line_length: Some(max_len),
..Default::default()
};
let output = generate_purified_makefile_with_options(&purified.ast, &options);
for line in output.lines() {
prop_assert!(
line.len() <= max_len,
"Line exceeds max length {}: {} chars: '{}'",
max_len,
line.len(),
line
);
}
}
#[test]
fn prop_skip_blank_line_removal_preserves_structure(makefile in makefile_strategy()) {
let ast = parse_makefile(&makefile).expect("Valid makefile should parse");
let purified = purify_makefile(&ast);
let options = MakefileGeneratorOptions {
skip_blank_line_removal: true,
..Default::default()
};
let output = generate_purified_makefile_with_options(&purified.ast, &options);
prop_assert!(
output.contains("\n\n"),
"Expected blank lines with skip_blank_line_removal"
);
}
#[test]
fn prop_combined_options_work_together(
makefile in makefile_strategy(),
max_len in 60usize..100usize
) {
let ast = parse_makefile(&makefile).expect("Valid makefile should parse");
let purified = purify_makefile(&ast);
let options = MakefileGeneratorOptions {
preserve_formatting: true,
max_line_length: Some(max_len),
skip_blank_line_removal: true,
skip_consolidation: false, };
let output = generate_purified_makefile_with_options(&purified.ast, &options);
for line in output.lines() {
prop_assert!(
line.len() <= max_len,
"Line exceeds max length: {}",
line.len()
);
}
prop_assert!(
output.contains("\n\n"),
"Expected blank lines with combined options"
);
}
#[test]
fn prop_output_is_deterministic(makefile in makefile_strategy()) {
let ast = parse_makefile(&makefile).expect("Valid makefile should parse");
let purified = purify_makefile(&ast);
let options = MakefileGeneratorOptions {
preserve_formatting: true,
max_line_length: Some(80),
..Default::default()
};
let output1 = generate_purified_makefile_with_options(&purified.ast, &options);
let output2 = generate_purified_makefile_with_options(&purified.ast, &options);
prop_assert_eq!(
output1,
output2,
"Same input should produce identical output"
);
}
#[test]
fn prop_output_is_valid_makefile_syntax(makefile in makefile_strategy()) {
let ast = parse_makefile(&makefile).expect("Valid makefile should parse");
let purified = purify_makefile(&ast);
let options = MakefileGeneratorOptions {
preserve_formatting: true,
..Default::default()
};
let output = generate_purified_makefile_with_options(&purified.ast, &options);
let reparse_result = parse_makefile(&output);
prop_assert!(
reparse_result.is_ok(),
"Generated output should be parseable: {:?}",
reparse_result.err()
);
}
#[test]
fn prop_line_breaks_preserve_tabs(makefile in makefile_strategy()) {
let ast = parse_makefile(&makefile).expect("Valid makefile should parse");
let purified = purify_makefile(&ast);
let options = MakefileGeneratorOptions {
max_line_length: Some(40), ..Default::default()
};
let output = generate_purified_makefile_with_options(&purified.ast, &options);
for line in output.lines() {
if line.trim_start().starts_with("@echo") || line.trim_start().starts_with("echo") {
prop_assert!(
line.starts_with('\t'),
"Recipe line should start with tab: '{}'",
line
);
}
}
}
}
#[cfg(test)]
mod config {
use super::*;
#[test]
fn test_proptest_runs() {
let config = ProptestConfig {
cases: 100, max_shrink_iters: 1000,
..Default::default()
};
proptest!(config, |(makefile in makefile_strategy())| {
prop_assert!(!makefile.is_empty());
});
}
}