Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions crates/fensu-cli/src/check/_helpers/evaluation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,23 @@ fn evaluate_rust_target(request: EvaluationRequest<'_>) -> Result<TargetEvaluati
.iter()
.map(|source| source.target_path.as_str())
.collect::<HashSet<_>>();
let rust_source_count = source_paths
.iter()
.filter(|path| path.ends_with(".rs"))
.count();
if rust_source_count > 0 && analysis.facts.files.is_empty() {
let cause = analysis
.diagnostics
.iter()
.find(|diagnostic| diagnostic.code == fensu_rust::METADATA_SETUP_CODE)
.map_or_else(
|| "Cargo reported no workspace source files".to_owned(),
|diagnostic| diagnostic.message.clone(),
);
return Err(format!(
"Rust analysis found 0 Rust files, but the target has {rust_source_count}; no Rust rule can be evaluated. {cause}"
));
}
let mut faults: Vec<Fault> = Vec::new();
for diagnostic in analysis.diagnostics {
let diagnostic_path = diagnostic.path.to_string_lossy().replace('\\', "/");
Expand Down
81 changes: 79 additions & 2 deletions crates/fensu-cli/tests/command/test_check_rust.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::helpers::{require_workspace_python, run_check, run_check_with, write};
use crate::test_types::{
InvalidCheckConfigTestCase, RustCacheTestCase, RustCheckTestCase,
RustCustomCacheInvalidationTestCase, RustCustomCoverageTestCase, RustCustomFileRuleTestCase,
RustCustomHostRoutingTestCase, RustCustomMalformedSourceTestCase,
RustCustomCacheInvalidationTestCase, RustCustomCoverageTestCase, RustCustomFailClosedTestCase,
RustCustomFileRuleTestCase, RustCustomHostRoutingTestCase, RustCustomMalformedSourceTestCase,
RustCustomNarrowCacheTestCase, RustCustomPolicyTestCase, RustMetadataCacheTestCase,
};

Expand Down Expand Up @@ -919,6 +919,83 @@ fn given_malformed_rust_when_custom_rule_reads_file_facts_then_parse_error_remai
}
}

#[test]
fn given_unavailable_rust_facts_when_custom_rule_is_selected_then_check_fails_closed() {
let python = require_workspace_python!();
let package = "[package]\nname = \"example\"\nversion = \"0.1.0\"\nedition = \"2021\"\n";
let test_cases = [
RustCustomFailClosedTestCase {
description: "cargo cannot run",
manifest: package,
rule_suffix: "",
search_path: |empty| empty.as_os_str().to_owned(),
expected_error: "failed to start `cargo metadata`",
},
RustCustomFailClosedTestCase {
description: "workspace has Rust sources but no packages",
manifest: "[workspace]\nmembers = []\nresolver = \"2\"\n",
rule_suffix: "",
search_path: |_| std::env::var_os("PATH").unwrap_or_default(),
expected_error: "Rust analysis found 0 Rust files, but the target has 1",
},
RustCustomFailClosedTestCase {
description: "host reply exceeds the capture limit",
manifest: package,
rule_suffix: "import sys\nsys.stdout.write('x' * (17 * 1024 * 1024))\n",
search_path: |_| std::env::var_os("PATH").unwrap_or_default(),
expected_error: "stdout exceeded its 16777216-byte capture limit",
},
];
for test_case in test_cases {
let repository = tempfile::tempdir().expect("temporary repository");
let empty_path = tempfile::tempdir().expect("empty search path");
write(repository.path().join("Cargo.toml"), test_case.manifest);
write(
repository.path().join("src/lib.rs"),
"pub struct LegacyThing;\n",
);
write(
repository.path().join("rules/custom.py"),
&format!(
"from fensu import AnalyzerId, Family, Fault, File, RuleContext, rule\n@rule(code='XRS006', family=Family.CUSTOM, slug='any-file', message='file seen', analyzers=(AnalyzerId.RUST,), cacheable=True)\ndef any_file(*, file: File, ctx: RuleContext) -> list[Fault]:\n return [ctx.path_fault()] if ctx.rust.file(file) is not None else []\n{}",
test_case.rule_suffix
),
);
write(
repository.path().join("fensu.toml"),
"[targets.rust]\nanalyzer = \"rust\"\nroots = [\"src\"]\ntests = []\ntooling = []\nrule_packs = [\"rust\"]\nrule_paths = [\"rules/custom.py\"]\nselect = [\"XRS006\"]\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", &python)
.env("PATH", (test_case.search_path)(empty_path.path()))
.env_remove("CARGO")
.output()
.expect("custom Rust check runs");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);

assert_eq!(
output.status.code(),
Some(2),
"{}: {stdout} {stderr}",
test_case.description
);
assert!(
stderr.contains(test_case.expected_error),
"{}: {stderr}",
test_case.description
);
assert!(
!stdout.contains("Found 0 faults"),
"{}: {stdout}",
test_case.description
);
}
}

#[test]
fn given_custom_rust_policy_when_checking_then_warning_ignore_and_exception_semantics_are_shared() {
let python = require_workspace_python!();
Expand Down
8 changes: 8 additions & 0 deletions crates/fensu-cli/tests/command/test_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ pub(crate) struct RustCustomNarrowCacheTestCase {
pub(crate) expected_reused_message: &'static str,
}

pub(crate) struct RustCustomFailClosedTestCase {
pub(crate) description: &'static str,
pub(crate) manifest: &'static str,
pub(crate) rule_suffix: &'static str,
pub(crate) search_path: fn(&std::path::Path) -> std::ffi::OsString,
pub(crate) expected_error: &'static str,
}

pub(crate) struct RustCustomMalformedSourceTestCase {
pub(crate) description: &'static str,
pub(crate) expected_exit_code: i32,
Expand Down
5 changes: 4 additions & 1 deletion docs/rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,10 @@ by family.
Rust sources, Cargo manifests, existing lockfiles, and target settings are cache inputs.
Existing lockfiles are read with locked metadata resolution. Lock-free workspaces use declared
dependency metadata without creating a lockfile. A Cargo metadata setup failure produces a
diagnostic and prevents caching, even if that diagnostic is excluded by rule selection.
diagnostic and prevents caching, even if that diagnostic is excluded by rule selection. When the
target has Rust sources but Cargo yields no source files, for example because `cargo` is not on
`PATH` or the workspace has no packages, `fensu check` exits 2 with the cause instead of
reporting a result.
## File placement and embedded resources

The `rust` pack enables `FPRSR311` (`rust-rsr311`) and `FPRSL106` (`rust-rsl106`)
Expand Down
3 changes: 3 additions & 0 deletions website/src/content/docs/docs/concepts/analyzers/rust.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,6 @@ lockfile.

If Cargo metadata cannot be read, Fensu reports a setup diagnostic and does not cache
the result. The result remains uncached even when rule selection hides that diagnostic.
When the target has Rust sources but Cargo yields no source files, for example because
`cargo` is not on `PATH` or the workspace has no packages, `fensu check` exits 2 with
the cause instead of reporting a result.
Loading