use cargo_metadata::{Metadata, MetadataCommand};
use serde::Deserialize;
use std::fs;
use crate::cli::commands::deps;
use crate::core::analyzers::{
audit::run_vulnerability_audit, clippy::run_clippy, unsafe_code::run_unsafe, unused::run_unused,
};
use crate::core::error::{ErrorCode, Result, UpkeepError};
use crate::core::output::{
print_json, AuditOutput, ClippyOutput, DepsOutput, QualityOutput, SkipReason, UnsafeOutput,
UnusedOutput,
};
use crate::core::scorers::quality::{
score_quality, Availability, ClippySummary, DependencyFreshness, MsrvStatus, QualityInputs,
SecuritySummary, UnsafeSummary, UnusedSummary,
};
pub async fn run(json: bool, require_complete: bool) -> Result<()> {
let deps_future = deps::analyze(false);
let audit_future = run_blocking("audit", run_vulnerability_audit);
let clippy_future = run_clippy();
let msrv_future = check_msrv();
let unused_future = run_unused();
let unsafe_future = run_unsafe();
let (deps_result, audit_result, clippy_result, msrv_result, unused_result, unsafe_result) = tokio::join!(
deps_future,
audit_future,
clippy_future,
msrv_future,
unused_future,
unsafe_future
);
let output = build_quality_output(
deps_result,
audit_result,
clippy_result,
msrv_result,
unused_result,
unsafe_result,
);
emit_output(json, &output)?;
enforce_exit_policy(&output, require_complete)
}
fn enforce_exit_policy(output: &QualityOutput, require_complete: bool) -> Result<()> {
if output.score.is_none() {
return Err(UpkeepError::message(
ErrorCode::IncompleteAnalysis,
format!(
"quality analysis measured nothing: all {} metrics were unavailable, so no score \
was produced; the report says why each one could not run",
output.breakdown.len()
),
));
}
if require_complete && !output.complete {
return Err(UpkeepError::message(
ErrorCode::IncompleteAnalysis,
format!(
"quality analysis incomplete: {} of {} metrics could not be measured ({:.0}% of \
weight measured); --require-complete treats that as a failure",
output.unavailable.len(),
output.breakdown.len(),
output.measured_weight * 100.0
),
));
}
Ok(())
}
fn emit_output(json: bool, output: &QualityOutput) -> Result<()> {
if json {
print_json(output)
} else {
println!("{output}");
Ok(())
}
}
fn availability<T, U>(result: Result<T>, map: impl FnOnce(T) -> U) -> Availability<U> {
match result {
Ok(value) => Availability::Measured(map(value)),
Err(err) => unavailable_from(&err),
}
}
fn unavailable_from<T>(err: &UpkeepError) -> Availability<T> {
if err.code() == ErrorCode::MissingTool {
Availability::not_installed(err.to_string())
} else {
Availability::failed(err.to_string())
}
}
fn dependency_freshness(result: Result<DepsOutput>) -> Availability<DependencyFreshness> {
let output = match result {
Ok(output) => output,
Err(err) => return unavailable_from(&err),
};
let registry_unavailable = output
.skipped_packages
.iter()
.any(|skipped| skipped.reason == SkipReason::RegistryUnavailable);
let unsupported_registry = output
.skipped_packages
.iter()
.any(|skipped| skipped.reason == SkipReason::UnsupportedRegistry);
let registry_metadata_missing = output
.skipped_packages
.iter()
.any(|skipped| skipped.reason == SkipReason::RegistryMetadataMissing);
let unanswered_resolve = output.skipped_packages.iter().any(|skipped| {
matches!(
skipped.reason,
SkipReason::MissingResolve | SkipReason::AmbiguousResolve
)
});
if output.checked == 0
&& (registry_unavailable
|| unsupported_registry
|| registry_metadata_missing
|| unanswered_resolve)
{
let detail = if registry_unavailable
&& !unsupported_registry
&& !registry_metadata_missing
&& !unanswered_resolve
{
"the crates.io registry was unavailable"
} else if unsupported_registry
&& !registry_unavailable
&& !registry_metadata_missing
&& !unanswered_resolve
{
"no supported registry comparison was available"
} else {
"no supported registry comparison could be completed"
};
return Availability::failed(format!(
"{detail}; none of the {} declared dependencies could be checked for newer versions",
output.total
));
}
Availability::Measured(DependencyFreshness {
total: output.checked,
outdated: output.outdated,
})
}
fn build_quality_output(
deps_result: Result<DepsOutput>,
audit_result: Result<AuditOutput>,
clippy_result: Result<ClippyOutput>,
msrv_result: Result<MsrvStatus>,
unused_result: Result<UnusedOutput>,
unsafe_result: Result<UnsafeOutput>,
) -> QualityOutput {
score_quality(QualityInputs {
dependency_freshness: dependency_freshness(deps_result),
security: availability(audit_result, |output| SecuritySummary {
critical: output.summary.critical,
high: output.summary.high,
moderate: output.summary.moderate,
low: output.summary.low,
}),
unused: availability(unused_result, |output| UnusedSummary {
unused_count: output.unused.len(),
}),
unsafe_code: availability(unsafe_result, |output| UnsafeSummary {
total_unsafe: output.summary.total_unsafe,
}),
clippy: availability(clippy_result, |output| ClippySummary {
warnings: output.warnings,
errors: output.errors,
}),
msrv: availability(msrv_result, |status| status),
})
}
async fn check_msrv() -> Result<MsrvStatus> {
run_blocking("MSRV check", || {
let metadata = MetadataCommand::new().exec().map_err(|err| {
UpkeepError::context(ErrorCode::Metadata, "failed to load cargo metadata", err)
})?;
msrv_status(&metadata)
})
.await
}
#[derive(Debug, Deserialize)]
struct WorkspaceManifest {
workspace: Option<WorkspaceTable>,
}
#[derive(Debug, Deserialize)]
struct WorkspaceTable {
package: Option<WorkspacePackage>,
}
#[derive(Debug, Deserialize)]
struct WorkspacePackage {
#[serde(rename = "rust-version")]
rust_version: Option<String>,
}
fn msrv_status(metadata: &Metadata) -> Result<MsrvStatus> {
if let Some(root) = metadata.root_package() {
return Ok(if root.rust_version.is_some() {
MsrvStatus::Valid
} else {
MsrvStatus::Missing
});
}
let workspace_packages = metadata.workspace_packages();
if !workspace_packages.is_empty()
&& workspace_packages
.iter()
.all(|package| package.rust_version.is_some())
{
return Ok(MsrvStatus::Valid);
}
let manifest_path = metadata.workspace_root.join("Cargo.toml");
let contents = fs::read_to_string(&manifest_path).map_err(|err| {
UpkeepError::context(
ErrorCode::Metadata,
format!("failed to read workspace manifest {manifest_path}"),
err,
)
})?;
let manifest: WorkspaceManifest = toml::from_str(&contents).map_err(|err| {
UpkeepError::context(
ErrorCode::Metadata,
format!("failed to parse workspace manifest {manifest_path}"),
err,
)
})?;
Ok(
if manifest
.workspace
.and_then(|workspace| workspace.package)
.and_then(|package| package.rust_version)
.is_some()
{
MsrvStatus::Valid
} else {
MsrvStatus::Missing
},
)
}
async fn run_blocking<T, F>(label: &str, func: F) -> Result<T>
where
T: Send + 'static,
F: FnOnce() -> Result<T> + Send + 'static,
{
tokio::task::spawn_blocking(func).await.map_err(|err| {
UpkeepError::message(ErrorCode::TaskFailed, format!("{label} task failed: {err}"))
})?
}
#[cfg(test)]
mod tests {
use super::{
build_quality_output, check_msrv, enforce_exit_policy, msrv_status, run_blocking,
MsrvStatus,
};
use crate::core::analyzers::clippy::parse_diagnostics;
use crate::core::error::{ErrorCode, UpkeepError};
use crate::core::output::{
AuditOutput, AuditSummary, AuditWarning, AuditWarningKind, ClippyOutput, Confidence,
DependencyType, DepsOutput, Grade, MetricScore, OutdatedPackage, QualityOutput, Severity,
SkipReason, SkippedDependency, UnavailableMetric, UnavailableReason, UnsafeOutput,
UnsafePackage, UnsafeSummary as UnsafeOutputSummary, UnusedDep, UnusedOutput, UpdateType,
Vulnerability,
};
use crate::core::scorers::quality::{
score_quality, Availability, QualityInputs, SecuritySummary, UnsafeSummary, UnusedSummary,
METRIC_CLIPPY, METRIC_DEPENDENCY_FRESHNESS, METRIC_MSRV, METRIC_SECURITY,
METRIC_UNSAFE_CODE, METRIC_UNUSED_DEPS, WEIGHT_UNUSED_DEPS,
};
use cargo_metadata::{Metadata, MetadataCommand};
use serde_json::Value;
use std::fs;
use tempfile::TempDir;
const FLOAT_TOLERANCE: f32 = 0.01;
fn err() -> UpkeepError {
UpkeepError::message(ErrorCode::TaskFailed, "boom")
}
fn deps_output(
declared: usize,
checked: usize,
outdated: usize,
registry_skipped: usize,
) -> DepsOutput {
assert!(
outdated <= checked,
"outdated ({outdated}) cannot exceed checked ({checked}): a dependency is only \
reported outdated after its latest version was compared"
);
assert!(
checked + registry_skipped <= declared,
"checked ({checked}) + registry_skipped ({registry_skipped}) cannot exceed declared \
edges ({declared}): grouping and skip deduplication only ever shrink the count"
);
let packages: Vec<OutdatedPackage> = (0..outdated)
.map(|index| OutdatedPackage {
name: format!("outdated-{index}"),
alias: None,
current: "1.0.0".to_string(),
latest: "2.0.0".to_string(),
required: "1.0".to_string(),
update_type: UpdateType::Major,
dependency_type: DependencyType::Normal,
members: vec!["demo".to_string()],
})
.collect();
let skipped_packages: Vec<SkippedDependency> = (0..registry_skipped)
.map(|index| SkippedDependency {
name: format!("unchecked-{index}"),
alias: None,
required: "1.0".to_string(),
reason: SkipReason::RegistryUnavailable,
dependency_type: DependencyType::Normal,
source: Some("registry+https://github.com/rust-lang/crates.io-index".to_string()),
target: None,
})
.collect();
DepsOutput {
total: declared,
checked,
outdated: packages.len(),
major: packages.len(),
minor: 0,
patch: 0,
packages,
skipped: skipped_packages.len(),
skipped_packages,
warnings: Vec::new(),
security: None,
workspace: false,
members: vec!["demo".to_string()],
skipped_members: Vec::new(),
}
}
fn clean_audit() -> AuditOutput {
AuditOutput {
vulnerabilities: Vec::new(),
warnings: Vec::new(),
summary: AuditSummary {
critical: 0,
high: 0,
moderate: 0,
low: 0,
total: 0,
},
}
}
fn warning_only_audit() -> AuditOutput {
AuditOutput {
vulnerabilities: Vec::new(),
warnings: vec![AuditWarning {
kind: AuditWarningKind::Unmaintained,
package: "legacy".to_string(),
package_version: "1.0.0".to_string(),
advisory_id: Some("RUSTSEC-2099-0002".to_string()),
title: Some("Legacy is unmaintained".to_string()),
path: vec!["demo".to_string(), "legacy".to_string()],
fix_available: Some(false),
}],
summary: AuditSummary {
critical: 0,
high: 0,
moderate: 0,
low: 0,
total: 0,
},
}
}
fn clean_clippy() -> ClippyOutput {
ClippyOutput {
warnings: 0,
errors: 0,
warnings_by_lint: Default::default(),
details: Vec::new(),
score: 100.0,
}
}
fn clean_unused() -> UnusedOutput {
UnusedOutput {
unused: Vec::new(),
possibly_unused: Vec::new(),
}
}
fn clean_unsafe() -> UnsafeOutput {
UnsafeOutput {
packages: Vec::new(),
summary: UnsafeOutputSummary {
packages: 0,
unsafe_functions: 0,
unsafe_impls: 0,
unsafe_traits: 0,
unsafe_blocks: 0,
unsafe_expressions: 0,
total_unsafe: 0,
},
}
}
const SECURITY_COUNTS: [usize; 4] = [1, 2, 3, 4];
fn vulnerability(index: usize, severity: Severity) -> Vulnerability {
Vulnerability {
id: format!("RUSTSEC-2024-{index:04}"),
package: format!("vulnerable-{index}"),
package_version: "1.0.0".to_string(),
severity,
title: "example advisory".to_string(),
path: vec!["demo".to_string(), format!("vulnerable-{index}")],
fix_available: true,
}
}
fn asymmetric_audit() -> AuditOutput {
let [critical, high, moderate, low] = SECURITY_COUNTS;
let severities: [(usize, fn() -> Severity); 4] = [
(critical, || Severity::Critical),
(high, || Severity::High),
(moderate, || Severity::Moderate),
(low, || Severity::Low),
];
let mut vulnerabilities = Vec::new();
for (count, severity) in severities {
for _ in 0..count {
let index = vulnerabilities.len();
vulnerabilities.push(vulnerability(index, severity()));
}
}
AuditOutput {
summary: AuditSummary {
critical,
high,
moderate,
low,
total: vulnerabilities.len(),
},
vulnerabilities,
warnings: Vec::new(),
}
}
const UNUSED_CONFIRMED: usize = 2;
const UNUSED_POSSIBLE: usize = 5;
fn asymmetric_unused() -> UnusedOutput {
UnusedOutput {
unused: (0..UNUSED_CONFIRMED)
.map(|index| UnusedDep {
name: format!("unused-{index}"),
dependency_type: DependencyType::Normal,
confidence: Confidence::High,
})
.collect(),
possibly_unused: (0..UNUSED_POSSIBLE)
.map(|index| format!("possibly-unused-{index}"))
.collect(),
}
}
fn unsafe_package(name: &str, counts: [usize; 5]) -> UnsafePackage {
let [unsafe_functions, unsafe_impls, unsafe_traits, unsafe_blocks, unsafe_expressions] =
counts;
UnsafePackage {
name: name.to_string(),
version: "1.0.0".to_string(),
package_id: None,
unsafe_functions,
unsafe_impls,
unsafe_traits,
unsafe_blocks,
unsafe_expressions,
total_unsafe: counts.iter().sum(),
}
}
fn asymmetric_unsafe() -> UnsafeOutput {
let packages = vec![
unsafe_package("alpha", [2, 1, 3, 2, 4]),
unsafe_package("beta", [1, 0, 1, 3, 2]),
];
let mut summary = UnsafeOutputSummary {
packages: packages.len(),
unsafe_functions: 0,
unsafe_impls: 0,
unsafe_traits: 0,
unsafe_blocks: 0,
unsafe_expressions: 0,
total_unsafe: 0,
};
for package in &packages {
summary.unsafe_functions += package.unsafe_functions;
summary.unsafe_impls += package.unsafe_impls;
summary.unsafe_traits += package.unsafe_traits;
summary.unsafe_blocks += package.unsafe_blocks;
summary.unsafe_expressions += package.unsafe_expressions;
summary.total_unsafe += package.total_unsafe;
}
UnsafeOutput { packages, summary }
}
fn missing_tool(tool: &str) -> UpkeepError {
UpkeepError::message(
ErrorCode::MissingTool,
format!("cargo-{tool} is not installed; install with `cargo install cargo-{tool}`"),
)
}
fn assert_close(actual: f32, expected: f32) {
assert!(
(actual - expected).abs() < FLOAT_TOLERANCE,
"expected {expected}, got {actual}"
);
}
fn score_of(output: &QualityOutput, name: &str) -> Option<f32> {
output
.breakdown
.iter()
.find(|metric| metric.name == name)
.unwrap_or_else(|| panic!("missing breakdown entry for {name}"))
.score
}
fn nothing_measured() -> QualityInputs {
QualityInputs {
dependency_freshness: Availability::failed("not under test"),
security: Availability::failed("not under test"),
unused: Availability::failed("not under test"),
unsafe_code: Availability::failed("not under test"),
clippy: Availability::failed("not under test"),
msrv: Availability::failed("not under test"),
}
}
fn security_reference_score(counts: [usize; 4]) -> Option<f32> {
let [critical, high, moderate, low] = counts;
let output = score_quality(QualityInputs {
security: Availability::Measured(SecuritySummary {
critical,
high,
moderate,
low,
}),
..nothing_measured()
});
score_of(&output, METRIC_SECURITY)
}
fn unused_reference_score(unused_count: usize) -> Option<f32> {
let output = score_quality(QualityInputs {
unused: Availability::Measured(UnusedSummary { unused_count }),
..nothing_measured()
});
score_of(&output, METRIC_UNUSED_DEPS)
}
fn unsafe_reference_score(total_unsafe: usize) -> Option<f32> {
let output = score_quality(QualityInputs {
unsafe_code: Availability::Measured(UnsafeSummary { total_unsafe }),
..nothing_measured()
});
score_of(&output, METRIC_UNSAFE_CODE)
}
fn assert_sensitive(score: Option<f32>) {
let score = score.expect("the metric under test must be measured");
assert!(
score > 0.0 && score < 100.0,
"fixture scores {score}, where a misread count could be absorbed by the \
0/100 clamp; choose counts that land strictly inside the range"
);
}
fn unavailable_entry<'a>(output: &'a QualityOutput, name: &str) -> &'a UnavailableMetric {
output
.unavailable
.iter()
.find(|metric| metric.name == name)
.unwrap_or_else(|| panic!("expected {name} to be unavailable"))
}
fn virtual_workspace_metadata(
workspace_rust_version: Option<&str>,
inherit_rust_version: bool,
) -> (TempDir, Metadata) {
let temp_dir = tempfile::tempdir().expect("temp dir");
let root = temp_dir.path();
let member_dir = root.join("member");
fs::create_dir_all(member_dir.join("src")).expect("create member src");
let workspace_package = workspace_rust_version
.map(|version| format!("\n[workspace.package]\nrust-version = \"{version}\"\n"))
.unwrap_or_default();
fs::write(
root.join("Cargo.toml"),
format!("[workspace]\nresolver = \"2\"\nmembers = [\"member\"]\n{workspace_package}"),
)
.expect("write workspace manifest");
let inherited = if inherit_rust_version {
"rust-version.workspace = true\n"
} else {
""
};
fs::write(
member_dir.join("Cargo.toml"),
format!(
"[package]\nname = \"member\"\nversion = \"0.1.0\"\nedition = \"2021\"\n{inherited}"
),
)
.expect("write member manifest");
fs::write(member_dir.join("src/lib.rs"), "pub fn stub() {}\n")
.expect("write member source");
let mut command = MetadataCommand::new();
command.manifest_path(root.join("Cargo.toml"));
let metadata = command.exec().expect("load fixture metadata");
(temp_dir, metadata)
}
#[tokio::test]
async fn run_blocking_returns_ok_value() {
let value = run_blocking("ok", || Ok(42)).await.unwrap();
assert_eq!(value, 42);
}
#[tokio::test]
async fn run_blocking_propagates_inner_error() {
let err = run_blocking::<u8, _>("fail", || {
Err(UpkeepError::message(ErrorCode::InvalidData, "nope"))
})
.await
.unwrap_err();
assert_eq!(err.code(), ErrorCode::InvalidData);
}
#[tokio::test]
async fn check_msrv_returns_valid_when_set() {
let status = check_msrv().await.unwrap();
assert!(matches!(
status,
crate::core::scorers::quality::MsrvStatus::Valid
));
}
#[test]
fn virtual_workspace_level_msrv_is_valid_without_member_inheritance() {
let (_temp_dir, metadata) = virtual_workspace_metadata(Some("1.70"), false);
assert!(metadata.root_package().is_none());
assert!(metadata
.workspace_packages()
.iter()
.all(|package| package.rust_version.is_none()));
assert!(matches!(
msrv_status(&metadata).expect("MSRV status"),
MsrvStatus::Valid
));
}
#[test]
fn virtual_workspace_inherited_msrv_is_valid() {
let (_temp_dir, metadata) = virtual_workspace_metadata(Some("1.70"), true);
assert!(metadata.root_package().is_none());
assert!(metadata
.workspace_packages()
.iter()
.all(|package| package.rust_version.is_some()));
assert!(matches!(
msrv_status(&metadata).expect("MSRV status"),
MsrvStatus::Valid
));
}
#[test]
fn virtual_workspace_without_msrv_is_missing() {
let (_temp_dir, metadata) = virtual_workspace_metadata(None, false);
assert!(metadata.root_package().is_none());
assert!(matches!(
msrv_status(&metadata).expect("MSRV status"),
MsrvStatus::Missing
));
}
#[test]
fn build_quality_output_reports_failures_as_unavailable_metrics() {
let output = build_quality_output(
Err(err()),
Err(err()),
Err(err()),
Ok(MsrvStatus::Valid),
Err(err()),
Err(err()),
);
let reported: Vec<&str> = output
.unavailable
.iter()
.map(|metric| metric.name.as_str())
.collect();
assert_eq!(
reported,
vec![
METRIC_DEPENDENCY_FRESHNESS,
METRIC_SECURITY,
METRIC_UNUSED_DEPS,
METRIC_UNSAFE_CODE,
METRIC_CLIPPY,
]
);
assert!(output
.unavailable
.iter()
.all(|metric| metric.reason == UnavailableReason::Failed && metric.detail == "boom"));
assert!(!output.complete);
assert_close(output.measured_weight, 0.10);
assert_close(output.score.expect("score"), 100.0);
assert_eq!(score_of(&output, METRIC_MSRV), Some(100.0));
assert!(output.recommendations.is_empty());
}
#[test]
fn clippy_command_and_quality_breakdown_agree() {
let stdout = r#"{"reason":"compiler-message","message":{"level":"warning","code":{"code":"clippy::needless_return"},"message":"avoid needless return","spans":[{"file_name":"src/lib.rs","line_start":10,"is_primary":true}]}}
{"reason":"compiler-message","message":{"level":"warning","code":{"code":"clippy::redundant_clone"},"message":"redundant clone","spans":[{"file_name":"src/lib.rs","line_start":20,"is_primary":true}]}}
{"reason":"compiler-message","message":{"level":"warning","code":{"code":"clippy::needless_return"},"message":"avoid needless return","spans":[{"file_name":"src/lib.rs","line_start":30,"is_primary":true}]}}
{"reason":"compiler-message","message":{"level":"error","code":{"code":"clippy::panic"},"message":"do not panic","spans":[{"file_name":"src/main.rs","line_start":42,"is_primary":true}]}}"#;
let analyzed = parse_diagnostics(stdout);
assert_eq!(analyzed.warnings, 3);
assert_eq!(analyzed.errors, 1);
let reported_by_clippy = analyzed.score;
let output = build_quality_output(
Err(err()),
Err(err()),
Ok(analyzed),
Err(err()),
Err(err()),
Err(err()),
);
assert_eq!(score_of(&output, METRIC_CLIPPY), Some(reported_by_clippy));
}
#[test]
fn security_summary_mapping_is_not_swappable() {
let expected = security_reference_score(SECURITY_COUNTS);
assert_sensitive(expected);
for (left, right) in [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] {
let mut swapped = SECURITY_COUNTS;
swapped.swap(left, right);
assert_ne!(
security_reference_score(swapped),
expected,
"SECURITY_COUNTS is not sensitive to swapping fields {left} and {right} of \
[critical, high, moderate, low]; a mapping that transposed them would score \
the same and this test could not see it"
);
}
let output = build_quality_output(
Err(err()),
Ok(asymmetric_audit()),
Err(err()),
Err(err()),
Err(err()),
Err(err()),
);
assert_eq!(score_of(&output, METRIC_SECURITY), expected);
}
#[test]
fn audit_warnings_do_not_affect_the_quality_grade() {
let clean = build_quality_output(
Ok(deps_output(0, 0, 0, 0)),
Ok(clean_audit()),
Ok(clean_clippy()),
Ok(MsrvStatus::Valid),
Ok(clean_unused()),
Ok(clean_unsafe()),
);
let warned = build_quality_output(
Ok(deps_output(0, 0, 0, 0)),
Ok(warning_only_audit()),
Ok(clean_clippy()),
Ok(MsrvStatus::Valid),
Ok(clean_unused()),
Ok(clean_unsafe()),
);
assert_eq!(score_of(&warned, METRIC_SECURITY), Some(100.0));
assert_eq!(warned.score, clean.score);
assert_eq!(warned.grade, clean.grade);
}
#[test]
fn unused_summary_mapping_reads_the_confirmed_list() {
let expected = unused_reference_score(UNUSED_CONFIRMED);
assert_sensitive(expected);
for wrong in [UNUSED_POSSIBLE, UNUSED_CONFIRMED + UNUSED_POSSIBLE] {
assert_ne!(
unused_reference_score(wrong),
expected,
"a mapping counting {wrong} entries would score the same as the correct \
{UNUSED_CONFIRMED}; the fixture cannot tell the two apart"
);
}
let output = build_quality_output(
Err(err()),
Err(err()),
Err(err()),
Err(err()),
Ok(asymmetric_unused()),
Err(err()),
);
assert_eq!(score_of(&output, METRIC_UNUSED_DEPS), expected);
}
#[test]
fn unsafe_summary_mapping_reads_the_total() {
let unsafe_output = asymmetric_unsafe();
let summary = &unsafe_output.summary;
let expected = unsafe_reference_score(summary.total_unsafe);
assert_sensitive(expected);
for (field, count) in [
("packages", summary.packages),
("unsafe_functions", summary.unsafe_functions),
("unsafe_impls", summary.unsafe_impls),
("unsafe_traits", summary.unsafe_traits),
("unsafe_blocks", summary.unsafe_blocks),
("unsafe_expressions", summary.unsafe_expressions),
] {
assert_ne!(
unsafe_reference_score(count),
expected,
"a mapping reading `{field}` ({count}) instead of `total_unsafe` ({}) would \
score the same; the fixture cannot tell the two apart",
summary.total_unsafe
);
}
let output = build_quality_output(
Err(err()),
Err(err()),
Err(err()),
Err(err()),
Err(err()),
Ok(unsafe_output),
);
assert_eq!(score_of(&output, METRIC_UNSAFE_CODE), expected);
}
#[test]
fn build_quality_output_marks_missing_machete_as_not_installed() {
let output = build_quality_output(
Err(err()),
Err(err()),
Err(err()),
Ok(MsrvStatus::Valid),
Err(missing_tool("machete")),
Err(err()),
);
let entry = unavailable_entry(&output, METRIC_UNUSED_DEPS);
assert_eq!(entry.reason, UnavailableReason::NotInstalled);
assert_close(entry.weight, WEIGHT_UNUSED_DEPS);
assert!(entry.detail.contains("cargo install cargo-machete"));
assert_eq!(score_of(&output, METRIC_UNUSED_DEPS), None);
assert_eq!(
unavailable_entry(&output, METRIC_SECURITY).reason,
UnavailableReason::Failed
);
}
#[test]
fn build_quality_output_marks_missing_geiger_as_not_installed() {
let output = build_quality_output(
Err(err()),
Err(err()),
Err(err()),
Ok(MsrvStatus::Valid),
Err(err()),
Err(missing_tool("geiger")),
);
let entry = unavailable_entry(&output, METRIC_UNSAFE_CODE);
assert_eq!(entry.reason, UnavailableReason::NotInstalled);
assert!(entry.detail.contains("cargo install cargo-geiger"));
assert_eq!(score_of(&output, METRIC_UNSAFE_CODE), None);
}
#[test]
fn build_quality_output_failed_msrv_check_is_unavailable_not_missing() {
let output = build_quality_output(
Ok(deps_output(10, 10, 0, 0)),
Ok(clean_audit()),
Ok(clean_clippy()),
Err(err()),
Ok(clean_unused()),
Ok(clean_unsafe()),
);
assert_eq!(
unavailable_entry(&output, METRIC_MSRV).reason,
UnavailableReason::Failed
);
assert_eq!(score_of(&output, METRIC_MSRV), None);
assert_eq!(output.unavailable.len(), 1);
assert!(!output
.recommendations
.contains(&"Declare a valid MSRV in Cargo.toml.".to_string()));
assert_close(output.score.expect("score"), 100.0);
assert_eq!(output.grade, Some(Grade::A));
assert!(!output.complete);
assert_close(output.measured_weight, 0.90);
}
#[test]
fn build_quality_output_registry_unavailable_is_not_perfect_freshness() {
let output = build_quality_output(
Ok(deps_output(10, 0, 0, 10)),
Ok(clean_audit()),
Ok(clean_clippy()),
Ok(MsrvStatus::Valid),
Ok(clean_unused()),
Ok(clean_unsafe()),
);
assert_eq!(score_of(&output, METRIC_DEPENDENCY_FRESHNESS), None);
let entry = unavailable_entry(&output, METRIC_DEPENDENCY_FRESHNESS);
assert_eq!(entry.reason, UnavailableReason::Failed);
assert!(
entry.detail.contains("crates.io registry was unavailable"),
"unexpected detail: {}",
entry.detail
);
assert!(!output.complete);
assert_close(output.measured_weight, 0.80);
assert!(!output
.recommendations
.contains(&"Update outdated dependencies.".to_string()));
}
#[test]
fn build_quality_output_all_unsupported_registries_is_unavailable() {
let mut deps = deps_output(2, 0, 0, 0);
deps.skipped_packages.push(SkippedDependency {
name: "private-crate".to_string(),
alias: None,
required: "1.0".to_string(),
reason: SkipReason::UnsupportedRegistry,
dependency_type: DependencyType::Normal,
source: Some("registry+https://packages.example.com/index".to_string()),
target: Some("cfg(unix)".to_string()),
});
deps.skipped = deps.skipped_packages.len();
let output = build_quality_output(
Ok(deps),
Ok(clean_audit()),
Ok(clean_clippy()),
Ok(MsrvStatus::Valid),
Ok(clean_unused()),
Ok(clean_unsafe()),
);
assert_eq!(score_of(&output, METRIC_DEPENDENCY_FRESHNESS), None);
let entry = unavailable_entry(&output, METRIC_DEPENDENCY_FRESHNESS);
assert_eq!(entry.reason, UnavailableReason::Failed);
assert!(entry.detail.contains("no supported registry comparison"));
assert!(!output.complete);
}
#[test]
fn build_quality_output_all_missing_resolve_is_unavailable() {
let mut deps = deps_output(1, 0, 0, 0);
deps.skipped_packages.push(SkippedDependency {
name: "unresolved".to_string(),
alias: None,
required: "1.0".to_string(),
reason: SkipReason::MissingResolve,
dependency_type: DependencyType::Normal,
source: None,
target: None,
});
deps.skipped = 1;
let output = build_quality_output(
Ok(deps),
Ok(clean_audit()),
Ok(clean_clippy()),
Ok(MsrvStatus::Valid),
Ok(clean_unused()),
Ok(clean_unsafe()),
);
assert_eq!(score_of(&output, METRIC_DEPENDENCY_FRESHNESS), None);
assert!(!output.complete);
}
#[test]
fn build_quality_output_all_ambiguous_resolve_is_unavailable() {
let mut deps = deps_output(1, 0, 0, 0);
deps.skipped_packages.push(SkippedDependency {
name: "wasm-bindgen".to_string(),
alias: None,
required: "=0.2.100".to_string(),
reason: SkipReason::AmbiguousResolve,
dependency_type: DependencyType::Normal,
source: None,
target: None,
});
deps.skipped = 1;
let output = build_quality_output(
Ok(deps),
Ok(clean_audit()),
Ok(clean_clippy()),
Ok(MsrvStatus::Valid),
Ok(clean_unused()),
Ok(clean_unsafe()),
);
assert_eq!(score_of(&output, METRIC_DEPENDENCY_FRESHNESS), None);
assert!(!output.complete);
}
#[test]
fn build_quality_output_registry_outage_is_unmeasured_when_edges_exceed_groups() {
let output = build_quality_output(
Ok(deps_output(2, 0, 0, 1)),
Ok(clean_audit()),
Ok(clean_clippy()),
Ok(MsrvStatus::Valid),
Ok(clean_unused()),
Ok(clean_unsafe()),
);
assert_eq!(
score_of(&output, METRIC_DEPENDENCY_FRESHNESS),
None,
"freshness must be unavailable, not a perfect score over a comparison never made"
);
let entry = unavailable_entry(&output, METRIC_DEPENDENCY_FRESHNESS);
assert_eq!(entry.reason, UnavailableReason::Failed);
assert!(
entry.detail.contains("crates.io registry was unavailable"),
"unexpected detail: {}",
entry.detail
);
assert!(!output.complete);
assert_close(output.measured_weight, 0.80);
}
#[test]
fn build_quality_output_partial_outage_scores_over_groups_not_edges() {
let output = build_quality_output(
Ok(deps_output(10, 6, 3, 2)),
Ok(clean_audit()),
Ok(clean_clippy()),
Ok(MsrvStatus::Valid),
Ok(clean_unused()),
Ok(clean_unsafe()),
);
assert_close(
score_of(&output, METRIC_DEPENDENCY_FRESHNESS).expect("freshness measured"),
50.0,
);
assert!(output.complete);
}
#[test]
fn build_quality_output_partial_registry_outage_scores_the_checked_subset() {
let output = build_quality_output(
Ok(deps_output(10, 6, 3, 4)),
Ok(clean_audit()),
Ok(clean_clippy()),
Ok(MsrvStatus::Valid),
Ok(clean_unused()),
Ok(clean_unsafe()),
);
assert_close(
score_of(&output, METRIC_DEPENDENCY_FRESHNESS).expect("freshness measured"),
50.0,
);
assert!(output.complete);
assert_close(output.measured_weight, 1.0);
assert!(output
.recommendations
.contains(&"Update outdated dependencies.".to_string()));
}
#[test]
fn build_quality_output_non_registry_skips_do_not_shrink_the_denominator() {
let mut deps = deps_output(10, 10, 2, 0);
for (index, reason) in [
SkipReason::NonRegistry,
SkipReason::TargetSpecific,
SkipReason::OptionalNotActivated,
]
.into_iter()
.enumerate()
{
deps.skipped_packages.push(SkippedDependency {
name: format!("not-applicable-{index}"),
alias: None,
required: "1.0".to_string(),
reason,
dependency_type: DependencyType::Normal,
source: None,
target: None,
});
}
deps.skipped = deps.skipped_packages.len();
let output = build_quality_output(
Ok(deps),
Ok(clean_audit()),
Ok(clean_clippy()),
Ok(MsrvStatus::Valid),
Ok(clean_unused()),
Ok(clean_unsafe()),
);
assert_close(
score_of(&output, METRIC_DEPENDENCY_FRESHNESS).expect("freshness measured"),
80.0,
);
assert!(output.complete);
}
#[test]
fn build_quality_output_no_dependencies_is_measured_at_one_hundred() {
let output = build_quality_output(
Ok(deps_output(0, 0, 0, 0)),
Ok(clean_audit()),
Ok(clean_clippy()),
Ok(MsrvStatus::Valid),
Ok(clean_unused()),
Ok(clean_unsafe()),
);
assert_close(
score_of(&output, METRIC_DEPENDENCY_FRESHNESS).expect("freshness measured"),
100.0,
);
assert!(output.complete);
}
#[test]
fn build_quality_output_total_failure_yields_no_grade() {
let output = build_quality_output(
Err(err()),
Err(err()),
Err(err()),
Err(err()),
Err(err()),
Err(err()),
);
assert_eq!(output.score, None);
assert_eq!(output.grade, None);
assert!(!output.complete);
assert_close(output.measured_weight, 0.0);
assert_eq!(output.unavailable.len(), 6);
assert!(output.breakdown.iter().all(|metric| metric.score.is_none()));
let text = format!("{output}");
assert!(text.contains("Analysis incomplete: 6 of 6 metrics"));
assert!(text.contains("Score: unavailable"));
assert!(text.contains("Grade: unavailable"));
assert!(!text.contains("Grade: A"));
}
#[test]
fn emit_output_json_shape() {
let output = QualityOutput {
score: Some(92.5),
grade: Some(Grade::A),
complete: false,
measured_weight: 0.75,
breakdown: vec![
MetricScore {
name: METRIC_SECURITY.to_string(),
score: Some(90.0),
weight: 0.25,
},
MetricScore {
name: METRIC_UNUSED_DEPS.to_string(),
score: None,
weight: 0.15,
},
],
unavailable: vec![UnavailableMetric {
name: METRIC_UNUSED_DEPS.to_string(),
weight: 0.15,
reason: UnavailableReason::NotInstalled,
detail: "cargo-machete is not installed".to_string(),
}],
recommendations: vec!["Address advisories".to_string()],
};
let value = serde_json::to_value(&output).expect("serialize");
assert_eq!(value["grade"], Value::String("A".into()));
assert_eq!(value["complete"], Value::Bool(false));
assert_eq!(
value["breakdown"][0]["name"],
Value::String("Security".into())
);
assert_eq!(value["breakdown"][1]["score"], Value::Null);
assert_eq!(
value["unavailable"][0]["reason"],
Value::String("not_installed".into())
);
}
#[test]
fn emit_output_json_nulls_score_and_grade_when_nothing_measured() {
let output = build_quality_output(
Err(err()),
Err(err()),
Err(err()),
Err(err()),
Err(err()),
Err(err()),
);
let value = serde_json::to_value(&output).expect("serialize");
assert_eq!(value["score"], Value::Null);
assert_eq!(value["grade"], Value::Null);
assert_eq!(value["complete"], Value::Bool(false));
assert_eq!(value["breakdown"].as_array().expect("breakdown").len(), 6);
assert!(value["breakdown"]
.as_array()
.expect("breakdown")
.iter()
.all(|metric| metric["score"] == Value::Null));
}
fn complete_run() -> QualityOutput {
let output = build_quality_output(
Ok(deps_output(0, 0, 0, 0)),
Ok(clean_audit()),
Ok(clean_clippy()),
Ok(MsrvStatus::Valid),
Ok(clean_unused()),
Ok(clean_unsafe()),
);
assert!(output.complete && output.score.is_some(), "fixture premise");
output
}
fn partial_run() -> QualityOutput {
let output = build_quality_output(
Err(err()),
Err(err()),
Err(err()),
Ok(MsrvStatus::Valid),
Err(err()),
Err(err()),
);
assert!(
!output.complete && output.score.is_some(),
"fixture premise"
);
output
}
fn unmeasured_run() -> QualityOutput {
let output = build_quality_output(
Err(err()),
Err(err()),
Err(err()),
Err(err()),
Err(err()),
Err(err()),
);
assert!(output.score.is_none(), "fixture premise");
output
}
#[test]
fn exit_policy_passes_a_complete_run_either_way() {
enforce_exit_policy(&complete_run(), false).expect("complete run exits zero");
enforce_exit_policy(&complete_run(), true)
.expect("--require-complete is satisfied by a complete run");
}
#[test]
fn exit_policy_passes_a_partial_run_by_default() {
enforce_exit_policy(&partial_run(), false).expect("partial run exits zero by default");
}
#[test]
fn exit_policy_fails_a_partial_run_under_require_complete() {
let err = enforce_exit_policy(&partial_run(), true).expect_err("--require-complete fails");
assert_eq!(err.code(), ErrorCode::IncompleteAnalysis);
}
#[test]
fn exit_policy_always_fails_a_run_that_measured_nothing() {
let err = enforce_exit_policy(&unmeasured_run(), false)
.expect_err("no score must exit nonzero without the flag");
assert_eq!(err.code(), ErrorCode::IncompleteAnalysis);
let err = enforce_exit_policy(&unmeasured_run(), true)
.expect_err("no score must exit nonzero with the flag");
assert_eq!(err.code(), ErrorCode::IncompleteAnalysis);
}
#[test]
fn exit_policy_failures_explain_themselves() {
let unmeasured = enforce_exit_policy(&unmeasured_run(), false)
.expect_err("no score")
.to_string();
assert!(
unmeasured.contains("measured nothing")
&& unmeasured.contains("all 6 metrics were unavailable")
&& unmeasured.contains("why each one could not run"),
"unhelpful message: {unmeasured}"
);
let partial = enforce_exit_policy(&partial_run(), true)
.expect_err("--require-complete")
.to_string();
assert!(
partial.contains("5 of 6 metrics could not be measured")
&& partial.contains("10% of weight measured")
&& partial.contains("--require-complete"),
"unhelpful message: {partial}"
);
}
}