use super::generators::*;
use super::SemanticAnalyzer;
use crate::bash_transpiler::codegen::{BashToRashTranspiler, TranspileOptions};
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig {
cases: 100, // Start with 100 for faster test runs
max_shrink_iters: 1000,
.. ProptestConfig::default()
})]
#[test]
fn prop_valid_scripts_analyze_successfully(script in bash_script()) {
let mut analyzer = SemanticAnalyzer::new();
let result = analyzer.analyze(&script);
prop_assert!(result.is_ok());
}
#[test]
fn prop_transpilation_is_deterministic(script in bash_script()) {
let mut transpiler1 = BashToRashTranspiler::new(TranspileOptions::default());
let mut transpiler2 = BashToRashTranspiler::new(TranspileOptions::default());
let result1 = transpiler1.transpile(&script)?;
let result2 = transpiler2.transpile(&script)?;
prop_assert_eq!(result1, result2);
}
#[test]
fn prop_variable_names_preserved(name in bash_variable_name()) {
use crate::bash_parser::ast::*;
let ast = BashAst {
statements: vec![BashStmt::Assignment {
name: name.clone(),
index: None,
value: BashExpr::Literal("test".to_string()),
exported: false,
span: Span::dummy(),
}],
metadata: AstMetadata {
source_file: None,
line_count: 1,
parse_time_ms: 0,
},
};
let mut transpiler = BashToRashTranspiler::new(TranspileOptions::default());
let rash_code = transpiler.transpile(&ast)?;
let expected = format!("let {}", name);
prop_assert!(rash_code.contains(&expected));
}
#[test]
fn prop_exported_vars_tracked(name in bash_variable_name()) {
use crate::bash_parser::ast::*;
let ast = BashAst {
statements: vec![BashStmt::Assignment {
name: name.clone(),
index: None,
value: BashExpr::Literal("value".to_string()),
exported: true,
span: Span::dummy(),
}],
metadata: AstMetadata {
source_file: None,
line_count: 1,
parse_time_ms: 0,
},
};
let mut analyzer = SemanticAnalyzer::new();
let report = analyzer.analyze(&ast)?;
prop_assert!(report.effects.env_modifications.contains(&name));
}
#[test]
fn prop_purified_bash_uses_posix_shebang(script in bash_script()) {
let purified = generate_purified_bash(&script);
prop_assert!(
purified.starts_with("#!/bin/sh"),
"Purified bash must use POSIX sh shebang, got: {}",
purified.lines().next().unwrap_or("")
);
prop_assert!(
!purified.contains("#!/bin/bash") && !purified.contains("#!/usr/bin/bash"),
"Purified bash must not contain bash-specific shebangs"
);
let purified2 = generate_purified_bash(&script);
prop_assert_eq!(purified, purified2, "Purification must be deterministic");
}
#[test]
fn prop_purified_bash_preserves_commands(name in bash_identifier(), arg in bash_string()) {
use crate::bash_parser::ast::*;
let ast = BashAst {
statements: vec![BashStmt::Command {
name: name.clone(),
args: vec![BashExpr::Literal(arg.clone())],
redirects: vec![],
span: Span::dummy(),
}],
metadata: AstMetadata {
source_file: None,
line_count: 1,
parse_time_ms: 0,
},
};
let purified = generate_purified_bash(&ast);
prop_assert!(purified.contains(&name), "Command name '{}' not preserved in: {}", name, purified);
}
#[test]
fn prop_ISSUE_059_001_command_subst_strings_parse_safely(
cmd in "[a-z]{1,10}",
arg in "[a-zA-Z0-9_]{1,10}"
) {
use crate::bash_parser::BashParser;
let script = format!(r#"OUTPUT="$({} "{}")" "#, cmd, arg);
let result = BashParser::new(&script);
if let Ok(mut parser) = result {
let _ = parser.parse();
}
}
#[test]
fn prop_ISSUE_059_002_andlist_roundtrip(
left_cmd in "[a-z]{1,10}",
right_cmd in "[a-z]{1,10}"
) {
use crate::bash_parser::ast::*;
let ast = BashAst {
statements: vec![BashStmt::AndList {
left: Box::new(BashStmt::Command {
name: left_cmd.clone(),
args: vec![],
redirects: vec![],
span: Span::dummy(),
}),
right: Box::new(BashStmt::Command {
name: right_cmd.clone(),
args: vec![],
redirects: vec![],
span: Span::dummy(),
}),
span: Span::dummy(),
}],
metadata: AstMetadata {
source_file: None,
line_count: 1,
parse_time_ms: 0,
},
};
let purified = generate_purified_bash(&ast);
prop_assert!(
purified.contains("&&"),
"AndList must generate && in output: {}",
purified
);
prop_assert!(
purified.contains(&left_cmd) && purified.contains(&right_cmd),
"Both commands must be preserved: {}",
purified
);
}
#[test]
fn prop_ISSUE_059_003_orlist_roundtrip(
left_cmd in "[a-z]{1,10}",
right_cmd in "[a-z]{1,10}"
) {
use crate::bash_parser::ast::*;
let ast = BashAst {
statements: vec![BashStmt::OrList {
left: Box::new(BashStmt::Command {
name: left_cmd.clone(),
args: vec![],
redirects: vec![],
span: Span::dummy(),
}),
right: Box::new(BashStmt::Command {
name: right_cmd.clone(),
args: vec![],
redirects: vec![],
span: Span::dummy(),
}),
span: Span::dummy(),
}],
metadata: AstMetadata {
source_file: None,
line_count: 1,
parse_time_ms: 0,
},
};
let purified = generate_purified_bash(&ast);
prop_assert!(
purified.contains("||"),
"OrList must generate || in output: {}",
purified
);
prop_assert!(
purified.contains(&left_cmd) && purified.contains(&right_cmd),
"Both commands must be preserved: {}",
purified
);
}
#[test]
fn prop_ISSUE_059_004_logical_operators_parse_safely(
cmd in bash_identifier(),
op in prop::sample::select(vec!["&&", "||"])
) {
use crate::bash_parser::BashParser;
let script = format!("{} {} true", cmd, op);
let result = BashParser::new(&script);
prop_assert!(result.is_ok(), "Lexer should succeed");
let mut parser = result.unwrap();
let parse_result = parser.parse();
prop_assert!(
parse_result.is_ok(),
"Parser should accept '{}': {:?}",
script,
parse_result.err()
);
}
}