pub mod audit;
pub mod clippy;
pub mod complexity;
pub mod coverage;
pub mod deny;
pub mod duplicates;
pub mod fmt;
pub mod hack;
pub mod loc;
pub mod mutants;
pub mod size;
pub mod tests;
use crate::context::Context;
use crate::schema::{
AuditResult, ClippyResult, CollectorStatus, ComplexityResult, CoverageResult, DenyResult,
DuplicatesResult, FmtResult, HackResult, LocResult, MetricsSummary, MutantsResult, ProjectInfo,
SizeResult, TestResult,
};
use std::time::Instant;
pub trait Collector: Send + Sync {
fn name(&self) -> &'static str;
fn is_available(&self) -> bool;
fn collect(&self, ctx: &Context) -> Result<CollectorOutput, CollectorError>;
}
#[derive(Debug, Clone)]
pub struct CollectorOutput {
pub status: CollectorStatus,
pub duration_ms: u64,
pub stdout: String,
pub stderr: String,
}
#[derive(Debug, thiserror::Error)]
pub enum CollectorError {
#[error("collector not available: {0}")]
NotAvailable(String),
#[error("execution failed: {0}")]
ExecutionFailed(String),
#[error("parse error: {0}")]
ParseError(String),
#[error("I/O error: {0}")]
IoError(String),
}
pub struct MockCollector {
pub name_val: &'static str,
pub available: bool,
pub output: CollectorOutput,
}
impl Collector for MockCollector {
fn name(&self) -> &'static str {
self.name_val
}
fn is_available(&self) -> bool {
self.available
}
fn collect(&self, _ctx: &Context) -> Result<CollectorOutput, CollectorError> {
Ok(self.output.clone())
}
}
pub fn run_collectors(
collectors: &[Box<dyn Collector>],
ctx: &Context,
parallel: bool,
) -> MetricsSummary {
let start = Instant::now();
let project_name = ctx
.workspace_root
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".to_string());
let mut results: Vec<(&str, CollectorOutput)> = Vec::new();
if parallel {
use rayon::prelude::*;
results = collectors
.par_iter()
.filter(|col| {
let name_lower = col.name().to_lowercase();
!ctx.disabled_collectors
.iter()
.any(|c| c.to_string() == name_lower)
})
.filter(|col| col.is_available())
.flat_map(|col| match col.collect(ctx) {
Ok(o) => vec![(col.name(), o)],
Err(e) => {
let output = CollectorOutput {
status: CollectorStatus::Error,
duration_ms: 0,
stdout: String::new(),
stderr: format!("{:?}", e),
};
vec![(col.name(), output)]
}
})
.collect();
} else {
for col in collectors {
let name_lower = col.name().to_lowercase();
if ctx
.disabled_collectors
.iter()
.any(|c| c.to_string() == name_lower)
{
continue;
}
if !col.is_available() {
continue;
}
match col.collect(ctx) {
Ok(o) => results.push((col.name(), o)),
Err(e) => {
let output = CollectorOutput {
status: CollectorStatus::Error,
duration_ms: 0,
stdout: String::new(),
stderr: format!("{:?}", e),
};
results.push((col.name(), output));
}
}
}
}
let mut fmt_result = FmtResult {
status: CollectorStatus::Skipped,
details: Default::default(),
};
let mut clippy_result = ClippyResult {
status: CollectorStatus::Skipped,
warning_count: 0,
details: vec![],
};
let mut test_result = TestResult {
status: CollectorStatus::Skipped,
passed: 0,
failed: 0,
ignored: 0,
runner: None,
};
let mut coverage_result = CoverageResult {
status: CollectorStatus::Skipped,
line_percent: 0.0,
};
let mut deny_result = DenyResult {
status: CollectorStatus::Skipped,
banned_count: 0,
license_violations: 0,
};
let mut audit_result = AuditResult {
status: CollectorStatus::Skipped,
vulnerability_count: 0,
critical_count: 0,
};
let mut hack_result = HackResult {
status: CollectorStatus::Skipped,
feature_combinations_tested: 0,
};
let mut mutants_result = MutantsResult {
status: CollectorStatus::Skipped,
mutation_score: 0.0,
caught: 0,
missed: 0,
};
let mut duplicates_result = DuplicatesResult {
status: CollectorStatus::Skipped,
total_lines: 0,
duplicate_lines: 0,
files_with_duplicates: 0,
duplicate_files: vec![],
};
let mut loc_result = LocResult {
status: CollectorStatus::Skipped,
total_lines: 0,
code_lines: 0,
comment_lines: 0,
blank_lines: 0,
long_lines: 0,
max_line_length_found: 0,
max_line_length_allowed: 0,
files: 0,
files_with_long_lines: 0,
long_line_files: vec![],
};
let mut size_result = SizeResult {
status: CollectorStatus::Skipped,
files: 0,
max_lines_per_file: 0,
max_code_lines_per_file: 0,
max_lines_per_function: 0,
max_parameters_per_function: 0,
violations: vec![],
};
for (name, output) in &results {
match *name {
"fmt" => fmt_result.status.clone_from(&output.status),
"clippy" => clippy_result.status.clone_from(&output.status),
"tests" => test_result.status.clone_from(&output.status),
"coverage" => coverage_result.status.clone_from(&output.status),
"deny" => deny_result.status.clone_from(&output.status),
"audit" => audit_result.status.clone_from(&output.status),
"hack" => hack_result.status.clone_from(&output.status),
"mutants" => mutants_result.status.clone_from(&output.status),
"duplicates" => duplicates_result.status.clone_from(&output.status),
"loc" => loc_result.status.clone_from(&output.status),
"size" => size_result.status.clone_from(&output.status),
_ => {}
}
}
MetricsSummary {
schema_version: "1".to_string(),
generated_at: format!("{}", start.elapsed().as_secs()),
rustquty_version: env!("CARGO_PKG_VERSION").to_string(),
project: ProjectInfo {
name: project_name,
rust_edition: "2021".to_string(),
workspace_root: ctx.workspace_root.to_string_lossy().to_string(),
},
collectors: crate::schema::CollectorsSummary {
fmt: fmt_result,
clippy: clippy_result,
tests: test_result,
coverage: coverage_result,
deny: deny_result,
audit: audit_result,
hack: hack_result,
mutants: mutants_result,
duplicates: duplicates_result,
loc: loc_result,
size: size_result,
complexity: ComplexityResult {
status: CollectorStatus::Skipped,
functions: 0,
max_cyclomatic_complexity: 0,
max_nesting_depth: 0,
complex_functions: 0,
violations: vec![],
},
},
}
}
#[cfg(test)]
mod collector_tests {
use super::*;
#[test]
fn test_mock_collector() {
let mock = MockCollector {
name_val: "test",
available: true,
output: CollectorOutput {
status: CollectorStatus::Pass,
duration_ms: 10,
stdout: String::new(),
stderr: String::new(),
},
};
assert_eq!(mock.name(), "test");
assert!(mock.is_available());
}
}