pub(crate) struct GeneratedTestTargets {
pub(crate) crate_ident: String,
pub(crate) has_proptest: bool,
}
pub(crate) fn read_generated_test_targets(manifest: &str) -> Option<GeneratedTestTargets> {
let manifest: toml::Value = toml::from_str(manifest).ok()?;
let package_name = manifest.get("package")?.get("name")?.as_str()?;
let crate_ident = manifest
.get("lib")
.and_then(|lib| lib.get("name"))
.and_then(toml::Value::as_str)
.unwrap_or(package_name)
.replace('-', "_");
let has_proptest = manifest
.get("dev-dependencies")
.and_then(|deps| deps.get("proptest"))
.is_some();
Some(GeneratedTestTargets {
crate_ident,
has_proptest,
})
}
impl CoverageImprovementService {
async fn generate_property_tests(&self, targets: &[PathBuf]) -> Result<usize> {
crate::status_eprintln!("🧪 Generating property-based tests...");
let manifest_path = self.config.project_path.join("Cargo.toml");
let manifest = tokio::fs::read_to_string(&manifest_path).await.ok();
let Some(test_targets) = manifest.as_deref().and_then(read_generated_test_targets) else {
eprintln!(
"⚠️ Not generating property tests: no readable [package] in {}",
manifest_path.display()
);
return Ok(0);
};
if !test_targets.has_proptest {
eprintln!(
"⚠️ Not generating property tests: {} has no `proptest` dev-dependency.",
manifest_path.display()
);
eprintln!(
" Add `proptest = \"1\"` under [dev-dependencies] and re-run — emitting the\n\
\x20 tests now would leave the project unable to compile its test targets."
);
return Ok(0);
}
let mut tests_generated = 0;
let mut written: Vec<PathBuf> = Vec::new();
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(&test_targets.crate_ident, 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?;
written.push(test_path);
tests_generated += functions.len();
crate::status_eprintln!(
" ✅ Generated {} tests for {} -> {}",
functions.len(),
target.display(),
test_filename
);
}
if !written.is_empty() && !self.generated_tests_compile().await {
for path in &written {
let _ = tokio::fs::remove_file(path).await;
}
eprintln!(
"↩️ Reverted {} generated test file(s): they do not compile.",
written.len()
);
return Ok(0);
}
crate::status_eprintln!("✅ Generated {} property tests total", tests_generated);
Ok(tests_generated)
}
async fn generated_tests_compile(&self) -> bool {
let output = Command::new("cargo")
.args(["test", "--no-run", "--tests"])
.current_dir(&self.config.project_path)
.output()
.await;
match output {
Ok(out) if out.status.success() => true,
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
eprintln!("⚠️ Generated tests do not compile:");
for line in stderr.lines().take(20) {
eprintln!(" {line}");
}
false
}
Err(e) => {
eprintln!("⚠️ Could not verify generated tests compile: {e}");
false
}
}
}
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,
crate_ident: &str,
_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::*;
"#,
);
module.push_str(&format!("use {}::*;\n\n", crate_ident));
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(), }
}
}