bashrs 6.66.0

Rust-to-Shell transpiler for deterministic bootstrap scripts
#[cfg(test)]
mod prop_expansion_tests {
    use super::*;
    use crate::bash_parser::ast::*;
    use proptest::prelude::*;

    // Property: Glob patterns are preserved
    proptest! {
        #[test]
        fn prop_glob_patterns_preserved(
            var_name in "[a-z][a-z0-9]{0,5}",
            extension in "txt|log|md|rs"
        ) {
            let glob_pattern = format!("*.{}", extension);

            let ast = BashAst {
                statements: vec![BashStmt::For {
                    variable: var_name.clone(),
                    items: BashExpr::Glob(glob_pattern.clone()),
                    body: vec![BashStmt::Command {
                        name: "echo".to_string(),
                        args: vec![BashExpr::Variable(var_name.clone())],
                        redirects: vec![],
                        span: Span::dummy(),
                    }],
                    span: Span::dummy(),
                }],
                metadata: AstMetadata {
                    source_file: None,
                    line_count: 1,
                    parse_time_ms: 0,
                },
            };

            let purified = generators::generate_purified_bash(&ast);

            // PROPERTY: Glob pattern must be preserved
            prop_assert!(
                purified.contains(&glob_pattern),
                "Purified output must preserve glob pattern '{}', got: {}",
                glob_pattern,
                purified
            );

            // PROPERTY: For loop structure must be present
            prop_assert!(
                purified.contains("for") && purified.contains("in") && purified.contains("do") && purified.contains("done"),
                "Purified output must contain for loop structure, got: {}",
                purified
            );
        }
    }

    // Property: Glob transformation is deterministic
    proptest! {
        #[test]
        fn prop_glob_transformation_is_deterministic(
            pattern in "[*?\\[\\]a-z.]+{1,10}"
        ) {
            let ast = BashAst {
                statements: vec![BashStmt::For {
                    variable: "f".to_string(),
                    items: BashExpr::Glob(pattern.clone()),
                    body: vec![BashStmt::Command {
                        name: "echo".to_string(),
                        args: vec![BashExpr::Variable("f".to_string())],
                        redirects: vec![],
                        span: Span::dummy(),
                    }],
                    span: Span::dummy(),
                }],
                metadata: AstMetadata {
                    source_file: None,
                    line_count: 1,
                    parse_time_ms: 0,
                },
            };

            // Generate twice
            let purified1 = generators::generate_purified_bash(&ast);
            let purified2 = generators::generate_purified_bash(&ast);

            // PROPERTY: Determinism - byte-identical output
            prop_assert_eq!(
                purified1,
                purified2,
                "Glob transformation must be deterministic"
            );
        }
    }

    // Property: Glob patterns with different wildcards
    proptest! {
        #[test]
        fn prop_glob_wildcards_preserved(
            prefix in "[a-z]{1,5}",
            wildcard in "\\*|\\?|\\[0-9\\]"
        ) {
            let pattern = format!("{}{}", prefix, wildcard);

            let ast = BashAst {
                statements: vec![BashStmt::Command {
                    name: "ls".to_string(),
                    args: vec![BashExpr::Glob(pattern.clone())],
                    redirects: vec![],
                    span: Span::dummy(),
                }],
                metadata: AstMetadata {
                    source_file: None,
                    line_count: 1,
                    parse_time_ms: 0,
                },
            };

            let purified = generators::generate_purified_bash(&ast);

            // PROPERTY: Pattern must be in output
            prop_assert!(
                purified.contains(&prefix),
                "Purified output must contain prefix '{}', got: {}",
                prefix,
                purified
            );
        }
    }

    // Property: Error-if-unset expansion preserves variable and message
    proptest! {
        #[test]
        fn prop_error_if_unset_preserves_components(
            var_name in "[a-zA-Z_][a-zA-Z0-9_]{0,10}",
            error_msg in "[a-zA-Z ]{5,30}"
        ) {
            let ast = BashAst {
                statements: vec![BashStmt::Command {
                    name: "echo".to_string(),
                    args: vec![BashExpr::ErrorIfUnset {
                        variable: var_name.clone(),
                        message: Box::new(BashExpr::Literal(error_msg.clone())),
                    }],
                    redirects: vec![],
                    span: Span::dummy(),
                }],
                metadata: AstMetadata {
                    source_file: None,
                    line_count: 1,
                    parse_time_ms: 0,
                },
            };

            let purified = generators::generate_purified_bash(&ast);

            // PROPERTY: Must contain the variable name
            prop_assert!(
                purified.contains(&var_name),
                "Purified output must contain variable name '{}', got: {}",
                var_name,
                purified
            );

            // PROPERTY: Must contain the error message
            prop_assert!(
                purified.contains(&error_msg),
                "Purified output must contain error message '{}', got: {}",
                error_msg,
                purified
            );

            // PROPERTY: Must contain :? operator
            prop_assert!(
                purified.contains(":?"),
                "Purified output must contain :? operator, got: {}",
                purified
            );
        }
    }

    // Property: Error-if-unset expansion is deterministic
    proptest! {
        #[test]
        fn prop_error_if_unset_is_deterministic(
            var_name in "[a-zA-Z_][a-zA-Z0-9_]{0,10}",
            error_msg in "[a-zA-Z ]{5,30}"
        ) {
            let ast = BashAst {
                statements: vec![BashStmt::Assignment {
                    name: "result".to_string(),
            index: None,
                    value: BashExpr::ErrorIfUnset {
                        variable: var_name.clone(),
                        message: Box::new(BashExpr::Literal(error_msg.clone())),
                    },
                    exported: false,
                    span: Span::dummy(),
                }],
                metadata: AstMetadata {
                    source_file: None,
                    line_count: 1,
                    parse_time_ms: 0,
                },
            };

            // Generate twice
            let purified1 = generators::generate_purified_bash(&ast);
            let purified2 = generators::generate_purified_bash(&ast);

            // PROPERTY: Determinism - byte-identical output
            prop_assert_eq!(
                purified1,
                purified2,
                "Error-if-unset expansion must be deterministic"
            );
        }
    }

    // Property: Error-if-unset uses :? not :- or :=
    proptest! {
        #[test]
        fn prop_error_if_unset_uses_correct_operator(
            var_name in "[a-zA-Z_][a-zA-Z0-9_]{0,10}",
            error_msg in "[a-zA-Z ]{5,30}"
        ) {
            let ast = BashAst {
                statements: vec![BashStmt::Command {
                    name: "test".to_string(),
                    args: vec![BashExpr::ErrorIfUnset {
                        variable: var_name.clone(),
                        message: Box::new(BashExpr::Literal(error_msg.clone())),
                    }],
                    redirects: vec![],
                    span: Span::dummy(),
                }],
                metadata: AstMetadata {
                    source_file: None,
                    line_count: 1,
                    parse_time_ms: 0,
                },
            };

            let purified = generators::generate_purified_bash(&ast);

            // PROPERTY: Must use :? operator
            prop_assert!(
                purified.contains(":?"),
                "Purified output must contain :? operator, got: {}",
                purified
            );

            // PROPERTY: Must NOT use :- or := operators
            prop_assert!(
                !purified.contains(":-") && !purified.contains(":="),
                "Purified output must not contain :- or := (should be :?), got: {}",
                purified
            );
        }
    }

    // Property: Alternative value expansion preserves variable and alternative
    proptest! {
        #[test]
        fn prop_alternative_value_preserves_components(
            var_name in "[a-zA-Z_][a-zA-Z0-9_]{0,10}",
            alt_value in "[a-zA-Z]{3,15}"
        ) {
            let ast = BashAst {
                statements: vec![BashStmt::Command {
                    name: "echo".to_string(),
                    args: vec![BashExpr::AlternativeValue {
                        variable: var_name.clone(),
                        alternative: Box::new(BashExpr::Literal(alt_value.clone())),
                    }],
                    redirects: vec![],
                    span: Span::dummy(),
                }],
                metadata: AstMetadata {
                    source_file: None,
                    line_count: 1,
                    parse_time_ms: 0,
                },
            };

            let purified = generators::generate_purified_bash(&ast);

            // PROPERTY: Must contain the variable name
            prop_assert!(
                purified.contains(&var_name),
                "Purified output must contain variable name '{}', got: {}",
                var_name,
                purified
            );

            // PROPERTY: Must contain the alternative value
            prop_assert!(
                purified.contains(&alt_value),
                "Purified output must contain alternative value '{}', got: {}",
                alt_value,
                purified
            );

            // PROPERTY: Must contain :+ operator
            prop_assert!(
                purified.contains(":+"),
                "Purified output must contain :+ operator, got: {}",
                purified
            );
        }
    }

    // Property: Alternative value expansion is deterministic
    proptest! {
        #[test]
        fn prop_alternative_value_is_deterministic(
            var_name in "[a-zA-Z_][a-zA-Z0-9_]{0,10}",
            alt_value in "[a-zA-Z]{3,15}"
        ) {
            let ast = BashAst {
                statements: vec![BashStmt::Assignment {
                    name: "result".to_string(),
            index: None,
                    value: BashExpr::AlternativeValue {
                        variable: var_name.clone(),
                        alternative: Box::new(BashExpr::Literal(alt_value.clone())),
                    },
                    exported: false,
                    span: Span::dummy(),
                }],
                metadata: AstMetadata {
                    source_file: None,
                    line_count: 1,
                    parse_time_ms: 0,
                },
            };

            // Generate twice
            let purified1 = generators::generate_purified_bash(&ast);
            let purified2 = generators::generate_purified_bash(&ast);

            // PROPERTY: Determinism - byte-identical output
            prop_assert_eq!(
                purified1,
                purified2,
                "Alternative value expansion must be deterministic"
            );
        }
    }

    // Property: Alternative value uses :+ not :-, :=, or :?
    proptest! {
        #[test]
        fn prop_alternative_value_uses_correct_operator(
            var_name in "[a-zA-Z_][a-zA-Z0-9_]{0,10}",
            alt_value in "[a-zA-Z]{3,15}"
        ) {
            let ast = BashAst {
                statements: vec![BashStmt::Command {
                    name: "test".to_string(),
                    args: vec![BashExpr::AlternativeValue {
                        variable: var_name.clone(),
                        alternative: Box::new(BashExpr::Literal(alt_value.clone())),
                    }],
                    redirects: vec![],
                    span: Span::dummy(),
                }],
                metadata: AstMetadata {
                    source_file: None,
                    line_count: 1,
                    parse_time_ms: 0,
                },
            };

            let purified = generators::generate_purified_bash(&ast);

            // PROPERTY: Must use :+ operator
            prop_assert!(
                purified.contains(":+"),
                "Purified output must contain :+ operator, got: {}",
                purified
            );

            // PROPERTY: Must NOT use :-, :=, or :? operators
            prop_assert!(
                !purified.contains(":-") && !purified.contains(":=") && !purified.contains(":?"),
                "Purified output must not contain :-, :=, or :? (should be :+), got: {}",
                purified
            );
        }
    }
}