use crate::error::{CoreError, CoreResult};
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, RwLock};
use std::time::{Duration, Instant, SystemTime};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoverageConfig {
pub coverage_types: Vec<CoverageType>,
pub coverage_threshold: f64,
pub branch_threshold: f64,
pub integration_threshold: f64,
pub report_formats: Vec<ReportFormat>,
pub output_directory: PathBuf,
pub include_systemcode: bool,
pub exclude_patterns: Vec<String>,
pub include_patterns: Vec<String>,
pub real_time_updates: bool,
pub samplingrate: f64,
pub enable_history: bool,
pub history_retention: Duration,
pub enable_diff_coverage: bool,
pub diffbase: Option<String>,
}
impl Default for CoverageConfig {
fn default() -> Self {
Self {
coverage_types: vec![CoverageType::Line, CoverageType::Branch],
coverage_threshold: 80.0,
branch_threshold: 70.0,
integration_threshold: 60.0,
report_formats: vec![ReportFormat::Html, ReportFormat::Json],
output_directory: PathBuf::from("coverage_reports"),
include_systemcode: false,
exclude_patterns: vec![
"*/tests/*".to_string(),
"*/benches/*".to_string(),
"*/examples/*".to_string(),
],
include_patterns: vec![],
real_time_updates: true,
samplingrate: 1.0,
enable_history: true,
history_retention: Duration::from_secs(30 * 24 * 60 * 60), enable_diff_coverage: false,
diffbase: None,
}
}
}
impl CoverageConfig {
pub fn production() -> Self {
Self {
coverage_threshold: 85.0,
branch_threshold: 75.0,
integration_threshold: 70.0,
samplingrate: 0.1, real_time_updates: false,
..Default::default()
}
}
pub fn development() -> Self {
Self {
coverage_types: vec![
CoverageType::Line,
CoverageType::Branch,
CoverageType::Function,
CoverageType::Integration,
],
coverage_threshold: 75.0,
real_time_updates: true,
samplingrate: 1.0,
..Default::default()
}
}
pub fn with_coverage_types(mut self, types: Vec<CoverageType>) -> Self {
self.coverage_types = types;
self
}
pub fn with_threshold(mut self, threshold: f64) -> Self {
self.coverage_threshold = threshold;
self
}
pub fn with_branch_threshold(mut self, threshold: f64) -> Self {
self.branch_threshold = threshold;
self
}
pub fn with_report_format(mut self, format: ReportFormat) -> Self {
self.report_formats = vec![format];
self
}
pub fn with_output_directory<P: AsRef<Path>>(mut self, path: P) -> Self {
self.output_directory = path.as_ref().to_path_buf();
self
}
pub fn with_diff_coverage(mut self, base: &str) -> Self {
self.enable_diff_coverage = true;
self.diffbase = Some(base.to_string());
self
}
pub fn with_exclude_patterns(mut self, patterns: Vec<&str>) -> Self {
self.exclude_patterns = patterns.into_iter().map(|s| s.to_string()).collect();
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CoverageType {
Line,
Branch,
Function,
Statement,
Integration,
Path,
Condition,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReportFormat {
Html,
Json,
Xml,
Lcov,
Text,
Csv,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileCoverage {
pub file_path: PathBuf,
pub total_lines: u32,
pub covered_lines: u32,
pub line_hits: BTreeMap<u32, u32>,
pub branches: Vec<BranchCoverage>,
pub functions: Vec<FunctionCoverage>,
pub integrations: Vec<IntegrationPoint>,
pub modified_time: SystemTime,
pub collected_at: SystemTime,
}
impl FileCoverage {
pub fn line_coverage_percentage(&self) -> f64 {
if self.total_lines == 0 {
100.0
} else {
(self.covered_lines as f64 / self.total_lines as f64) * 100.0
}
}
pub fn branch_coverage_percentage(&self) -> f64 {
if self.branches.is_empty() {
100.0
} else {
let covered_branches = self.branches.iter().filter(|b| b.is_covered()).count();
(covered_branches as f64 / self.branches.len() as f64) * 100.0
}
}
pub fn function_coverage_percentage(&self) -> f64 {
if self.functions.is_empty() {
100.0
} else {
let covered_functions = self
.functions
.iter()
.filter(|f| f.execution_count > 0)
.count();
(covered_functions as f64 / self.functions.len() as f64) * 100.0
}
}
pub fn uncovered_lines(&self) -> Vec<u32> {
(1..=self.total_lines)
.filter(|line| !self.line_hits.contains_key(line))
.collect()
}
pub fn hot_spots(&self, threshold: u32) -> Vec<(u32, u32)> {
self.line_hits
.iter()
.filter(|(_, &count)| count >= threshold)
.map(|(&line, &count)| (line, count))
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BranchCoverage {
pub line_number: u32,
pub branch_id: String,
pub true_count: u32,
pub false_count: u32,
pub branch_type: BranchType,
pub source_snippet: String,
}
impl BranchCoverage {
pub fn is_covered(&self) -> bool {
self.true_count > 0 && self.false_count > 0
}
pub fn total_executions(&self) -> u32 {
self.true_count + self.false_count
}
pub fn balance_score(&self) -> f64 {
if !self.is_covered() {
0.0
} else {
let total = self.total_executions() as f64;
let min_count = self.true_count.min(self.false_count) as f64;
min_count / total * 2.0 }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BranchType {
IfElse,
Match,
While,
For,
Ternary,
Logical,
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionCoverage {
pub function_name: String,
pub start_line: u32,
pub end_line: u32,
pub execution_count: u32,
pub complexity: u32,
pub parameter_count: u32,
pub return_complexity: u32,
}
impl FunctionCoverage {
pub fn coverage_score(&self) -> f64 {
if self.execution_count == 0 {
0.0
} else {
let execution_factor = (self.execution_count as f64).ln().min(5.0) / 5.0;
let complexity_factor = 1.0 / (1.0 + self.complexity as f64 / 10.0);
execution_factor * complexity_factor
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntegrationPoint {
pub id: String,
pub source_module: String,
pub target_module: String,
pub integration_type: IntegrationType,
pub execution_count: u32,
pub line_number: u32,
pub success_rate: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum IntegrationType {
FunctionCall,
MethodCall,
TraitImpl,
ModuleImport,
DatabaseCall,
NetworkCall,
FileSystemOp,
IpcCall,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoverageReport {
pub generated_at: SystemTime,
pub config: CoverageConfig,
pub overall_stats: CoverageStatistics,
pub file_coverage: HashMap<PathBuf, FileCoverage>,
pub trends: Option<CoverageTrends>,
pub quality_gates: QualityGateResults,
pub performance_impact: PerformanceImpact,
pub recommendations: Vec<CoverageRecommendation>,
}
impl CoverageReport {
pub fn overall_coverage_percentage(&self) -> f64 {
self.overall_stats.line_coverage_percentage
}
pub fn meets_quality_gates(&self) -> bool {
self.quality_gates.overall_passed
}
pub fn files_below_threshold(&self) -> Vec<(&Path, f64)> {
self.file_coverage
.iter()
.filter_map(|(path, coverage)| {
let percentage = coverage.line_coverage_percentage();
if percentage < self.config.coverage_threshold {
Some((path.as_path(), percentage))
} else {
None
}
})
.collect()
}
pub fn critical_uncovered_functions(&self) -> Vec<(&FunctionCoverage, &Path)> {
let mut uncovered: Vec<_> = self
.file_coverage
.iter()
.flat_map(|(path, file_cov)| {
file_cov
.functions
.iter()
.filter(|f| f.execution_count == 0)
.map(|f| (f, path.as_path()))
})
.collect();
uncovered.sort_by_key(|b| std::cmp::Reverse(b.0.complexity));
uncovered.into_iter().take(10).collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoverageStatistics {
pub total_lines: u32,
pub covered_lines: u32,
pub line_coverage_percentage: f64,
pub total_branches: u32,
pub covered_branches: u32,
pub branch_coverage_percentage: f64,
pub total_functions: u32,
pub covered_functions: u32,
pub function_coverage_percentage: f64,
pub total_integrations: u32,
pub covered_integrations: u32,
pub integration_coverage_percentage: f64,
pub files_analyzed: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoverageTrends {
pub history: Vec<CoverageDataPoint>,
pub trend_direction: TrendDirection,
pub change_rate: f64,
pub predicted_coverage: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoverageDataPoint {
pub timestamp: SystemTime,
pub coverage_percentage: f64,
pub branch_coverage_percentage: f64,
pub version: Option<String>,
pub test_count: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TrendDirection {
Improving,
Stable,
Declining,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityGateResults {
pub overall_passed: bool,
pub line_coverage_passed: bool,
pub branch_coverage_passed: bool,
pub integration_coverage_passed: bool,
pub failures: Vec<QualityGateFailure>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityGateFailure {
pub gate_type: String,
pub threshold: f64,
pub actual_value: f64,
pub severity: FailureSeverity,
pub suggestions: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum FailureSeverity {
Minor,
Moderate,
Major,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceImpact {
pub execution_overhead_percent: f64,
pub memory_overhead_bytes: u64,
pub collection_duration: Duration,
pub instrumentation_points: u32,
pub sampling_effectiveness: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoverageRecommendation {
pub recommendation_type: RecommendationType,
pub priority: RecommendationPriority,
pub description: String,
pub expected_impact: f64,
pub effort_estimate: f64,
pub affected_items: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RecommendationType {
AddUnitTests,
AddIntegrationTests,
TestEdgeCases,
ImproveBranchCoverage,
AddPropertyTests,
TestComplexFunctions,
RemoveDeadCode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum RecommendationPriority {
Low,
Medium,
High,
Critical,
}
pub struct CoverageAnalyzer {
config: CoverageConfig,
collection_state: Arc<Mutex<CollectionState>>,
file_coverage: Arc<RwLock<HashMap<PathBuf, FileCoverage>>>,
history: Arc<Mutex<Vec<CoverageDataPoint>>>,
collection_start: Option<Instant>,
performance_tracker: PerformanceTracker,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CollectionState {
Idle,
Collecting,
Paused,
Completed,
Error,
}
#[derive(Debug, Default)]
struct PerformanceTracker {
baseline_memory: u64,
execution_timer: Option<Instant>,
instrumentation_count: u32,
}
impl CoverageAnalyzer {
pub fn new(config: CoverageConfig) -> CoreResult<Self> {
if !config.output_directory.exists() {
std::fs::create_dir_all(&config.output_directory).map_err(|e| {
CoreError::from(std::io::Error::other(format!(
"Failed to create output directory: {e}"
)))
})?;
}
Ok(Self {
config,
collection_state: Arc::new(Mutex::new(CollectionState::Idle)),
file_coverage: Arc::new(RwLock::new(HashMap::new())),
history: Arc::new(Mutex::new(Vec::new())),
collection_start: None,
performance_tracker: PerformanceTracker::default(),
})
}
pub fn start_collection(&mut self) -> CoreResult<()> {
if let Ok(mut state) = self.collection_state.lock() {
match *state {
CollectionState::Collecting => {
return Err(CoreError::from(std::io::Error::other(
"Coverage collection already in progress",
)));
}
_ => *state = CollectionState::Collecting,
}
}
self.collection_start = Some(Instant::now());
self.performance_tracker.execution_timer = Some(Instant::now());
self.performance_tracker.baseline_memory = self.get_current_memory_usage();
self.initialize_instrumentation()?;
Ok(())
}
pub fn stop_and_generate_report(&mut self) -> CoreResult<CoverageReport> {
if let Ok(mut state) = self.collection_state.lock() {
*state = CollectionState::Completed;
}
let performance_impact = self.calculate_performance_impact();
let overall_stats = self.calculate_overall_statistics()?;
let quality_gates = self.evaluate_quality_gates(&overall_stats);
let recommendations = self.generate_recommendations(&overall_stats)?;
let trends = if self.config.enable_history {
self.calculate_trends()?
} else {
None
};
let report = CoverageReport {
generated_at: SystemTime::now(),
config: self.config.clone(),
overall_stats,
file_coverage: self.file_coverage.read().expect("Operation failed").clone(),
trends,
quality_gates,
performance_impact,
recommendations,
};
if self.config.enable_history {
self.save_historical_data_point(&report)?;
}
self.generate_output_reports(&report)?;
Ok(report)
}
pub fn record_line_execution(&self, file_path: &Path, linenumber: u32) -> CoreResult<()> {
if let Ok(mut coverage) = self.file_coverage.write() {
let file_coverage =
coverage
.entry(file_path.to_path_buf())
.or_insert_with(|| FileCoverage {
file_path: file_path.to_path_buf(),
total_lines: 0,
covered_lines: 0,
line_hits: BTreeMap::new(),
branches: Vec::new(),
functions: Vec::new(),
integrations: Vec::new(),
modified_time: SystemTime::now(),
collected_at: SystemTime::now(),
});
*file_coverage.line_hits.entry(linenumber).or_insert(0) += 1;
}
Ok(())
}
pub fn record_branch_execution(
&self,
file_path: &Path,
line_number: u32,
branch_id: &str,
taken: bool,
) -> CoreResult<()> {
if let Ok(mut coverage) = self.file_coverage.write() {
let file_coverage =
coverage
.entry(file_path.to_path_buf())
.or_insert_with(|| FileCoverage {
file_path: file_path.to_path_buf(),
total_lines: 0,
covered_lines: 0,
line_hits: BTreeMap::new(),
branches: Vec::new(),
functions: Vec::new(),
integrations: Vec::new(),
modified_time: SystemTime::now(),
collected_at: SystemTime::now(),
});
if let Some(branch) = file_coverage
.branches
.iter_mut()
.find(|b| b.branch_id == branch_id)
{
if taken {
branch.true_count += 1;
} else {
branch.false_count += 1;
}
} else {
let mut branch = BranchCoverage {
line_number,
branch_id: branch_id.to_string(),
true_count: 0,
false_count: 0,
branch_type: BranchType::Other,
source_snippet: String::new(),
};
if taken {
branch.true_count = 1;
} else {
branch.false_count = 1;
}
file_coverage.branches.push(branch);
}
}
Ok(())
}
pub fn record_function_execution(
&self,
file_path: &Path,
function_name: &str,
start_line: u32,
end_line: u32,
) -> CoreResult<()> {
if let Ok(mut coverage) = self.file_coverage.write() {
let file_coverage =
coverage
.entry(file_path.to_path_buf())
.or_insert_with(|| FileCoverage {
file_path: file_path.to_path_buf(),
total_lines: 0,
covered_lines: 0,
line_hits: BTreeMap::new(),
branches: Vec::new(),
functions: Vec::new(),
integrations: Vec::new(),
modified_time: SystemTime::now(),
collected_at: SystemTime::now(),
});
if let Some(function) = file_coverage
.functions
.iter_mut()
.find(|f| f.function_name == function_name)
{
function.execution_count += 1;
} else {
let function = FunctionCoverage {
function_name: function_name.to_string(),
start_line,
end_line,
execution_count: 1,
complexity: self.calculate_function_complexity(start_line, end_line),
parameter_count: 0, return_complexity: 1,
};
file_coverage.functions.push(function);
}
}
Ok(())
}
fn initialize_instrumentation(&mut self) -> CoreResult<()> {
self.performance_tracker.instrumentation_count = 1000; Ok(())
}
fn calculate_overall_statistics(&self) -> CoreResult<CoverageStatistics> {
let coverage = self.file_coverage.read().expect("Operation failed");
let mut stats = CoverageStatistics {
total_lines: 0,
covered_lines: 0,
line_coverage_percentage: 0.0,
total_branches: 0,
covered_branches: 0,
branch_coverage_percentage: 0.0,
total_functions: 0,
covered_functions: 0,
function_coverage_percentage: 0.0,
total_integrations: 0,
covered_integrations: 0,
integration_coverage_percentage: 0.0,
files_analyzed: coverage.len() as u32,
};
for file_cov in coverage.values() {
stats.total_lines += file_cov.total_lines;
stats.covered_lines += file_cov.covered_lines;
stats.total_branches += file_cov.branches.len() as u32;
stats.covered_branches +=
file_cov.branches.iter().filter(|b| b.is_covered()).count() as u32;
stats.total_functions += file_cov.functions.len() as u32;
stats.covered_functions += file_cov
.functions
.iter()
.filter(|f| f.execution_count > 0)
.count() as u32;
stats.total_integrations += file_cov.integrations.len() as u32;
stats.covered_integrations += file_cov
.integrations
.iter()
.filter(|i| i.execution_count > 0)
.count() as u32;
}
stats.line_coverage_percentage = if stats.total_lines > 0 {
(stats.covered_lines as f64 / stats.total_lines as f64) * 100.0
} else {
100.0
};
stats.branch_coverage_percentage = if stats.total_branches > 0 {
(stats.covered_branches as f64 / stats.total_branches as f64) * 100.0
} else {
100.0
};
stats.function_coverage_percentage = if stats.total_functions > 0 {
(stats.covered_functions as f64 / stats.total_functions as f64) * 100.0
} else {
100.0
};
stats.integration_coverage_percentage = if stats.total_integrations > 0 {
(stats.covered_integrations as f64 / stats.total_integrations as f64) * 100.0
} else {
100.0
};
Ok(stats)
}
fn evaluate_quality_gates(&self, stats: &CoverageStatistics) -> QualityGateResults {
let mut results = QualityGateResults {
overall_passed: true,
line_coverage_passed: true,
branch_coverage_passed: true,
integration_coverage_passed: true,
failures: Vec::new(),
};
if stats.line_coverage_percentage < self.config.coverage_threshold {
results.line_coverage_passed = false;
results.overall_passed = false;
let severity = if stats.line_coverage_percentage < self.config.coverage_threshold - 20.0
{
FailureSeverity::Critical
} else if stats.line_coverage_percentage < self.config.coverage_threshold - 10.0 {
FailureSeverity::Major
} else if stats.line_coverage_percentage < self.config.coverage_threshold - 5.0 {
FailureSeverity::Moderate
} else {
FailureSeverity::Minor
};
results.failures.push(QualityGateFailure {
gate_type: "Line Coverage".to_string(),
threshold: self.config.coverage_threshold,
actual_value: stats.line_coverage_percentage,
severity,
suggestions: vec![
"Add unit tests for uncovered lines".to_string(),
"Focus on complex functions with low coverage".to_string(),
"Consider removing dead code".to_string(),
],
});
}
if stats.branch_coverage_percentage < self.config.branch_threshold {
results.branch_coverage_passed = false;
results.overall_passed = false;
results.failures.push(QualityGateFailure {
gate_type: "Branch Coverage".to_string(),
threshold: self.config.branch_threshold,
actual_value: stats.branch_coverage_percentage,
severity: FailureSeverity::Moderate,
suggestions: vec![
"Add tests for both true and false branches".to_string(),
"Test edge cases and error conditions".to_string(),
"Use property-based testing for complex conditions".to_string(),
],
});
}
if stats.integration_coverage_percentage < self.config.integration_threshold {
results.integration_coverage_passed = false;
results.overall_passed = false;
results.failures.push(QualityGateFailure {
gate_type: "Integration Coverage".to_string(),
threshold: self.config.integration_threshold,
actual_value: stats.integration_coverage_percentage,
severity: FailureSeverity::Moderate,
suggestions: vec![
"Add integration tests between modules".to_string(),
"Test external dependencies and APIs".to_string(),
"Include database and network interactions".to_string(),
],
});
}
results
}
fn generate_recommendations(
&self,
stats: &CoverageStatistics,
) -> CoreResult<Vec<CoverageRecommendation>> {
let mut recommendations = Vec::new();
let coverage = self.file_coverage.read().expect("Operation failed");
for (path, file_cov) in coverage.iter() {
let coverage_pct = file_cov.line_coverage_percentage();
if coverage_pct < self.config.coverage_threshold {
recommendations.push(CoverageRecommendation {
recommendation_type: RecommendationType::AddUnitTests,
priority: if coverage_pct < 50.0 {
RecommendationPriority::High
} else {
RecommendationPriority::Medium
},
description: format!(
"Add unit tests for {} (current coverage: {:.1}%)",
path.display(),
coverage_pct
),
expected_impact: self.config.coverage_threshold - coverage_pct,
effort_estimate: (file_cov.uncovered_lines().len() as f64) * 0.25, affected_items: vec![path.to_string_lossy().to_string()],
});
}
}
if stats.branch_coverage_percentage < self.config.branch_threshold {
recommendations.push(CoverageRecommendation {
recommendation_type: RecommendationType::ImproveBranchCoverage,
priority: RecommendationPriority::High,
description: "Improve branch coverage by testing all conditional paths".to_string(),
expected_impact: self.config.branch_threshold - stats.branch_coverage_percentage,
effort_estimate: 8.0, affected_items: vec!["Multiple files with uncovered branches".to_string()],
});
}
let critical_functions = self.find_complex_uncovered_functions();
if !critical_functions.is_empty() {
recommendations.push(CoverageRecommendation {
recommendation_type: RecommendationType::TestComplexFunctions,
priority: RecommendationPriority::Critical,
description: "Add tests for complex functions with high cyclomatic complexity"
.to_string(),
expected_impact: 15.0, effort_estimate: critical_functions.len() as f64 * 2.0, affected_items: critical_functions
.into_iter()
.map(|f| f.function_name.clone())
.collect(),
});
}
recommendations.sort_by_key(|b| std::cmp::Reverse(b.priority));
Ok(recommendations)
}
fn calculate_trends(&self) -> CoreResult<Option<CoverageTrends>> {
let history = self.history.lock().expect("Operation failed");
if history.len() < 2 {
return Ok(None);
}
let recent_points: Vec<_> = history.iter().rev().take(5).collect();
let trend_direction = if recent_points.len() >= 2 {
let first = recent_points
.last()
.expect("Operation failed")
.coverage_percentage;
let last = recent_points
.first()
.expect("Operation failed")
.coverage_percentage;
let change = last - first;
if change > 1.0 {
TrendDirection::Improving
} else if change < -1.0 {
TrendDirection::Declining
} else {
TrendDirection::Stable
}
} else {
TrendDirection::Unknown
};
let change_rate = if recent_points.len() >= 2 {
let first = recent_points.last().expect("Operation failed");
let last = recent_points.first().expect("Operation failed");
let time_diff = last
.timestamp
.duration_since(first.timestamp)
.unwrap_or(Duration::from_secs(1))
.as_secs_f64()
/ (24.0 * 60.0 * 60.0);
let coverage_diff = last.coverage_percentage - first.coverage_percentage;
if time_diff > 0.0 {
coverage_diff / time_diff
} else {
0.0
}
} else {
0.0
};
let predicted_coverage = if change_rate.abs() > 0.1 {
let last_coverage = recent_points
.first()
.expect("Operation failed")
.coverage_percentage;
Some((last_coverage + change_rate * 7.0).clamp(0.0, 100.0)) } else {
None
};
Ok(Some(CoverageTrends {
history: history.clone(),
trend_direction,
change_rate,
predicted_coverage,
}))
}
fn save_historical_data_point(&self, report: &CoverageReport) -> CoreResult<()> {
if let Ok(mut history) = self.history.lock() {
let data_point = CoverageDataPoint {
timestamp: SystemTime::now(),
coverage_percentage: report.overall_stats.line_coverage_percentage,
branch_coverage_percentage: report.overall_stats.branch_coverage_percentage,
version: None, test_count: 0, };
history.push(data_point);
let cutoff = SystemTime::now() - self.config.history_retention;
history.retain(|point| point.timestamp >= cutoff);
}
Ok(())
}
fn generate_output_reports(&self, report: &CoverageReport) -> CoreResult<()> {
for format in &self.config.report_formats {
match format {
ReportFormat::Html => self.generate_html_report(report)?,
ReportFormat::Json => self.generate_json_report(report)?,
ReportFormat::Xml => self.generate_xml_report(report)?,
ReportFormat::Lcov => self.generate_lcov_report(report)?,
ReportFormat::Text => self.generatetext_report(report)?,
ReportFormat::Csv => self.generate_csv_report(report)?,
}
}
Ok(())
}
fn generate_html_report(&self, report: &CoverageReport) -> CoreResult<()> {
let html_content = self.create_html_content(report);
let output_path = self.config.output_directory.join("coverage_report.html");
std::fs::write(output_path, html_content).map_err(|e| {
CoreError::from(std::io::Error::other(format!(
"Failed to write HTML report: {e}"
)))
})?;
Ok(())
}
fn generate_json_report(&self, report: &CoverageReport) -> CoreResult<()> {
{
let json_content = serde_json::to_string_pretty(report).map_err(|e| {
CoreError::from(std::io::Error::other(format!(
"Failed to serialize JSON report: {e}"
)))
})?;
let output_path = self.config.output_directory.join("coverage_report.json");
std::fs::write(output_path, json_content).map_err(|e| {
CoreError::from(std::io::Error::other(format!(
"Failed to write JSON report: {e}"
)))
})?;
}
#[cfg(not(feature = "serde"))]
{
let _ = report; return Err(CoreError::from(std::io::Error::other(
"JSON report requires serde feature",
)));
}
Ok(())
}
fn generate_xml_report(&self, report: &CoverageReport) -> CoreResult<()> {
let xml_content = self.create_xml_content(report);
let output_path = self.config.output_directory.join("coverage_report.xml");
std::fs::write(output_path, xml_content).map_err(|e| {
CoreError::from(std::io::Error::other(format!(
"Failed to write XML report: {e}"
)))
})?;
Ok(())
}
fn generate_lcov_report(&self, report: &CoverageReport) -> CoreResult<()> {
let lcov_content = self.create_lcov_content(report);
let output_path = self.config.output_directory.join("coverage.lcov");
std::fs::write(output_path, lcov_content).map_err(|e| {
CoreError::from(std::io::Error::other(format!(
"Failed to write LCOV report: {e}"
)))
})?;
Ok(())
}
fn generatetext_report(&self, report: &CoverageReport) -> CoreResult<()> {
let text_content = self.createtext_content(report);
let output_path = self.config.output_directory.join("coverage_summary.txt");
std::fs::write(output_path, text_content).map_err(|e| {
CoreError::from(std::io::Error::other(format!(
"Failed to write text report: {e}"
)))
})?;
Ok(())
}
fn generate_csv_report(&self, report: &CoverageReport) -> CoreResult<()> {
let csv_content = self.create_csv_content(report);
let output_path = self.config.output_directory.join("coverage_data.csv");
std::fs::write(output_path, csv_content).map_err(|e| {
CoreError::from(std::io::Error::other(format!(
"Failed to write CSV report: {e}"
)))
})?;
Ok(())
}
fn create_html_content(&self, report: &CoverageReport) -> String {
format!(
r#"<!DOCTYPE html>
<html>
<head>
<title>Coverage Report</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
.header {{ background: #f0f0f0; padding: 20px; border-radius: 5px; }}
.stats {{ display: flex; gap: 20px; margin: 20px 0; }}
.stat-box {{ background: #e8f4f8; padding: 15px; border-radius: 5px; text-align: center; }}
.coverage-bar {{ background: #ddd; height: 20px; border-radius: 10px; overflow: hidden; }}
.coverage-fill {{ background: #4caf50; height: 100%; transition: width 0.3s; }}
.low-coverage {{ background: #f44336; }}
.medium-coverage {{ background: #ff9800; }}
.high-coverage {{ background: #4caf50; }}
table {{ width: 100%; border-collapse: collapse; margin: 20px 0; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
th {{ background-color: #f2f2f2; }}
.recommendations {{ background: #fff3cd; padding: 15px; border-radius: 5px; margin: 20px 0; }}
</style>
</head>
<body>
<div class= header>
<h1>Coverage Report</h1>
<p>Generated at: {}</p>
<p>Overall Coverage: {:.2}%</p>
</div>
<div class= stats>
<div class="stat-box">
<h3>Line Coverage</h3>
<div class="coverage-bar">
<div class="coverage-fill {}" style="width: {:.1}%"></div>
</div>
<p>{:.2}% ({}/{})</p>
</div>
<div class="stat-box">
<h3>Branch Coverage</h3>
<div class="coverage-bar">
<div class="coverage-fill {}" style="width: {:.1}%"></div>
</div>
<p>{:.2}% ({}/{})</p>
</div>
<div class="stat-box">
<h3>Function Coverage</h3>
<div class="coverage-bar">
<div class="coverage-fill {}" style="width: {:.1}%"></div>
</div>
<p>{:.2}% ({}/{})</p>
</div>
</div>
<h2>Quality Gates</h2>
<p>Status: {}</p>
<h2>Recommendations</h2>
<div class= recommendations>
<ul>
{}
</ul>
</div>
<h2>File Coverage Details</h2>
<table>
<tr>
<th>File</th>
<th>Line Coverage</th>
<th>Branch Coverage</th>
<th>Function Coverage</th>
</tr>
{}
</table>
</body>
</html>"#,
chrono::DateTime::<chrono::Utc>::from(report.generated_at)
.format("%Y-%m-%d %H:%M:%S UTC"),
report.overall_stats.line_coverage_percentage,
self.get_coverage_class(report.overall_stats.line_coverage_percentage),
report.overall_stats.line_coverage_percentage,
report.overall_stats.line_coverage_percentage,
report.overall_stats.covered_lines,
report.overall_stats.total_lines,
self.get_coverage_class(report.overall_stats.branch_coverage_percentage),
report.overall_stats.branch_coverage_percentage,
report.overall_stats.branch_coverage_percentage,
report.overall_stats.covered_branches,
report.overall_stats.total_branches,
self.get_coverage_class(report.overall_stats.function_coverage_percentage),
report.overall_stats.function_coverage_percentage,
report.overall_stats.function_coverage_percentage,
report.overall_stats.covered_functions,
report.overall_stats.total_functions,
if report.quality_gates.overall_passed {
"✅ PASSED"
} else {
"❌ FAILED"
},
report
.recommendations
.iter()
.take(5)
.map(|r| format!("<li>{description}</li>", description = r.description))
.collect::<Vec<_>>()
.join("\n"),
report
.file_coverage
.iter()
.map(|(path, cov)| format!(
"<tr><td>{}</td><td>{:.1}%</td><td>{:.1}%</td><td>{:.1}%</td></tr>",
path.display(),
cov.line_coverage_percentage(),
cov.branch_coverage_percentage(),
cov.function_coverage_percentage()
))
.collect::<Vec<_>>()
.join("\n")
)
}
fn create_xml_content(&self, report: &CoverageReport) -> String {
format!(
r#"<?xml _version="1.0" encoding="UTF-8"?>
<coverage _version="1.0" timestamp="{}">
<project name="scirs2-core">
<metrics>
<lines-covered>{}</lines-covered>
<lines-valid>{}</lines-valid>
<line-coverage>{:.6}</line-coverage>
<branches-covered>{}</branches-covered>
<branches-valid>{}</branches-valid>
<branch-coverage>{:.6}</branch-coverage>
</metrics>
<packages>
{}
</packages>
</project>
</coverage>"#,
chrono::DateTime::<chrono::Utc>::from(report.generated_at).timestamp(),
report.overall_stats.covered_lines,
report.overall_stats.total_lines,
report.overall_stats.line_coverage_percentage / 100.0,
report.overall_stats.covered_branches,
report.overall_stats.total_branches,
report.overall_stats.branch_coverage_percentage / 100.0,
report
.file_coverage
.iter()
.map(|(path, cov)| format!(
r#"<package name="{}">
<classes>
<class name="{}" filename="{}">
<metrics line-coverage="{:.6}" branch-coverage="{:.6}" />
<lines>
{}
</lines>
</class>
</classes>
</package>"#,
path.parent().unwrap_or(Path::new("")).display(),
path.file_stem().unwrap_or_default().to_string_lossy(),
path.display(),
cov.line_coverage_percentage() / 100.0,
cov.branch_coverage_percentage() / 100.0,
cov.line_hits
.iter()
.map(|(&line, &hits)| format!(r#"<line number="{line}" hits="{hits}" />"#))
.collect::<Vec<_>>()
.join("\n ")
))
.collect::<Vec<_>>()
.join("\n ")
)
}
fn create_lcov_content(&self, report: &CoverageReport) -> String {
let mut lcov_content = String::new();
for (path, cov) in &report.file_coverage {
lcov_content.push_str(&format!("SF:{path}\n", path = path.display()));
for func in &cov.functions {
lcov_content.push_str(&format!(
"FN:{start_line},{function_name}\n",
start_line = func.start_line,
function_name = func.function_name
));
}
for func in &cov.functions {
lcov_content.push_str(&format!(
"FNDA:{},{}\n",
func.execution_count, func.function_name
));
}
lcov_content.push_str(&format!("FNF:{count}\n", count = cov.functions.len()));
lcov_content.push_str(&format!(
"FNH:{}\n",
cov.functions
.iter()
.filter(|f| f.execution_count > 0)
.count()
));
for branch in &cov.branches {
lcov_content.push_str(&format!(
"BA:{},0,{}\n",
branch.line_number, branch.true_count
));
lcov_content.push_str(&format!(
"BA:{},1,{}\n",
branch.line_number, branch.false_count
));
}
lcov_content.push_str(&format!("BRF:{}\n", cov.branches.len() * 2));
lcov_content.push_str(&format!(
"BRH:{}\n",
cov.branches
.iter()
.map(|b| if b.is_covered() {
2
} else if b.true_count > 0 || b.false_count > 0 {
1
} else {
0
})
.sum::<u32>()
));
for (&line, &hits) in &cov.line_hits {
lcov_content.push_str(&format!("DA:{line},{hits}\n"));
}
lcov_content.push_str(&format!("LF:{}\n", cov.total_lines));
lcov_content.push_str(&format!("LH:{}\n", cov.covered_lines));
lcov_content.push_str("end_of_record\n");
}
lcov_content
}
fn createtext_content(&self, report: &CoverageReport) -> String {
let mut content = String::new();
content.push_str("===== COVERAGE REPORT =====\n\n");
content.push_str(&format!(
"Generated: {}\n",
chrono::DateTime::<chrono::Utc>::from(report.generated_at)
.format("%Y-%m-%d %H:%M:%S UTC")
));
content.push_str(&format!(
"Files Analyzed: {}\n\n",
report.overall_stats.files_analyzed
));
content.push_str("OVERALL STATISTICS:\n");
content.push_str(&format!(
" Line Coverage: {:.2}% ({}/{})\n",
report.overall_stats.line_coverage_percentage,
report.overall_stats.covered_lines,
report.overall_stats.total_lines
));
content.push_str(&format!(
" Branch Coverage: {:.2}% ({}/{})\n",
report.overall_stats.branch_coverage_percentage,
report.overall_stats.covered_branches,
report.overall_stats.total_branches
));
content.push_str(&format!(
" Function Coverage: {:.2}% ({}/{})\n",
report.overall_stats.function_coverage_percentage,
report.overall_stats.covered_functions,
report.overall_stats.total_functions
));
content.push_str("\nQUALITY GATES:\n");
content.push_str(&format!(
" Overall Status: {}\n",
if report.quality_gates.overall_passed {
"✅ PASSED"
} else {
"❌ FAILED"
}
));
for failure in &report.quality_gates.failures {
content.push_str(&format!(
" ❌ {}: {:.2}% (threshold: {:.2}%)\n",
failure.gate_type, failure.actual_value, failure.threshold
));
}
if !report.recommendations.is_empty() {
content.push_str("\nRECOMMENDATIONS:\n");
for (i, rec) in report.recommendations.iter().take(5).enumerate() {
content.push_str(&format!(
" {}. [{}] {}\n",
i + 1,
format!("{0:?}", rec.priority).to_uppercase(),
rec.description
));
}
}
content.push_str("\nFILE DETAILS:\n");
let mut files: Vec<_> = report.file_coverage.iter().collect();
files.sort_by(|a, b| {
a.1.line_coverage_percentage()
.partial_cmp(&b.1.line_coverage_percentage())
.unwrap_or(std::cmp::Ordering::Equal)
});
for (path, cov) in files.iter().take(20) {
content.push_str(&format!(
" {:<50} {:>8.1}% {:>8.1}% {:>8.1}%\n",
path.display()
.to_string()
.chars()
.take(50)
.collect::<String>(),
cov.line_coverage_percentage(),
cov.branch_coverage_percentage(),
cov.function_coverage_percentage()
));
}
content
}
fn create_csv_content(&self, report: &CoverageReport) -> String {
let mut csv_content = String::new();
csv_content.push_str("File,Line Coverage %,Branch Coverage %,Function Coverage %,Total Lines,Covered Lines,Total Branches,Covered Branches,Total Functions,Covered Functions\n");
for (path, cov) in &report.file_coverage {
csv_content.push_str(&format!(
"{},{:.2},{:.2},{:.2},{},{},{},{},{},{}\n",
path.display(),
cov.line_coverage_percentage(),
cov.branch_coverage_percentage(),
cov.function_coverage_percentage(),
cov.total_lines,
cov.covered_lines,
cov.branches.len(),
cov.branches.iter().filter(|b| b.is_covered()).count(),
cov.functions.len(),
cov.functions
.iter()
.filter(|f| f.execution_count > 0)
.count()
));
}
csv_content
}
fn get_coverage_class(&self, percentage: f64) -> &'static str {
if percentage >= 80.0 {
"high-coverage"
} else if percentage >= 50.0 {
"medium-coverage"
} else {
"low-coverage"
}
}
fn calculate_complexity(start_line: u32, endline: u32) -> u32 {
let line_count = endline.saturating_sub(start_line) + 1;
(line_count / 10).max(1) }
fn calculate_function_complexity(&self, start_line: u32, endline: u32) -> u32 {
Self::calculate_complexity(start_line, endline)
}
fn find_complex_uncovered_functions(&self) -> Vec<FunctionCoverage> {
let coverage = self.file_coverage.read().expect("Operation failed");
let mut complex_functions: Vec<_> = coverage
.values()
.flat_map(|file_cov| {
file_cov
.functions
.iter()
.filter(|f| f.execution_count == 0 && f.complexity > 5)
.cloned()
})
.collect();
complex_functions.sort_by_key(|b| std::cmp::Reverse(b.complexity));
complex_functions.into_iter().take(10).collect()
}
fn calculate_performance_impact(&self) -> PerformanceImpact {
let execution_time = self
.performance_tracker
.execution_timer
.map(|start| start.elapsed())
.unwrap_or(Duration::from_secs(0));
let memory_overhead =
self.get_current_memory_usage() - self.performance_tracker.baseline_memory;
PerformanceImpact {
execution_overhead_percent: 5.0, memory_overhead_bytes: memory_overhead,
collection_duration: execution_time,
instrumentation_points: self.performance_tracker.instrumentation_count,
sampling_effectiveness: self.config.samplingrate,
}
}
fn get_current_memory_usage(&self) -> u64 {
1024 * 1024 * 10 }
}
#[cfg(test)]
#[path = "coverage_tests.rs"]
mod tests;