impl CoverageImprovementService {
async fn generate_property_tests(&self, targets: &[PathBuf]) -> Result<usize> {
eprintln!("🧪 Generating property-based tests...");
let mut tests_generated = 0;
for target in targets {
if target.extension().and_then(|s| s.to_str()) != Some("rs") {
continue;
}
let full_path = if target.is_absolute() {
target.clone()
} else {
self.config.project_path.join(target)
};
let content = match tokio::fs::read_to_string(&full_path).await {
Ok(c) => c,
Err(e) => {
eprintln!("⚠️ Could not read {}: {}", target.display(), e);
continue;
}
};
let syntax_tree = match syn::parse_file(&content) {
Ok(tree) => tree,
Err(e) => {
eprintln!("⚠️ Could not parse {}: {}", target.display(), e);
continue;
}
};
let functions = self.extract_public_functions(&syntax_tree);
if functions.is_empty() {
eprintln!(" ℹ️ No public functions found in {}", target.display());
continue;
}
let test_content = self.generate_proptest_module(target, &functions)?;
let test_filename = format!(
"proptest_{}.rs",
target.file_stem().unwrap_or_default().to_string_lossy()
);
let test_path = self.config.project_path.join("tests").join(&test_filename);
if let Some(parent) = test_path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
tokio::fs::write(&test_path, test_content).await?;
tests_generated += functions.len();
eprintln!(
" ✅ Generated {} tests for {} -> {}",
functions.len(),
target.display(),
test_filename
);
}
eprintln!("✅ Generated {} property tests total", tests_generated);
Ok(tests_generated)
}
pub(crate) fn extract_public_functions(&self, syntax_tree: &syn::File) -> Vec<syn::ItemFn> {
let mut functions = Vec::new();
for item in &syntax_tree.items {
if let syn::Item::Fn(func) = item {
if matches!(func.vis, syn::Visibility::Public(_)) {
functions.push(func.clone());
}
}
}
functions
}
pub(crate) fn generate_proptest_module(
&self,
target: &PathBuf,
functions: &[syn::ItemFn],
) -> Result<String> {
let mut module = String::from(
r#"//! Auto-generated property tests
//! Generated by pmat coverage improve
use proptest::prelude::*;
"#,
);
let module_name = target
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("target");
module.push_str(&format!("use crate::{}::*;\n\n", module_name));
for func in functions {
let func_name = &func.sig.ident;
let test_name = format!("proptest_{}", func_name);
let mut param_strategies = Vec::new();
let mut param_names = Vec::new();
for input in &func.sig.inputs {
if let syn::FnArg::Typed(pat_type) = input {
if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
let param_name = pat_ident.ident.to_string();
let strategy = self.generate_strategy_for_type(&pat_type.ty);
param_names.push(param_name.clone());
param_strategies.push(format!("{} in {}", param_name, strategy));
}
}
}
if param_strategies.is_empty() {
module.push_str(&format!(
r#"#[test]
fn {}() {{
// Function has no parameters
let _result = {}();
// Add assertions based on expected behavior
}}
"#,
test_name, func_name
));
} else {
let params_str = param_strategies.join(",\n ");
let call_params = param_names.join(", ");
module.push_str(&format!(
r#"proptest! {{
#[test]
fn {}(
{}
) {{
// Property test for {}
let _result = {}({});
// Basic invariant: function should not panic
prop_assert!(true);
}}
}}
"#,
test_name, params_str, func_name, func_name, call_params
));
}
}
Ok(module)
}
pub(crate) fn generate_strategy_for_type(&self, ty: &syn::Type) -> String {
match ty {
syn::Type::Path(type_path) => {
let type_str = type_path
.path
.segments
.last()
.map(|seg| seg.ident.to_string())
.unwrap_or_else(|| "unknown".to_string());
match type_str.as_str() {
"i8" => "any::<i8>()".to_string(),
"i16" => "any::<i16>()".to_string(),
"i32" => "any::<i32>()".to_string(),
"i64" => "any::<i64>()".to_string(),
"u8" => "any::<u8>()".to_string(),
"u16" => "any::<u16>()".to_string(),
"u32" => "any::<u32>()".to_string(),
"u64" => "any::<u64>()".to_string(),
"usize" => "any::<usize>()".to_string(),
"isize" => "any::<isize>()".to_string(),
"f32" => "any::<f32>()".to_string(),
"f64" => "any::<f64>()".to_string(),
"bool" => "any::<bool>()".to_string(),
"char" => "any::<char>()".to_string(),
"String" => r#"".*""#.to_string(),
"str" => r#"".*""#.to_string(),
"Vec" => "prop::collection::vec(any::<i32>(), 0..100)".to_string(),
"Option" => "prop::option::of(any::<i32>())".to_string(),
"Result" => "any::<i32>()".to_string(), "PathBuf" => r#""[a-z0-9/]+""#.to_string(),
"Path" => r#""[a-z0-9/]+""#.to_string(),
_ => "any::<i32>()".to_string(), }
}
syn::Type::Reference(type_ref) => {
self.generate_strategy_for_type(&type_ref.elem)
}
_ => "any::<i32>()".to_string(), }
}
}