use super::core::TestGenResult;
use crate::bash_parser::ast::*;
use std::collections::HashSet;
pub struct PropertyTestGenerator {
max_test_cases: usize,
}
impl Default for PropertyTestGenerator {
fn default() -> Self {
Self::new()
}
}
impl PropertyTestGenerator {
pub fn new() -> Self {
Self {
max_test_cases: 100,
}
}
pub fn generate_properties(&self, ast: &BashAst) -> TestGenResult<Vec<PropertyTest>> {
let mut tests = Vec::new();
for stmt in &ast.statements {
if let BashStmt::Function { name, body, .. } = stmt {
if let Some(test) = self.generate_determinism_test(name, body)? {
tests.push(test);
}
if let Some(test) = self.generate_idempotency_test(name, body)? {
tests.push(test);
}
tests.extend(self.generate_bounds_tests(name, body)?);
if let Some(test) = self.generate_type_preservation_test(name, body)? {
tests.push(test);
}
}
}
Ok(tests)
}
fn generate_determinism_test(
&self,
name: &str,
body: &[BashStmt],
) -> TestGenResult<Option<PropertyTest>> {
if self.has_nondeterministic_operations(body) {
return Ok(None);
}
let generators = self.infer_generators_from_function(name, body)?;
Ok(Some(PropertyTest {
name: format!("prop_{}_determinism", name),
property: Property::Determinism,
generators,
test_cases: self.max_test_cases,
}))
}
fn generate_idempotency_test(
&self,
name: &str,
body: &[BashStmt],
) -> TestGenResult<Option<PropertyTest>> {
if !self.is_potentially_idempotent(body) {
return Ok(None);
}
let generators = self.infer_generators_from_function(name, body)?;
Ok(Some(PropertyTest {
name: format!("prop_{}_idempotency", name),
property: Property::Idempotency,
generators,
test_cases: self.max_test_cases,
}))
}
fn generate_bounds_tests(
&self,
name: &str,
body: &[BashStmt],
) -> TestGenResult<Vec<PropertyTest>> {
let mut tests = Vec::new();
for stmt in body {
if let Some(bounds) = self.extract_bounds(stmt) {
let generators = vec![Generator::Integer {
min: bounds.min - 10,
max: bounds.max + 10,
}];
tests.push(PropertyTest {
name: format!("prop_{}_bounds_{}_{}", name, bounds.min, bounds.max),
property: Property::Bounds {
min: bounds.min,
max: bounds.max,
},
generators,
test_cases: self.max_test_cases,
});
}
}
Ok(tests)
}
fn generate_type_preservation_test(
&self,
name: &str,
body: &[BashStmt],
) -> TestGenResult<Option<PropertyTest>> {
let generators = self.infer_generators_from_function(name, body)?;
Ok(Some(PropertyTest {
name: format!("prop_{}_type_preservation", name),
property: Property::TypePreservation,
generators,
test_cases: self.max_test_cases,
}))
}
fn is_nondeterministic_command(name: &str) -> bool {
matches!(name, "random" | "date" | "time" | "rand" | "uuid")
}
fn if_stmt_has_nondeterminism(
&self,
then_block: &[BashStmt],
elif_blocks: &[(BashExpr, Vec<BashStmt>)],
else_block: &Option<Vec<BashStmt>>,
) -> bool {
if self.has_nondeterministic_operations(then_block) {
return true;
}
if elif_blocks
.iter()
.any(|(_, block)| self.has_nondeterministic_operations(block))
{
return true;
}
else_block
.as_deref()
.is_some_and(|block| self.has_nondeterministic_operations(block))
}
fn has_nondeterministic_operations(&self, body: &[BashStmt]) -> bool {
body.iter().any(|stmt| match stmt {
BashStmt::Command { name, .. } => Self::is_nondeterministic_command(name.as_str()),
BashStmt::If {
then_block,
elif_blocks,
else_block,
..
} => self.if_stmt_has_nondeterminism(then_block, elif_blocks, else_block),
BashStmt::While { body, .. } | BashStmt::For { body, .. } => {
self.has_nondeterministic_operations(body)
}
_ => false,
})
}
fn is_potentially_idempotent(&self, body: &[BashStmt]) -> bool {
for stmt in body {
if let BashStmt::Command { name, .. } = stmt {
if matches!(
name.as_str(),
"sort" | "uniq" | "tr" | "sed" | "awk" | "normalize" | "trim"
) {
return true;
}
}
}
false
}
fn default_string_generator() -> Generator {
Generator::String {
pattern: "[a-zA-Z0-9]{1,20}".to_string(),
}
}
fn default_integer_generator() -> Generator {
Generator::Integer {
min: -1000,
max: 1000,
}
}
fn add_generator_for_literal(
lit: &str,
generators: &mut Vec<Generator>,
seen_types: &mut HashSet<&'static str>,
) {
if lit.parse::<i64>().is_ok() {
if !seen_types.contains("integer") {
generators.push(Self::default_integer_generator());
seen_types.insert("integer");
}
} else if !seen_types.contains("string") {
generators.push(Self::default_string_generator());
seen_types.insert("string");
}
}
fn add_generator_for_arithmetic(
generators: &mut Vec<Generator>,
seen_types: &mut HashSet<&'static str>,
) {
if !seen_types.contains("integer") {
generators.push(Self::default_integer_generator());
seen_types.insert("integer");
}
}
fn infer_generators_from_function(
&self,
_name: &str,
body: &[BashStmt],
) -> TestGenResult<Vec<Generator>> {
let mut generators = Vec::new();
let mut seen_types = HashSet::new();
for stmt in body {
if let BashStmt::Assignment { value, .. } = stmt {
match value {
BashExpr::Literal(lit) => {
Self::add_generator_for_literal(lit, &mut generators, &mut seen_types);
}
BashExpr::Arithmetic(_) => {
Self::add_generator_for_arithmetic(&mut generators, &mut seen_types);
}
_ => {}
}
}
}
if generators.is_empty() {
generators.push(Self::default_string_generator());
}
Ok(generators)
}
fn extract_bounds(&self, stmt: &BashStmt) -> Option<BoundsInfo> {
if let BashStmt::If {
condition: BashExpr::Test { .. },
..
} = stmt
{
return Some(BoundsInfo { min: 0, max: 100 });
}
None
}
}
struct BoundsInfo {
min: i64,
max: i64,
}
#[derive(Debug, Clone)]
pub struct PropertyTest {
pub name: String,
pub property: Property,
pub generators: Vec<Generator>,
pub test_cases: usize,
}
impl PropertyTest {
pub fn to_rust_code(&self) -> String {
let mut code = String::new();
code.push_str("proptest! {\n");
code.push_str(" #[test]\n");
code.push_str(&format!(" fn {}(\n", self.name));
for (i, gen) in self.generators.iter().enumerate() {
let param_name = format!("arg{}", i);
let generator_code = gen.to_proptest_strategy();
code.push_str(&format!(" {} in {},\n", param_name, generator_code));
}
code.push_str(" ) {\n");
match &self.property {
Property::Determinism => {
code.push_str(" // Test determinism: same input → same output\n");
let args = (0..self.generators.len())
.map(|i| format!("arg{}", i))
.collect::<Vec<_>>()
.join(", ");
code.push_str(&format!(
" let result1 = function_under_test({});\n",
args
));
code.push_str(&format!(
" let result2 = function_under_test({});\n",
args
));
code.push_str(" prop_assert_eq!(result1, result2);\n");
}
Property::Idempotency => {
code.push_str(" // Test idempotency: f(f(x)) == f(x)\n");
let args = (0..self.generators.len())
.map(|i| format!("arg{}", i))
.collect::<Vec<_>>()
.join(", ");
code.push_str(&format!(
" let result1 = function_under_test({});\n",
args
));
code.push_str(" let result2 = function_under_test(&result1);\n");
code.push_str(" prop_assert_eq!(result1, result2);\n");
}
Property::Commutativity => {
code.push_str(" // Test commutativity: f(a, b) == f(b, a)\n");
if self.generators.len() >= 2 {
code.push_str(" let result1 = function_under_test(arg0, arg1);\n");
code.push_str(" let result2 = function_under_test(arg1, arg0);\n");
code.push_str(" prop_assert_eq!(result1, result2);\n");
}
}
Property::Bounds { min, max } => {
code.push_str(&format!(
" // Test bounds: result in range [{}, {}]\n",
min, max
));
let args = (0..self.generators.len())
.map(|i| format!("arg{}", i))
.collect::<Vec<_>>()
.join(", ");
code.push_str(&format!(
" let result = function_under_test({});\n",
args
));
code.push_str(&format!(" prop_assert!(result >= {});\n", min));
code.push_str(&format!(" prop_assert!(result <= {});\n", max));
}
Property::TypePreservation => {
code.push_str(" // Test type preservation\n");
let args = (0..self.generators.len())
.map(|i| format!("arg{}", i))
.collect::<Vec<_>>()
.join(", ");
code.push_str(&format!(
" let result = function_under_test({});\n",
args
));
code.push_str(" // Verify result has expected type\n");
code.push_str(" prop_assert!(std::mem::size_of_val(&result) > 0);\n");
}
Property::NoSideEffects => {
code.push_str(
" // Test no side effects: function doesn't modify external state\n",
);
let args = (0..self.generators.len())
.map(|i| format!("arg{}", i))
.collect::<Vec<_>>()
.join(", ");
code.push_str(&format!(
" let _result = function_under_test({});\n",
args
));
code.push_str(" // Verify no side effects occurred\n");
}
}
code.push_str(" }\n");
code.push_str("}\n");
code
}
}
#[derive(Debug, Clone)]
pub enum Property {
Determinism,
Idempotency,
Commutativity,
Bounds { min: i64, max: i64 },
TypePreservation,
NoSideEffects,
}
#[derive(Debug, Clone)]
pub enum Generator {
Integer { min: i64, max: i64 },
String { pattern: String },
Path { valid: bool },
}
impl Generator {
pub fn to_proptest_strategy(&self) -> String {
match self {
Generator::Integer { min, max } => {
format!("{}..={}", min, max)
}
Generator::String { pattern } => {
if pattern == "[a-zA-Z0-9]{1,20}" {
"\"[a-zA-Z0-9]{1,20}\"".to_string()
} else {
format!("\"{}\"", pattern)
}
}
Generator::Path { valid } => {
if *valid {
"\"/[a-z]{1,10}/[a-z]{1,10}\"".to_string()
} else {
"\"/[^/]{0,5}\"".to_string()
}
}
}
}
}