From 473b01bd56f1e748bd7d80bca7e9b1c431d44e66 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Tue, 10 Nov 2026 00:32:12 +0000 Subject: [PATCH] fix(check): say a mixed check is incomplete when a target cannot run When one target of a mixed check cannot run, for example a Rust target without cargo next to a Python target with custom rules, fensu check already exits 2 and prints the error, but the report of the targets that did run ended with "Found 0 faults", which reads like a clean check. The summary now says "Check incomplete: found N faults in the targets that ran". Those targets' faults are still printed. --- .../src/check/_helpers/evaluation.rs | 3 +- .../fensu-cli/src/check/_helpers/execution.rs | 3 +- .../src/check/main/render_check_results.rs | 11 +++- .../_helpers/check_partition/execution.rs | 2 +- crates/fensu-cli/src/reporting/main/report.rs | 11 +++- crates/fensu-cli/src/reporting/models.rs | 2 + crates/fensu-cli/src/tests/test_render.rs | 1 + .../tests/command/test_check_mixed_targets.rs | 55 +++++++++++++++++++ crates/fensu-cli/tests/command/test_types.rs | 7 +++ docs/rust.md | 8 +++ 10 files changed, 95 insertions(+), 8 deletions(-) diff --git a/crates/fensu-cli/src/check/_helpers/evaluation.rs b/crates/fensu-cli/src/check/_helpers/evaluation.rs index 98648644..3440ce12 100644 --- a/crates/fensu-cli/src/check/_helpers/evaluation.rs +++ b/crates/fensu-cli/src/check/_helpers/evaluation.rs @@ -946,7 +946,7 @@ pub(crate) fn render_results( mut results: Vec, root: &Path, color: bool, - show_warnings: bool, + (show_warnings, incomplete): (bool, bool), ) -> (String, i32) { let selected = results.iter().map(|result| result.selected).sum::(); let excluded = results.iter().map(|result| result.excluded).sum::(); @@ -1009,6 +1009,7 @@ pub(crate) fn render_results( evaluation_summary: summary.as_deref(), applied_exceptions, threshold_uses: &threshold_uses, + incomplete, }); (output, i32::from(!faults.is_empty())) } diff --git a/crates/fensu-cli/src/check/_helpers/execution.rs b/crates/fensu-cli/src/check/_helpers/execution.rs index 1897247d..97a4cd29 100644 --- a/crates/fensu-cli/src/check/_helpers/execution.rs +++ b/crates/fensu-cli/src/check/_helpers/execution.rs @@ -44,7 +44,8 @@ pub(crate) fn render_check( let cacheable = results .iter() .all(|result| result.cacheable.unwrap_or(true)); - let (output, exit_code) = render_results(results, &plan.root, plan.color, options.warn); + let (output, exit_code) = + render_results(results, &plan.root, plan.color, (options.warn, false)); let mut stderr = String::new(); stderr.push_str(&freshness(&plan)); if options.cache_stats { diff --git a/crates/fensu-cli/src/check/main/render_check_results.rs b/crates/fensu-cli/src/check/main/render_check_results.rs index cf38d37c..74ea326b 100644 --- a/crates/fensu-cli/src/check/main/render_check_results.rs +++ b/crates/fensu-cli/src/check/main/render_check_results.rs @@ -1,4 +1,4 @@ -//! Render one aggregate set of analyzer check results. +//! Render one aggregate set of analyzer check results, marked incomplete when a target could not run. use std::path::Path; @@ -8,7 +8,12 @@ pub(crate) fn render_check_results( results: Vec, root: &Path, color: bool, - show_warnings: bool, + (show_warnings, incomplete): (bool, bool), ) -> (String, i32) { - crate::check::_helpers::evaluation::render_results(results, root, color, show_warnings) + crate::check::_helpers::evaluation::render_results( + results, + root, + color, + (show_warnings, incomplete), + ) } diff --git a/crates/fensu-cli/src/command/_helpers/check_partition/execution.rs b/crates/fensu-cli/src/command/_helpers/check_partition/execution.rs index 4b7ea35d..96b17631 100644 --- a/crates/fensu-cli/src/command/_helpers/check_partition/execution.rs +++ b/crates/fensu-cli/src/command/_helpers/check_partition/execution.rs @@ -218,7 +218,7 @@ fn structured_check(request: StructuredCheckRequest<'_>) -> CliOutput { let (stdout, report_exit_code) = if command_error && results.is_empty() { (String::new(), 0) } else { - render_check_results(results, &root, color, show_warnings) + render_check_results(results, &root, color, (show_warnings, command_error)) }; let mut stderr = String::new(); if show_cache_stats { diff --git a/crates/fensu-cli/src/reporting/main/report.rs b/crates/fensu-cli/src/reporting/main/report.rs index 396f1bfe..3eabb18d 100644 --- a/crates/fensu-cli/src/reporting/main/report.rs +++ b/crates/fensu-cli/src/reporting/main/report.rs @@ -14,7 +14,14 @@ pub(crate) fn report(request: ReportRequest<'_>) -> String { } else { "faults" }; - let mut summary = format!("Found {} {noun}", request.faults.len()); + let mut summary = if request.incomplete { + format!( + "Check incomplete: found {} {noun} in the targets that ran", + request.faults.len() + ) + } else { + format!("Found {} {noun}", request.faults.len()) + }; if request.show_warnings { let noun = if request.warnings.len() == 1 { "warning" @@ -24,7 +31,7 @@ pub(crate) fn report(request: ReportRequest<'_>) -> String { summary.push_str(&format!(" and {} {noun}", request.warnings.len())); } if request.color { - let style = if request.faults.is_empty() { + let style = if request.faults.is_empty() && !request.incomplete { GREEN } else { ORANGE diff --git a/crates/fensu-cli/src/reporting/models.rs b/crates/fensu-cli/src/reporting/models.rs index a7c1a3c0..65915398 100644 --- a/crates/fensu-cli/src/reporting/models.rs +++ b/crates/fensu-cli/src/reporting/models.rs @@ -14,4 +14,6 @@ pub(crate) struct ReportRequest<'a> { pub(crate) evaluation_summary: Option<&'a str>, pub(crate) applied_exceptions: usize, pub(crate) threshold_uses: &'a [ThresholdUse], + /// Whether a target could not run, so the faults cover only the targets that did. + pub(crate) incomplete: bool, } diff --git a/crates/fensu-cli/src/tests/test_render.rs b/crates/fensu-cli/src/tests/test_render.rs index 67666948..a0b3af79 100644 --- a/crates/fensu-cli/src/tests/test_render.rs +++ b/crates/fensu-cli/src/tests/test_render.rs @@ -44,6 +44,7 @@ fn given_colored_faults_when_rendering_then_uses_historical_orange_styles() { evaluation_summary: None, applied_exceptions: 0, threshold_uses: &[], + incomplete: false, }); assert_eq!( diff --git a/crates/fensu-cli/tests/command/test_check_mixed_targets.rs b/crates/fensu-cli/tests/command/test_check_mixed_targets.rs index 12081939..3422624a 100644 --- a/crates/fensu-cli/tests/command/test_check_mixed_targets.rs +++ b/crates/fensu-cli/tests/command/test_check_mixed_targets.rs @@ -46,3 +46,58 @@ fn given_custom_python_rust_and_web_rules_when_checking_then_native_and_hosted_t } } } + +#[test] +fn given_custom_python_and_rust_targets_without_cargo_when_checking_then_the_summary_says_incomplete( +) { + let test_cases = [crate::test_types::RustMixedTargetUnavailableTestCase { + description: "a Rust target that cannot run fails the check, and the summary of the \ + Python findings says the check is incomplete", + expected_exit_code: 2, + expected_error: "FPRSL902 Rust analysis cannot run: cargo not found", + expected_summary: "Check incomplete: found 1 fault in the targets that ran", + }]; + for test_case in test_cases { + let repository = tempfile::tempdir().expect("temporary repository"); + write( + repository.path().join("Cargo.toml"), + "[package]\nname = \"example\"\nversion = \"0.1.0\"\nedition = \"2021\"\n", + ); + write( + repository.path().join("src/lib.rs"), + "pub fn sum(left: usize, right: usize) -> usize { left + right }\n", + ); + write( + repository.path().join("python/value.py"), + "def value(item):\n return item\n", + ); + write(repository.path().join("rules/custom.py"), "import ast\nfrom fensu import Family, Fault, RuleContext, rule\n@rule(code='XMIX001', family=Family.CUSTOM, slug='mixed-python', message='mixed Python')\ndef mixed_python(module: ast.Module, ctx: RuleContext) -> list[Fault]:\n return []\n"); + write(repository.path().join("fensu.toml"), "[targets.python]\nanalyzer = \"python\"\nroots = [\"python\"]\ntests = []\ntooling = []\nselect = [\"FFA001\", \"XMIX001\"]\nrule_paths = [\"rules/custom.py\"]\n[targets.rust]\nanalyzer = \"rust\"\nroots = [\"src\"]\ntests = []\ntooling = []\nrule_packs = [\"rust\"]\nselect = [\"FPRSS010\"]\n"); + let output = std::process::Command::new(env!("CARGO_BIN_EXE_fensu")) + .args(["check", "--no-color", "--no-cache"]) + .current_dir(repository.path()) + .env("FENSU_PYTHON", crate::helpers::workspace_python()) + .env("CARGO", "/cargo-does-not-exist/cargo") + .env("PATH", "/cargo-does-not-exist") + .output() + .expect("mixed check runs"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_eq!( + output.status.code(), + Some(test_case.expected_exit_code), + "{}: {stdout} {stderr}", + test_case.description + ); + assert!( + stderr.contains(test_case.expected_error), + "{}: {stderr}", + test_case.description + ); + assert!( + stdout.contains(test_case.expected_summary) && !stdout.contains("Found "), + "{}: {stdout}", + test_case.description + ); + } +} diff --git a/crates/fensu-cli/tests/command/test_types.rs b/crates/fensu-cli/tests/command/test_types.rs index 2983f469..27b5cb0d 100644 --- a/crates/fensu-cli/tests/command/test_types.rs +++ b/crates/fensu-cli/tests/command/test_types.rs @@ -80,6 +80,13 @@ pub(crate) struct RustMixedTargetTestCase { pub(crate) expected_codes: &'static [&'static str], } +pub(crate) struct RustMixedTargetUnavailableTestCase { + pub(crate) description: &'static str, + pub(crate) expected_exit_code: i32, + pub(crate) expected_error: &'static str, + pub(crate) expected_summary: &'static str, +} + pub(crate) struct RepositoryRuleTestCase { pub(crate) description: &'static str, pub(crate) expected_exit_code: i32, diff --git a/docs/rust.md b/docs/rust.md index 037ede93..dde1dae1 100644 --- a/docs/rust.md +++ b/docs/rust.md @@ -212,6 +212,14 @@ FPRSL902 Rust analysis cannot run: cargo not found; install the Rust toolchain ( `FPRSL901` remains the code for a manifest that cannot be read, parsed, or accepted by a working `cargo`. + +When other targets did run, their faults are still printed, and the summary says the check is +incomplete instead of reporting a count for the whole repository: + +```text +Check incomplete: found 1 fault in the targets that ran +``` + ## File placement and embedded resources The `rust` pack enables `FPRSR311` (`rust-rsr311`) and `FPRSL106` (`rust-rsl106`)