use crate::coverage::analysis::run_analysis;
use crate::coverage::tarpaulin::list_tests;
use crate::resolve::resolve_test_patterns;
use crate::types::models::TargetMode;
use crate::utils::cleanup::cleanup_target_dirs;
use crate::utils::io::save_analysis;
use clap::{Parser, Subcommand};
use std::path::{Path, PathBuf};
#[derive(Parser)]
#[command(
name = "isotarp",
about = "Analyze test coverage at the individual test level",
version,
long_about = "Isotarp identifies which tests provide unique code coverage by running each test through cargo-tarpaulin individually and analyzing the results."
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand)]
pub enum Commands {
List {
#[arg(short, long)]
package: String,
},
Analyze {
#[arg(short, long)]
package: String,
#[arg(short, long)]
tests: Option<Vec<String>>,
#[arg(short, long, default_value = "isotarp-output")]
output_dir: PathBuf,
#[arg(short, long, default_value = "isotarp-analysis.json")]
report: PathBuf,
#[arg(short = 'm', long, default_value_t = TargetMode::default(), value_name="MODE")]
target_mode: TargetMode,
},
}
pub fn execute_list_command(package: &str) -> Result<(), Box<dyn std::error::Error>> {
let tests = list_tests(package)?;
println!("Found {} tests in package '{}':", tests.len(), package);
for test in tests {
println!(" {}", test);
}
Ok(())
}
pub fn execute_analyze_command(
package: &str,
tests: Option<Vec<String>>,
output_dir: &Path,
report: &Path,
target_mode: TargetMode,
) -> Result<(), Box<dyn std::error::Error>> {
std::fs::create_dir_all(output_dir)?;
let available_tests = list_tests(package)?;
let test_names = match tests {
Some(specified_tests) => {
let (selected_tests, invalid_patterns) =
resolve_test_patterns(&available_tests, &specified_tests);
if !invalid_patterns.is_empty() {
println!("Warning: The following test patterns did not match any tests:");
for pattern in &invalid_patterns {
println!(" {}", pattern);
}
if selected_tests.is_empty() {
return Err("No matching tests to analyze".into());
}
println!("Continuing with {} matching tests.", selected_tests.len());
}
selected_tests
}
None => {
println!("No specific tests provided, analyzing all tests...");
available_tests
}
};
println!(
"Analyzing {} tests in package '{}' using target mode: {}",
test_names.len(),
package,
target_mode
);
let result = run_analysis(package, &test_names, output_dir, target_mode);
let analysis = match result {
Ok(analysis) => analysis,
Err(e) => {
cleanup_target_dirs(output_dir, &test_names);
return Err(Box::new(e));
}
};
save_analysis(&analysis, report)?;
println!("Analysis complete! Results saved to {}", report.display());
let tests_by_unique: Vec<_> = analysis.tests.iter().collect();
let mut tests_with_unique_coverage = Vec::new();
let mut tests_with_zero_unique_coverage = Vec::new();
let mut tests_with_zero_total_coverage = Vec::new();
for (test_name, stats) in tests_by_unique {
if stats.unique_covered_lines > 0 {
tests_with_unique_coverage.push((test_name, stats));
} else if stats.total_covered_lines > 0 {
tests_with_zero_unique_coverage.push((test_name, stats));
} else {
tests_with_zero_total_coverage.push((test_name, stats));
}
}
tests_with_unique_coverage
.sort_by(|a, b| b.1.unique_covered_lines.cmp(&a.1.unique_covered_lines));
if !tests_with_unique_coverage.is_empty() {
println!("\nTests with unique line coverage:");
for (test_name, stats) in &tests_with_unique_coverage {
let unique_pct =
(stats.unique_covered_lines as f64 / stats.total_covered_lines as f64) * 100.0;
println!(
" {}: {} unique lines ({:.1}% of {} total covered lines)",
test_name, stats.unique_covered_lines, unique_pct, stats.total_covered_lines
);
}
}
if !tests_with_zero_unique_coverage.is_empty() {
println!(
"\nTests with NO unique coverage (but covering {} total lines):",
tests_with_zero_unique_coverage
.iter()
.map(|(_, stats)| stats.total_covered_lines)
.sum::<u32>()
);
for (test_name, stats) in &tests_with_zero_unique_coverage {
println!(
" {}: 0 unique lines (covers {} total lines)",
test_name, stats.total_covered_lines
);
}
}
if !tests_with_zero_total_coverage.is_empty() {
println!("\nTests with NO code coverage:");
for (test_name, _) in &tests_with_zero_total_coverage {
println!(" {}", test_name);
}
}
cleanup_target_dirs(output_dir, &test_names);
Ok(())
}