use super::{
Check, CheckResult, CheckStatus, ProvenanceBuilder, TEST_TIMEOUT_SECS, has_tool_crash,
run_command, run_command_with_timeout,
};
use crate::Config;
use crate::cache;
use anyhow::Result;
use async_trait::async_trait;
use chrono::Local;
use std::path::Path;
pub struct CargoCheck;
pub struct ClippyCheck;
pub struct CargoTestCheck;
pub struct RustfmtCheck;
pub struct CargoAuditCheck;
pub struct CargoGeigerCheck;
#[async_trait]
impl Check for CargoCheck {
fn name(&self) -> &str {
"Cargo check"
}
fn check_eligibility(&self, config: &Config) -> super::CheckEligibility {
if !config.profile.has_cargo {
return super::CheckEligibility::Skip(format!(
"profile {}",
config.profile.kind.as_str().to_lowercase()
));
}
super::CheckEligibility::Run
}
fn cache_key(&self, config: &Config) -> Option<String> {
Some(cache::rust_hash(&config.repo_root))
}
async fn run(&self, config: &Config) -> Result<CheckResult> {
let start = std::time::Instant::now();
let started_at = Local::now().to_rfc3339();
let cwd = config
.profile
.cargo_root
.as_ref()
.unwrap_or(&config.repo_root);
let args = &["check", "--message-format=short"];
let output = run_command("cargo", args, cwd).await?;
let finished_at = Local::now().to_rfc3339();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!("{}\n{}", stdout, stderr);
let status = if output.status.success() {
CheckStatus::Passed
} else {
CheckStatus::Failed
};
Ok(CheckResult {
name: self.name().to_string(),
status,
duration: start.elapsed(),
output: combined.clone(),
cached: false,
provenance: Some(
ProvenanceBuilder {
cmd: "cargo",
args,
cwd,
output: &output,
combined_output: &combined,
started_at: &started_at,
finished_at: &finished_at,
cache_key: self.cache_key(config),
}
.build_with_repo_root(Some(&config.repo_root)),
),
})
}
}
#[async_trait]
impl Check for ClippyCheck {
fn name(&self) -> &str {
"Clippy"
}
fn check_eligibility(&self, config: &Config) -> super::CheckEligibility {
if !config.profile.has_cargo {
return super::CheckEligibility::Skip(format!(
"profile {}",
config.profile.kind.as_str().to_lowercase()
));
}
if config.is_fast_remote_only_standard() && !config.should_run_heavy_rust_lint() {
return super::CheckEligibility::Skip("fast remote-only preset".to_string());
}
if !config.run_lint {
return super::CheckEligibility::Skip("lint disabled".to_string());
}
if !config.should_run_heavy_rust_lint() {
return super::CheckEligibility::Skip("lint disabled".to_string());
}
super::CheckEligibility::Run
}
fn cache_key(&self, config: &Config) -> Option<String> {
Some(format!("clippy-{}", cache::rust_hash(&config.repo_root)))
}
async fn run(&self, config: &Config) -> Result<CheckResult> {
let start = std::time::Instant::now();
let started_at = Local::now().to_rfc3339();
let cwd = config
.profile
.cargo_root
.as_ref()
.unwrap_or(&config.repo_root);
let args = &["clippy", "--message-format=short", "--", "-D", "warnings"];
let output = run_command("cargo", args, cwd).await?;
let finished_at = Local::now().to_rfc3339();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!("{}\n{}", stdout, stderr);
let status = if output.status.success() {
if clippy_has_real_warnings(&combined) {
CheckStatus::Warnings
} else {
CheckStatus::Passed
}
} else {
CheckStatus::Failed
};
Ok(CheckResult {
name: self.name().to_string(),
status,
duration: start.elapsed(),
output: combined.clone(),
cached: false,
provenance: Some(
ProvenanceBuilder {
cmd: "cargo",
args,
cwd,
output: &output,
combined_output: &combined,
started_at: &started_at,
finished_at: &finished_at,
cache_key: self.cache_key(config),
}
.build_with_repo_root(Some(&config.repo_root)),
),
})
}
}
fn is_cargo_build_script_warning(line: &str) -> bool {
let trimmed = line.trim_start();
let Some(rest) = trimmed.strip_prefix("warning: ") else {
return false;
};
match rest.split_once(": ") {
Some((prefix, _)) => prefix.contains('@') && !prefix.contains(char::is_whitespace),
None => false,
}
}
fn clippy_has_real_warnings(combined: &str) -> bool {
combined
.lines()
.filter(|line| line.contains("warning:"))
.any(|line| !is_cargo_build_script_warning(line))
}
#[async_trait]
impl Check for CargoTestCheck {
fn name(&self) -> &str {
"Cargo test"
}
fn check_eligibility(&self, config: &Config) -> super::CheckEligibility {
if !config.profile.has_cargo {
return super::CheckEligibility::Skip(format!(
"profile {}",
config.profile.kind.as_str().to_lowercase()
));
}
if config.is_fast_remote_only_standard() && !config.run_tests {
return super::CheckEligibility::Skip("fast remote-only preset".to_string());
}
if !config.run_tests {
return super::CheckEligibility::Skip("tests disabled".to_string());
}
super::CheckEligibility::Run
}
fn cache_key(&self, _config: &Config) -> Option<String> {
None
}
async fn run(&self, config: &Config) -> Result<CheckResult> {
let start = std::time::Instant::now();
let started_at = Local::now().to_rfc3339();
let cwd = config
.profile
.cargo_root
.as_ref()
.unwrap_or(&config.repo_root);
let args = &["test", "--all-targets", "--no-fail-fast"];
let output = run_command_with_timeout("cargo", args, cwd, TEST_TIMEOUT_SECS).await?;
let finished_at = Local::now().to_rfc3339();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!("{}\n{}", stdout, stderr);
let status = if output.status.success() {
CheckStatus::Passed
} else {
CheckStatus::Failed
};
Ok(CheckResult {
name: self.name().to_string(),
status,
duration: start.elapsed(),
output: combined.clone(),
cached: false,
provenance: Some(
ProvenanceBuilder {
cmd: "cargo",
args,
cwd,
output: &output,
combined_output: &combined,
started_at: &started_at,
finished_at: &finished_at,
cache_key: self.cache_key(config),
}
.build_with_repo_root(Some(&config.repo_root)),
),
})
}
}
#[async_trait]
impl Check for RustfmtCheck {
fn name(&self) -> &str {
"Rustfmt"
}
fn check_eligibility(&self, config: &Config) -> super::CheckEligibility {
if !config.profile.has_cargo {
return super::CheckEligibility::Skip(format!(
"profile {}",
config.profile.kind.as_str().to_lowercase()
));
}
if config.is_fast_remote_only_standard() && !config.should_run_heavy_rust_lint() {
return super::CheckEligibility::Skip("fast remote-only preset".to_string());
}
if !config.run_lint {
return super::CheckEligibility::Skip("lint disabled".to_string());
}
if !config.should_run_heavy_rust_lint() {
return super::CheckEligibility::Skip("lint disabled".to_string());
}
super::CheckEligibility::Run
}
fn cache_key(&self, config: &Config) -> Option<String> {
Some(format!("rustfmt-{}", cache::rust_hash(&config.repo_root)))
}
async fn run(&self, config: &Config) -> Result<CheckResult> {
let start = std::time::Instant::now();
let started_at = Local::now().to_rfc3339();
let cwd = config
.profile
.cargo_root
.as_ref()
.unwrap_or(&config.repo_root);
let args = &["fmt", "--check"];
let output = run_command("cargo", args, cwd).await?;
let finished_at = Local::now().to_rfc3339();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!("{}\n{}", stdout, stderr);
let status = if output.status.success() {
CheckStatus::Passed
} else {
CheckStatus::Warnings
};
Ok(CheckResult {
name: self.name().to_string(),
status,
duration: start.elapsed(),
output: combined.clone(),
cached: false,
provenance: Some(
ProvenanceBuilder {
cmd: "cargo",
args,
cwd,
output: &output,
combined_output: &combined,
started_at: &started_at,
finished_at: &finished_at,
cache_key: self.cache_key(config),
}
.build_with_repo_root(Some(&config.repo_root)),
),
})
}
}
#[async_trait]
impl Check for CargoAuditCheck {
fn name(&self) -> &str {
"Cargo audit"
}
fn check_eligibility(&self, config: &Config) -> super::CheckEligibility {
if !config.profile.has_cargo {
return super::CheckEligibility::Skip(format!(
"profile {}",
config.profile.kind.as_str().to_lowercase()
));
}
if !config.run_security && !config.run_lint {
return super::CheckEligibility::Skip("security disabled".to_string());
}
if which::which("cargo-audit").is_err() {
return super::CheckEligibility::Skip(
"tool not installed (cargo-audit is missing)".to_string(),
);
}
super::CheckEligibility::Run
}
fn cache_key(&self, config: &Config) -> Option<String> {
let day = Local::now().format("%Y-%m-%d");
let cargo_root = config
.profile
.cargo_root
.as_ref()
.unwrap_or(&config.repo_root);
Some(format!(
"audit-{}-{}",
cache::cargo_lock_hash(cargo_root),
day
))
}
async fn run(&self, config: &Config) -> Result<CheckResult> {
let start = std::time::Instant::now();
let started_at = Local::now().to_rfc3339();
let cwd = config
.profile
.cargo_root
.as_ref()
.unwrap_or(&config.repo_root);
let args = &["audit", "--json"];
let output = run_command("cargo", args, cwd).await?;
let finished_at = Local::now().to_rfc3339();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!("{}\n{}", stdout, stderr);
let status = classify_cargo_audit_status(output.status.success(), &stdout, &combined);
Ok(CheckResult {
name: self.name().to_string(),
status,
duration: start.elapsed(),
output: combined.clone(),
cached: false,
provenance: Some(
ProvenanceBuilder {
cmd: "cargo",
args,
cwd,
output: &output,
combined_output: &combined,
started_at: &started_at,
finished_at: &finished_at,
cache_key: self.cache_key(config),
}
.build_with_repo_root(Some(&config.repo_root)),
),
})
}
}
fn classify_cargo_audit_status(
command_succeeded: bool,
stdout: &str,
combined: &str,
) -> CheckStatus {
if let Some(vulnerability_count) = cargo_audit_vulnerability_count(stdout) {
if vulnerability_count > 0 {
return CheckStatus::Failed;
}
if command_succeeded {
return CheckStatus::Passed;
}
if cargo_audit_has_warnings(combined) {
return CheckStatus::Warnings;
}
return CheckStatus::Failed;
}
if command_succeeded {
return CheckStatus::Passed;
}
if cargo_audit_has_warnings(combined) {
return CheckStatus::Warnings;
}
if combined.contains("RUSTSEC-") {
return CheckStatus::Failed;
}
CheckStatus::Failed
}
fn cargo_audit_vulnerability_count(stdout: &str) -> Option<usize> {
let parsed = serde_json::from_str::<serde_json::Value>(stdout).ok()?;
let vulnerabilities = parsed.get("vulnerabilities")?;
if let Some(count) = vulnerabilities
.get("count")
.and_then(|value| value.as_u64())
{
return Some(count as usize);
}
if let Some(list) = vulnerabilities
.get("list")
.and_then(|value| value.as_array())
{
return Some(list.len());
}
Some(0)
}
fn cargo_audit_has_warnings(output: &str) -> bool {
output.to_ascii_lowercase().contains("warning")
}
#[async_trait]
impl Check for CargoGeigerCheck {
fn name(&self) -> &str {
"Cargo geiger"
}
fn check_eligibility(&self, config: &Config) -> super::CheckEligibility {
if !config.profile.has_cargo {
return super::CheckEligibility::Skip(format!(
"profile {}",
config.profile.kind.as_str().to_lowercase()
));
}
if !config.run_security {
return super::CheckEligibility::Skip("security disabled".to_string());
}
if !config.security_full {
return super::CheckEligibility::Skip("requires --security-full".to_string());
}
if which::which("cargo-geiger").is_err() {
return super::CheckEligibility::Skip(
"tool not installed (cargo-geiger is missing)".to_string(),
);
}
super::CheckEligibility::Run
}
fn cache_key(&self, config: &Config) -> Option<String> {
Some(format!("geiger-{}", cache::rust_hash(&config.repo_root)))
}
async fn run(&self, config: &Config) -> Result<CheckResult> {
let start = std::time::Instant::now();
let started_at = Local::now().to_rfc3339();
let cwd = config
.profile
.cargo_root
.as_ref()
.unwrap_or(&config.repo_root);
if cargo_metadata_is_virtual_manifest(cwd).await {
return Ok(CheckResult {
name: self.name().to_string(),
status: CheckStatus::Skipped,
duration: start.elapsed(),
output: "Cargo geiger skipped: cargo metadata reports a virtual workspace manifest; cargo-geiger requires a concrete package. Configure package selection or run geiger per workspace member.".to_string(),
cached: false,
provenance: None,
});
}
let args = &["geiger", "--output-format", "Ratio"];
let output = match run_command_with_timeout("cargo", args, cwd, 600).await {
Ok(output) => output,
Err(err) if super::is_timeout_error(&err) => {
return Ok(CheckResult {
name: self.name().to_string(),
status: CheckStatus::Skipped,
duration: start.elapsed(),
output: format!("cargo geiger skipped: {err}"),
cached: false,
provenance: None,
});
}
Err(err) => return Err(err),
};
let finished_at = Local::now().to_rfc3339();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let combined = format!("{}\n{}", stdout, stderr);
let status = classify_cargo_geiger_status(output.status.success(), &combined);
Ok(CheckResult {
name: self.name().to_string(),
status,
duration: start.elapsed(),
output: combined.clone(),
cached: false,
provenance: Some(
ProvenanceBuilder {
cmd: "cargo",
args,
cwd,
output: &output,
combined_output: &combined,
started_at: &started_at,
finished_at: &finished_at,
cache_key: self.cache_key(config),
}
.build_with_repo_root(Some(&config.repo_root)),
),
})
}
}
fn classify_cargo_geiger_status(command_succeeded: bool, output: &str) -> CheckStatus {
if output.contains("is a virtual manifest")
&& output.contains("requires running against an actual package")
{
return CheckStatus::Skipped;
}
if has_tool_crash(output) {
return CheckStatus::Error;
}
if is_virtual_manifest_error(output) {
return CheckStatus::Skipped;
}
let dependency_scan_warnings = output
.lines()
.any(|line| line.starts_with("WARNING: Dependency file was never scanned:"));
let warning_summary = output.lines().any(|line| {
line.starts_with("error: Found ")
&& line
.split_whitespace()
.last()
.is_some_and(|token| token == "warnings")
});
if !command_succeeded {
if dependency_scan_warnings && warning_summary {
return CheckStatus::Warnings;
}
return CheckStatus::Error;
}
match geiger_unsafe_found(output) {
Some(true) => CheckStatus::Warnings,
Some(false) => CheckStatus::Passed,
None => CheckStatus::Warnings,
}
}
fn geiger_unsafe_found(output: &str) -> Option<bool> {
let mut saw_row = false;
let mut unsafe_found = false;
for token in output.split_whitespace() {
let Some((ratio, _pct)) = token.split_once('=') else {
continue;
};
let Some((safe, total)) = ratio.split_once('/') else {
continue;
};
let (Ok(safe), Ok(total)) = (safe.parse::<u64>(), total.parse::<u64>()) else {
continue;
};
saw_row = true;
if total > safe {
unsafe_found = true;
}
}
saw_row.then_some(unsafe_found)
}
fn is_virtual_manifest_error(output: &str) -> bool {
output.contains("virtual manifest")
&& output.contains("requires running against an actual package")
}
async fn cargo_metadata_is_virtual_manifest(cwd: &Path) -> bool {
let Ok(output) = crate::checks::run_command_with_timeout(
"cargo",
&["metadata", "--no-deps", "--format-version", "1"],
cwd,
60,
)
.await
else {
return false;
};
if !output.status.success() {
return false;
}
let Ok(metadata) = serde_json::from_slice::<serde_json::Value>(&output.stdout) else {
return false;
};
metadata.get("root_package").is_some_and(|v| v.is_null())
&& metadata
.get("workspace_members")
.and_then(|v| v.as_array())
.is_some_and(|members| !members.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::ExecutionMode;
use crate::config::{test_config_builder, test_rust_profile};
fn create_test_config(has_cargo: bool, run_lint: bool, run_tests: bool) -> Config {
test_config_builder()
.profile(test_rust_profile(has_cargo))
.execution_mode(ExecutionMode::Standard)
.run_lint(run_lint)
.run_tests(run_tests)
.do_fetch(false)
.use_cache(false)
.create_zip(false)
.build()
}
#[test]
fn test_cargo_check_name() {
let check = CargoCheck;
assert_eq!(check.name(), "Cargo check");
}
#[test]
fn test_clippy_check_name() {
let check = ClippyCheck;
assert_eq!(check.name(), "Clippy");
}
#[test]
fn test_cargo_check_can_run_with_cargo() {
let config = create_test_config(true, false, false);
let check = CargoCheck;
assert_eq!(
check.check_eligibility(&config),
super::super::CheckEligibility::Run
);
}
#[test]
fn test_cargo_check_cannot_run_without_cargo() {
let config = create_test_config(false, false, false);
let check = CargoCheck;
assert!(matches!(
check.check_eligibility(&config),
super::super::CheckEligibility::Skip(_)
));
}
#[test]
fn test_clippy_check_can_run() {
let config = create_test_config(true, true, false);
let check = ClippyCheck;
assert_eq!(
check.check_eligibility(&config),
super::super::CheckEligibility::Run
);
}
#[test]
fn test_clippy_check_cannot_run_without_lint() {
let config = create_test_config(true, false, false);
let check = ClippyCheck;
assert!(matches!(
check.check_eligibility(&config),
super::super::CheckEligibility::Skip(_)
));
}
#[test]
fn test_clippy_check_cannot_run_without_cargo() {
let config = create_test_config(false, true, false);
let check = ClippyCheck;
assert!(matches!(
check.check_eligibility(&config),
super::super::CheckEligibility::Skip(_)
));
}
#[test]
fn test_clippy_check_skips_fast_remote_only_by_default() {
let mut config = create_test_config(true, true, false);
config.remote_only = true;
let check = ClippyCheck;
assert!(matches!(
check.check_eligibility(&config),
super::super::CheckEligibility::Skip(_)
));
}
#[test]
fn test_clippy_check_can_run_fast_remote_only_when_forced() {
let mut config = create_test_config(true, true, false);
config.remote_only = true;
config.lint_forced = true;
let check = ClippyCheck;
assert_eq!(
check.check_eligibility(&config),
super::super::CheckEligibility::Run
);
}
#[test]
fn test_cargo_test_check_can_run() {
let config = create_test_config(true, false, true);
let check = CargoTestCheck;
assert_eq!(
check.check_eligibility(&config),
super::super::CheckEligibility::Run
);
}
#[test]
fn test_cargo_test_check_cannot_run_without_tests() {
let config = create_test_config(true, false, false);
let check = CargoTestCheck;
assert!(matches!(
check.check_eligibility(&config),
super::super::CheckEligibility::Skip(_)
));
}
#[test]
fn test_cargo_geiger_requires_security_full() {
let mut config = create_test_config(true, true, false);
config.run_security = true;
assert!(!config.security_full);
let check = CargoGeigerCheck;
assert!(matches!(
check.check_eligibility(&config),
super::super::CheckEligibility::Skip(_)
));
}
#[test]
fn test_cargo_geiger_skipped_when_security_disabled() {
let mut config = create_test_config(true, true, false);
config.run_security = false;
config.security_full = true;
let check = CargoGeigerCheck;
match check.check_eligibility(&config) {
super::super::CheckEligibility::Skip(reason) => {
assert!(
reason.contains("security disabled"),
"unexpected skip reason: {reason}"
);
}
super::super::CheckEligibility::Run => {
panic!("geiger must not run when security is globally disabled")
}
}
}
#[test]
fn test_cargo_check_cache_key() {
let config = create_test_config(true, false, false);
let check = CargoCheck;
let key = check.cache_key(&config);
assert!(key.is_some());
}
#[test]
fn test_clippy_check_cache_key() {
let config = create_test_config(true, true, false);
let check = ClippyCheck;
let key = check.cache_key(&config);
assert!(key.is_some());
assert!(key.unwrap().starts_with("clippy-"));
}
#[test]
fn test_cargo_audit_cache_key_is_day_scoped() {
let config = create_test_config(true, true, false);
let check = CargoAuditCheck;
let key = check.cache_key(&config).expect("audit cache key");
assert!(key.starts_with("audit-"), "unexpected key: {key}");
let today = Local::now().format("%Y-%m-%d").to_string();
assert!(
key.ends_with(&today),
"audit key must be scoped to the current day ({today}), got: {key}"
);
}
#[test]
fn test_cargo_audit_cache_key_follows_cargo_root_not_repo_root() {
let tmp = tempfile::tempdir().expect("tempdir");
let root = tmp.path();
let member = root.join("member");
std::fs::create_dir_all(&member).unwrap();
std::fs::write(root.join("Cargo.toml"), "[workspace]\n").unwrap();
std::fs::write(root.join("Cargo.lock"), "# root lock\n").unwrap();
std::fs::write(member.join("Cargo.toml"), "[package]\nname = \"m\"\n").unwrap();
std::fs::write(member.join("Cargo.lock"), "# member lock DIFFERENT\n").unwrap();
let mut root_profile = test_rust_profile(true);
root_profile.cargo_root = Some(root.to_path_buf());
let config_root = test_config_builder()
.repo_root(root)
.profile(root_profile)
.build();
let mut member_profile = test_rust_profile(true);
member_profile.cargo_root = Some(member.clone());
let config_member = test_config_builder()
.repo_root(root)
.profile(member_profile)
.build();
let check = CargoAuditCheck;
let key_root = check.cache_key(&config_root).expect("root key");
let key_member = check.cache_key(&config_member).expect("member key");
assert_ne!(
key_root, key_member,
"audit key must follow cargo_root, not the shared repo root"
);
}
#[test]
fn test_cargo_audit_vulnerabilities_are_failed() {
let stdout = r#"{
"vulnerabilities": {
"found": true,
"count": 2,
"list": [
{"advisory": {"id": "RUSTSEC-2023-0001"}},
{"advisory": {"id": "RUSTSEC-2023-0002"}}
]
}
}"#;
let status = classify_cargo_audit_status(false, stdout, stdout);
assert_eq!(status, CheckStatus::Failed);
}
#[test]
fn test_cargo_audit_clean_report_is_passed() {
let stdout = r#"{
"vulnerabilities": {
"found": false,
"count": 0,
"list": []
}
}"#;
let status = classify_cargo_audit_status(true, stdout, stdout);
assert_eq!(status, CheckStatus::Passed);
}
#[test]
fn test_cargo_audit_warning_only_is_warnings() {
let stdout = r#"{
"vulnerabilities": {
"found": false,
"count": 0,
"list": []
}
}"#;
let stderr = "warning: advisory database is stale";
let combined = format!("{}\n{}", stdout, stderr);
let status = classify_cargo_audit_status(false, stdout, &combined);
assert_eq!(status, CheckStatus::Warnings);
}
#[test]
fn test_cargo_audit_non_json_failure_is_failed() {
let combined = "error: failed to fetch advisory db";
let status = classify_cargo_audit_status(false, "not-json", combined);
assert_eq!(status, CheckStatus::Failed);
}
#[test]
fn test_cargo_geiger_warning_flood_is_non_blocking_warning() {
let output = "\
WARNING: Dependency file was never scanned: /tmp/dep.rs
WARNING: Dependency file was never scanned: /tmp/dep2.rs
error: Found 2 warnings
";
let status = classify_cargo_geiger_status(false, output);
assert_eq!(status, CheckStatus::Warnings);
}
#[test]
fn test_cargo_geiger_real_failures_stay_errors() {
let output = "error: cargo-geiger panicked";
let status = classify_cargo_geiger_status(false, output);
assert_eq!(status, CheckStatus::Error);
}
#[test]
fn test_cargo_geiger_virtual_manifest_degrades_to_skipped() {
let output = "manifest path `/repo/Cargo.toml` is a virtual manifest, \
but this command requires running against an actual package in this workspace";
let status = classify_cargo_geiger_status(false, output);
assert_eq!(status, CheckStatus::Skipped);
}
const GEIGER_RATIO_WITH_UNSAFE: &str = "\
Metric output format: x/y=z%
x = safe code found in the crate
y = total code found in the crate
z = percentage of safe ratio as defined by x/y
Symbols:
:) = No `unsafe` usage found, declares #![forbid(unsafe_code)]
? = No `unsafe` usage found, missing #![forbid(unsafe_code)]
! = `unsafe` usage found
Functions Expressions Impls Traits Methods Dependency
2/2=100.00% 1/2=50.00% 0/0=100.00% 0/0=100.00% 0/0=100.00% ! geigertest 0.1.0
2/2=100.00% 1/2=50.00% 0/0=100.00% 0/0=100.00% 0/0=100.00%
";
const GEIGER_RATIO_ALL_SAFE: &str = "\
Symbols:
:) = No `unsafe` usage found, declares #![forbid(unsafe_code)]
? = No `unsafe` usage found, missing #![forbid(unsafe_code)]
! = `unsafe` usage found
Functions Expressions Impls Traits Methods Dependency
5/5=100.00% 3/3=100.00% 0/0=100.00% 0/0=100.00% 1/1=100.00% ? safecrate 0.1.0
5/5=100.00% 3/3=100.00% 0/0=100.00% 0/0=100.00% 1/1=100.00%
";
#[test]
fn cargo_geiger_unsafe_ratio_is_warnings_not_passed() {
let status = classify_cargo_geiger_status(true, GEIGER_RATIO_WITH_UNSAFE);
assert_eq!(status, CheckStatus::Warnings);
}
#[test]
fn cargo_geiger_all_safe_ratio_is_passed() {
let status = classify_cargo_geiger_status(true, GEIGER_RATIO_ALL_SAFE);
assert_eq!(status, CheckStatus::Passed);
}
#[test]
fn cargo_geiger_success_without_ratio_table_is_not_green() {
let status = classify_cargo_geiger_status(true, "some unexpected geiger output\n");
assert_eq!(status, CheckStatus::Warnings);
}
#[test]
fn geiger_unsafe_found_reads_ratio_cells() {
assert_eq!(geiger_unsafe_found(GEIGER_RATIO_WITH_UNSAFE), Some(true));
assert_eq!(geiger_unsafe_found(GEIGER_RATIO_ALL_SAFE), Some(false));
assert_eq!(geiger_unsafe_found("no ratios here"), None);
assert_eq!(geiger_unsafe_found("0/0=100.00%"), Some(false));
}
#[test]
fn clippy_build_script_warnings_do_not_trip_warn_status() {
let combined = "\n\
warning: codescribe-core@0.12.2: Embedding MiniLM model from: /Users/x/models\n\
warning: codescribe-core@0.12.2: Embedded models for codescribe: Whisper=runtime_load_from_cache\n \
Compiling codescribe v0.12.2 (/repo)\n \
Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.46s\n";
assert!(!clippy_has_real_warnings(combined));
}
#[test]
fn clippy_real_lint_warning_trips_warn_status() {
let combined =
"src/main.rs:10:5: warning: unused variable `x`\nwarning: 1 warning emitted\n";
assert!(clippy_has_real_warnings(combined));
}
#[test]
fn clippy_mixed_build_script_and_real_warning_is_real() {
let combined = "\
warning: dep-crate@1.2.3: building native lib\n\
src/lib.rs:3:1: warning: function `foo` is never used\n";
assert!(clippy_has_real_warnings(combined));
}
#[test]
fn is_cargo_build_script_warning_detection() {
assert!(is_cargo_build_script_warning(
"warning: codescribe-core@0.12.2: Embedding MiniLM model from: /x"
));
assert!(is_cargo_build_script_warning(
"warning: some-crate@1.0.0-beta.1: doing native build work"
));
assert!(!is_cargo_build_script_warning(
"src/main.rs:10:5: warning: unused variable `x`"
));
assert!(!is_cargo_build_script_warning(
"warning: unused import: `std::io`"
));
assert!(!is_cargo_build_script_warning(
"warning: 2 warnings emitted"
));
}
#[test]
fn test_is_virtual_manifest_error_detection() {
assert!(is_virtual_manifest_error(
"Cargo.toml is a virtual manifest, but this command requires running against an actual package",
));
assert!(!is_virtual_manifest_error("error: Found 3 warnings"));
assert!(!is_virtual_manifest_error("a virtual manifest"));
}
}