use clap::Parser;
use serde_json::Value;
use serde_json::from_str;
use std::env;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use stern4rust::reporting::run_outcome::RunOutcome;
use stern4rust::runner::Runner;
use stern4rust::settings::args::Args;
use stern4rust::settings::config_file::ConfigFile;
const THIS_CRATE: &str = "cargo-stern4rust";
fn args_from(parts: &[&str]) -> Args {
Args::parse_from(parts.iter().map(|part| (*part).to_string()))
}
fn config_directory(name: &str, contents: &str) -> PathBuf {
let path = probe_package(name);
fs::write(path.join("stern4rust.toml"), contents).expect("write the config");
path
}
fn header_file(name: &str, contents: &str) -> PathBuf {
let path = env::temp_dir().join(format!("stern4rust_header_{name}.txt"));
fs::write(&path, contents).expect("write the header file");
path
}
fn probe_package(name: &str) -> PathBuf {
let path = env::temp_dir().join(format!("stern4rust_run_{name}"));
let _ = fs::remove_dir_all(&path);
fs::create_dir_all(path.join("src")).expect("create the package");
fs::write(
path.join("Cargo.toml"),
"[package]
name = \"probe\"
version = \"0.1.0\"
edition = \"2021\"
",
)
.expect("write the manifest");
fs::write(
path.join("src/lib.rs"),
"pub mod widget;
",
)
.expect("write the registry");
fs::write(
path.join("src/widget.rs"),
"pub struct Widget;
",
)
.expect("write the module");
path
}
fn probe_twin_workspace(name: &str) -> PathBuf {
let root = env::temp_dir().join(format!("stern4rust_twin_{name}"));
let _ = fs::remove_dir_all(&root);
for member in ["alpha", "beta"] {
fs::create_dir_all(root.join(member).join("src")).expect("create the member");
fs::write(
root.join(member).join("Cargo.toml"),
format!(
"[package]
name = \"{member}\"
version = \"0.1.0\"
edition = \"2021\"
"
),
)
.expect("write the member manifest");
fs::write(
root.join(member).join("src/lib.rs"),
"pub mod widget;
",
)
.expect("write the registry");
fs::write(
root.join(member).join("src/widget.rs"),
"pub fn widget_count() -> usize {
0
}
",
)
.expect("write the module");
}
fs::write(
root.join("Cargo.toml"),
"[workspace]
resolver = \"2\"
members = [\"alpha\", \"beta\"]
",
)
.expect("write the workspace manifest");
root
}
fn probe_workspace(name: &str) -> PathBuf {
let root = env::temp_dir().join(format!("stern4rust_ws_{name}"));
let _ = fs::remove_dir_all(&root);
for member in ["alpha", "beta"] {
fs::create_dir_all(root.join(member).join("src")).expect("create the member");
fs::write(
root.join(member).join("Cargo.toml"),
format!(
"[package]
name = \"{member}\"
version = \"0.1.0\"
edition = \"2021\"
"
),
)
.expect("write the member manifest");
fs::write(
root.join(member).join("src/lib.rs"),
"pub mod widget;
",
)
.expect("write the registry");
fs::write(
root.join(member).join("src/widget.rs"),
"pub struct Widget;
",
)
.expect("write the module");
}
fs::write(
root.join("Cargo.toml"),
"[workspace]
resolver = \"2\"
members = [\"alpha\", \"beta\"]
",
)
.expect("write the workspace manifest");
fs::write(
root.join(ConfigFile::NAME),
"[package.beta]
skip = [\"test-free-source\"]
",
)
.expect("write the config");
root
}
fn report_for(root: &Path, package: Option<&str>) -> String {
let manifest = root.join("Cargo.toml");
let mut parts = vec![
"cargo-stern4rust".to_string(),
"--manifest-path".to_string(),
manifest.to_string_lossy().into_owned(),
];
if let Some(name) = package {
parts.push("--package".to_string());
parts.push(name.to_string());
}
Runner::run_reporting(args_from(
&parts.iter().map(String::as_str).collect::<Vec<_>>(),
))
.expect("the run itself should succeed")
.1
}
fn run_with_header(name: &str, contents: &str) -> RunOutcome {
let path = header_file(name, contents);
Runner::run(args_from(&[
"cargo-stern4rust",
"--manifest-path",
"Cargo.toml",
"--package",
THIS_CRATE,
"--header-file",
&path.to_string_lossy(),
]))
.expect("the run itself should succeed")
}
fn run_workspace(root: &Path, package: Option<&str>) -> Result<RunOutcome, anyhow::Error> {
let manifest = root.join("Cargo.toml");
let mut parts = vec![
"cargo-stern4rust".to_string(),
"--manifest-path".to_string(),
manifest.to_string_lossy().into_owned(),
];
if let Some(name) = package {
parts.push("--package".to_string());
parts.push(name.to_string());
}
Runner::run(args_from(
&parts.iter().map(String::as_str).collect::<Vec<_>>(),
))
}
#[test]
fn run_against_an_unknown_package_is_an_error() {
let path = header_file("unknown_package", "// Copyright 2025\n");
let args = args_from(&[
"cargo-stern4rust",
"--manifest-path",
"Cargo.toml",
"--package",
"no-such-package",
"--header-file",
&path.to_string_lossy(),
]);
let result = Runner::run(args);
assert!(result.is_err());
}
#[test]
fn run_against_this_crate_with_a_header_it_does_not_carry_reports_rules_broken() {
let outcome = run_with_header("foreign", "// Copyright 1999 Someone Else\n");
assert_eq!(outcome, RunOutcome::RulesBroken);
}
#[test]
fn run_against_this_crate_with_its_own_header_is_clean() {
let outcome = run_with_header(
"own",
"// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>\n\
// Licensed under the MIT License\n\
// SPDX-License-Identifier: MIT\n",
);
assert_eq!(outcome, RunOutcome::Clean);
}
#[test]
fn run_over_a_whole_workspace_with_a_section_succeeds() {
let root = probe_workspace("whole");
let result = run_workspace(&root, None);
assert!(result.is_ok(), "{:?}", result.err());
}
#[test]
fn run_over_a_workspace_whose_section_names_no_member_is_an_error() {
let root = probe_workspace("typo");
fs::write(
root.join(ConfigFile::NAME),
"[package.gamma]
skip = [\"test-free-source\"]
",
)
.expect("write the config");
let result = run_workspace(&root, None);
let error = result.expect_err("a section naming no member must not pass");
assert!(format!("{error}").contains("gamma"));
}
#[test]
fn run_over_the_whole_workspace_reports_the_stand_down_its_section_asks_for() {
let root = probe_workspace("report_whole");
let report = report_for(&root, None);
assert!(report.contains("test-free-source (skipped)"), "{report}");
assert!(report.contains("rules_skipped=1"), "{report}");
}
#[test]
fn run_reporting_in_json_carries_a_roster_for_each_package() {
let root = probe_workspace("json_rosters");
let manifest = root.join("Cargo.toml");
let (_, report) = Runner::run_reporting(args_from(&[
"cargo-stern4rust",
"--manifest-path",
&manifest.to_string_lossy(),
"--format",
"json",
]))
.expect("the run itself should succeed");
let document: Value = from_str(&report).expect("valid json");
let packages = document["packages"].as_array().expect("a packages array");
let named: Vec<&str> = packages
.iter()
.filter_map(|package| package["package"].as_str())
.collect();
assert_eq!(named, vec!["alpha", "beta"]);
}
#[test]
fn run_reporting_in_json_names_the_member_that_stood_a_rule_down() {
let root = probe_workspace("json_skip");
let manifest = root.join("Cargo.toml");
let (_, report) = Runner::run_reporting(args_from(&[
"cargo-stern4rust",
"--manifest-path",
&manifest.to_string_lossy(),
"--format",
"json",
]))
.expect("the run itself should succeed");
let document: Value = from_str(&report).expect("valid json");
let beta = document["packages"]
.as_array()
.expect("array")
.iter()
.find(|package| package["package"] == "beta")
.expect("beta");
assert_eq!(beta["rules_skipped"][0], "test-free-source");
let alpha = document["packages"]
.as_array()
.expect("array")
.iter()
.find(|package| package["package"] == "alpha")
.expect("alpha");
assert_eq!(alpha["rules_skipped"].as_array().expect("array").len(), 0);
}
#[test]
fn run_reporting_over_twin_members_counts_each_members_offence() {
let root = probe_twin_workspace("counts_each");
let report = report_for(&root, None);
let occurrences = report.matches("src/widget.rs").count();
assert!(
occurrences >= 2,
"expected a finding per member, got {occurrences} in {report}"
);
}
#[test]
fn run_reporting_over_twin_members_in_json_carries_both_findings() {
let root = probe_twin_workspace("json_both");
let manifest = root.join("Cargo.toml");
let (_, report) = Runner::run_reporting(args_from(&[
"cargo-stern4rust",
"--manifest-path",
&manifest.to_string_lossy(),
"--format",
"json",
"--rule",
"tested-public-api",
]))
.expect("the run itself should succeed");
let document: Value = from_str(&report).expect("valid json");
let offences = document["offences"].as_array().expect("an offences array");
assert_eq!(offences.len(), 2, "{report}");
assert_eq!(document["offences_found"], 2);
}
#[test]
fn run_reporting_with_rules_in_json_carries_the_same_rules_as_the_text() {
let (_, report) = Runner::run_reporting(args_from(&[
"cargo-stern4rust",
"--manifest-path",
"Cargo.toml",
"--rules",
"--format",
"json",
]))
.expect("listing the rules should succeed");
let parsed: Value = from_str(&report).expect("valid json");
let rules = parsed["rules"].as_array().expect("a rules array");
assert_eq!(rules.len(), 21);
assert!(rules.iter().all(|entry| {
!entry["name"].as_str().unwrap_or_default().is_empty()
&& !entry["breaks"].as_str().unwrap_or_default().is_empty()
&& !entry["instead"].as_str().unwrap_or_default().is_empty()
}));
}
#[test]
fn run_reporting_with_rules_lists_every_rule_with_an_example_and_a_remedy() {
let (outcome, report) = Runner::run_reporting(args_from(&[
"cargo-stern4rust",
"--manifest-path",
"Cargo.toml",
"--rules",
]))
.expect("listing the rules should succeed");
assert_eq!(outcome, RunOutcome::Clean);
assert!(report.contains("ordered-imports"), "{report}");
assert!(
report.contains("Imports in src/ run in alphabetic order."),
"{report}"
);
assert!(report.contains("use zzz::Zed;"), "{report}");
}
#[test]
fn run_reporting_with_rules_names_every_rule_the_registry_holds() {
let expected = [
"readable-source",
"arrange-act-assert",
"declared-by-name",
"directory-file-count",
"directory-subfolder-count",
"imported-paths",
"module-registry",
"ordered-imports",
"paired-test-file",
"pure-traits",
"registry-completeness",
"single-implemented-type",
"spdx-matches-manifest",
"test-file-name-postfix",
"test-file-structure",
"test-free-source",
"test-naming",
"tested-public-api",
"tests-layout",
"workspace-dependencies",
"header",
];
let (_, report) = Runner::run_reporting(args_from(&[
"cargo-stern4rust",
"--manifest-path",
"Cargo.toml",
"--rules",
]))
.expect("listing the rules should succeed");
let missing: Vec<&str> = expected
.iter()
.copied()
.filter(|name| !report.contains(name))
.collect();
assert!(missing.is_empty(), "missing: {missing:?}");
}
#[test]
fn run_scoped_to_a_member_never_names_an_applied_rule_as_skipped() {
let root = probe_workspace("report_agree");
let report = report_for(&root, Some("alpha"));
let applied = report
.lines()
.find(|line| line.trim_start().starts_with("applied:"))
.expect("a roster");
assert!(applied.contains("test-free-source"), "{report}");
assert!(!report.contains("test-free-source (skipped)"), "{report}");
}
#[test]
fn run_scoped_to_a_member_without_a_section_reports_nothing_skipped() {
let root = probe_workspace("report_alpha");
let report = report_for(&root, Some("alpha"));
assert!(
!report.contains("(skipped)"),
"alpha has no section, so nothing was stood down:\n{report}"
);
assert!(report.contains("rules_skipped=0"), "{report}");
}
#[test]
fn run_scoped_to_a_member_without_a_section_succeeds() {
let root = probe_workspace("scoped_alpha");
let result = run_workspace(&root, Some("alpha"));
assert!(
result.is_ok(),
"a section for another member must not fail this run: {:?}",
result.err()
);
}
#[test]
fn run_scoped_to_the_member_that_has_the_section_succeeds() {
let root = probe_workspace("scoped_beta");
let result = run_workspace(&root, Some("beta"));
assert!(result.is_ok(), "{:?}", result.err());
}
#[test]
fn run_scoped_to_the_member_with_a_section_reports_its_own_stand_down() {
let root = probe_workspace("report_beta");
let report = report_for(&root, Some("beta"));
assert!(report.contains("test-free-source (skipped)"), "{report}");
assert!(report.contains("rules_skipped=1"), "{report}");
}
#[test]
fn run_with_a_config_file_applies_its_settings() {
let path = config_directory(
"applies",
"rules = [\"tests-layout\"]
",
);
let outcome = Runner::run(args_from(&[
"cargo-stern4rust",
"--manifest-path",
&path.join("Cargo.toml").to_string_lossy(),
]))
.expect("the run itself should succeed");
assert_eq!(outcome, RunOutcome::Clean);
}
#[test]
fn run_with_a_written_baseline_forgives_the_old_and_still_fails_on_the_new() {
let path = config_directory(
"baseline",
"rules = [\"imported-paths\"]
",
);
let widget = path.join("src/widget.rs");
fs::write(
&widget,
"pub struct W;
impl W { pub fn go() { let _ = std::env::args(); } }
",
)
.expect("write the offence");
let manifest = path.join("Cargo.toml").to_string_lossy().to_string();
let judge = || {
Runner::run(args_from(&[
"cargo-stern4rust",
"--manifest-path",
&manifest,
]))
.expect("the run itself should succeed")
};
assert_eq!(
judge(),
RunOutcome::RulesBroken,
"the offence should be seen"
);
Runner::run(args_from(&[
"cargo-stern4rust",
"--manifest-path",
&manifest,
"--write-baseline",
]))
.expect("writing the baseline should succeed");
assert_eq!(
judge(),
RunOutcome::Clean,
"the recorded offence is forgiven"
);
fs::write(
&widget,
"pub struct W;
impl W { pub fn go() { let _ = std::env::args(); let _ = std::env::vars(); } }
",
)
.expect("introduce a new offence");
assert_eq!(
judge(),
RunOutcome::RulesBroken,
"a new offence still fails"
);
}
#[test]
fn run_with_an_exclusion_covering_every_file_finds_nothing_to_judge() {
let path = header_file(
"excluded",
"// nobody carries this header
",
);
let outcome = Runner::run(args_from(&[
"cargo-stern4rust",
"--manifest-path",
"Cargo.toml",
"--package",
THIS_CRATE,
"--header-file",
&path.to_string_lossy(),
"--exclude",
"**/*.rs",
]))
.expect("the run itself should succeed");
assert_eq!(outcome, RunOutcome::Clean);
}
#[test]
fn run_with_an_invalid_config_file_is_an_error() {
let path = config_directory(
"invalid",
"rules = 7
",
);
let outcome = Runner::run(args_from(&[
"cargo-stern4rust",
"--manifest-path",
&path.join("Cargo.toml").to_string_lossy(),
]));
assert!(outcome.is_err());
}
#[test]
fn run_with_an_unknown_rule_name_is_an_error() {
let args = args_from(&[
"cargo-stern4rust",
"--manifest-path",
"Cargo.toml",
"--skip",
"test-file-strucutre",
]);
let result = Runner::run(args);
assert!(result.is_err());
}
#[test]
fn run_with_an_unreadable_header_file_is_an_error() {
let absent = env::temp_dir().join("stern4rust_header_absent.txt");
let _ = fs::remove_file(&absent);
let args = args_from(&[
"cargo-stern4rust",
"--manifest-path",
"Cargo.toml",
"--header-file",
&absent.to_string_lossy(),
]);
let result = Runner::run(args);
assert!(result.is_err());
}
#[test]
fn run_with_an_unusable_exclude_pattern_is_an_error() {
let outcome = Runner::run(args_from(&[
"cargo-stern4rust",
"--manifest-path",
"Cargo.toml",
"--package",
THIS_CRATE,
"--exclude",
"fixture/[",
]));
assert!(outcome.is_err());
}
#[test]
fn run_with_the_header_rule_selected_but_no_header_file_is_an_error() {
let path = probe_package("header_rule_with_no_header_file");
let args = args_from(&[
"cargo-stern4rust",
"--manifest-path",
path.join("Cargo.toml").to_str().expect("manifest path"),
"--rule",
"header",
]);
let result = Runner::run(args);
assert!(result.is_err());
}
#[test]
fn run_without_a_header_file_still_applies_the_rules_that_need_no_configuration() {
let args = args_from(&["cargo-stern4rust", "--manifest-path", "Cargo.toml"]);
let result = Runner::run(args);
assert!(result.is_ok());
}